规并排序(Swift版本)

news/2024/5/17 19:32:05 标签: swift, 排序算法, 算法, 归并排序

Overview 概述

  1. 时间复杂度为 O(nlogn) ;
  2. 适合大规模的数据排序 ;
  3. 相比于冒泡排序、插入排序、选择排序这三种算法>排序算法, 更加常用 ;
  4. 用到了分治思想(即分而治之, 英文叫 “Divide and conquer”),非常巧妙 ;
  5. 英文名称: Merge Sort ;
  • 分治思想, 在很多领域都有广泛的应用,例如算法领域有分治算法归并排序、快速排序都属于分治算法,二分法查找也是一种分治算法);
  • 分治算法一般都是用"递归"来实现的 (分治是一种解决问题的处理思想,递归是一种编程技巧) ;

Principle 原理

main idea 核心思想

如果要排序一个数组,我们先把数组从中间分成前后两部分,然后对前后两部分分别排序 (递归使用同样的归并排序),再将排好序的两部分合并在一起,这样整个数组就都有序了。

decomposition diagram 分解示意图

<a class=归并排序分解图" />

用递归代码来实现归并排序

// 递推公式:
merge_sort(p…r) = merge(merge_sort(p…q), merge_sort(q+1…r))
// 终止条件 (不用再继续分解)
p >= r 

merge_sort(p…r) 表示,给下标从 p 到 r 之间的数组排序。我们将这个排序问题转化为了两个子问题,merge_sort(p…q) 和 merge_sort(q+1…r),其中下标 q 等于 p 和 r 的中间位置,也就是 (p+r)/2。当下标从 p 到 q 和从 q+1 到 r 这两个子数组都排好序之后,我们再将两个有序的子数组合并在一起,这样下标从 p 到 r 之间的数据就也排好序了。

Show The Swift Code

swift">class Solution {
    func sortArray(_ nums: [Int]) -> [Int] {
        var numbers = nums.map { $0 }
        mergeSort(&numbers, 0, numbers.count - 1)
        return numbers
    }
    
    final func mergeSort(_ numbers: inout [Int], _ leading: Int, _ trailing: Int) {
        guard leading < trailing else { return }
        // 取中间的索引
        let mid = (trailing + leading) >> 1
        // 左侧递归排序
        mergeSort(&numbers, leading, mid)
        // 右侧递归排序
        mergeSort(&numbers, mid + 1, trailing)
        // 合并两个有序区间
        merge(&numbers, leading, trailing, mid)
    }
    
    private final func merge(_ numbers: inout [Int], _ leading: Int, _ trailing: Int, _ mid: Int) {
        var i = leading
        var j = mid + 1
        var tmp = [Int]()
        // 比较分区的首个元素
        while i <= mid, j <= trailing {
            if numbers[i] < numbers[j] {
                tmp.append(numbers[i])
                i = i + 1
            } else {
                tmp.append(numbers[j])
                j = j + 1
            }
        }
        // 将左分区的剩余的元素依次添加到tmp
        while i <= mid {
            tmp.append(numbers[i])
            i = i + 1
        }
        // // 将右分区的剩余的元素依次添加到tmp
        while j <= trailing {
            tmp.append(numbers[j])
            j = j + 1
        }
        // 将完成合并的两个分区, 回写到numbers
        for idx in 0...(trailing - leading) {
            numbers[idx + leading] = tmp[idx]
        }
    }
}

合并分区函数merge(_ numbers: [Int], _ leading: Int, _ trailing: Int, _ mid: Int)

这个函数的作用就是,将已经有序的 numbers[leading...mid]numbers[mid+1....trailing] 合并成一个有序的数组,并且放入 numbers[leading....trailing]。那这个过程具体该如何做呢?如下图所示,每次调用需要申请一个临时数组 tmp。我们用两个游标 i 和 j,分别指向两个分区的第一个元素。比较这两个元素 numbers[i]和 numbers[j],如果 numbers[i]<=numbers[j],我们就把 numbers[i]放入到临时数组 tmp,并且 i 后移一位,否则将 numbers[j]放入到数组 tmp,j 后移一位。继续上述比较过程,直到其中一个子数组中的所有数据都放入临时数组中,再把另一个数组中的数据依次加入到临时数组的末尾,这个时候,临时数组中存储的就是两个子数组合并之后的结果了。最后再把临时数组 tmp 中的数据拷贝到原数组区间 numbers[leaing...trailing]中。

合并分区函数

Performance analysis 性能分析

是否为稳定排序 ?

是, 前提是合并分区时, 当两个分区中相互比对的元素大小相同, 将 leading 分区的元素加入 tmp.

时间复杂度是多少?

O ( n l o g n ) O(nlogn) O(nlogn)

看了几篇Blog, 感觉证明过程都不严谨, 知道怎样证明的欢迎指教🫰🏻

归并排序的执行效率与要排序的原始数组的有序程度无关,所以其时间复杂度是非常稳定的,不管是最好情况、最坏情况,还是平均情况,时间复杂度都是 O(nlogn)。

空间复杂度

O ( n ) O(n) O(n)
归并排序的时间复杂度任何情况下都是 O(nlogn),看起来非常优秀。(即便是快速排序,最坏情况下,时间复杂度也达到了 O(n2)。)但是,归并排序并没有像快排那样,应用广泛,这是为什么呢?因为它有一个致命的“弱点”,那就是归并排序不是原地算法>排序算法

这是因为归并排序的合并函数,在合并两个有序区间为一个有序区间时,需要借助额外的存储空间tmp。尽管每次合并操作都需要申请额外的内存空间,但在合并完成之后,临时开辟的内存空间就被释放掉了。在任意时刻,CPU 只会有一个函数在执行,也就只会有一个临时的内存空间在使用。临时内存空间最大也不会超过 n 个数据的大小,所以空间复杂度是 O(n)。


http://www.niftyadmin.cn/n/125944.html

相关文章

【java】为什么 main 方法是 public static void ?

main 方法是我们学习Java编程语言时知道的第一个方法&#xff0c;你是否曾经想过为什么 main 方法是 public、static、void 的。当然&#xff0c;很多人首先学的是C和C&#xff0c;但是在Java中main方法与前者有些细微的不同&#xff0c;它不会返回任何值&#xff0c;为什么 ma…

关于缓存的理解

关于缓存的理解 为系统引入缓存的理由 通常情况&#xff0c;在我们面临系统的基础设施&#xff0c;例如数据库无法处理量级的请求时候&#xff0c;总是会下意识的使用缓存&#xff0c;这次我们以设计的角度思考&#xff0c;在为你的系统引入缓存之前&#xff0c;它是否真的需…

科目一《综合素质》

目录综合素质重点题型分布注意事项章节分解第一章 职业理念第一节 教育观1. 教育观&#xff08;基本内涵&#xff09;一字不差背过第二节 学生观2. 学生观 一字不差背过第三节 教师观3. 教师观 一字不差背过第二章 教育法律法规第一节 教师的权利与义务第二节 学生的权利及其保…

魏玛早春 木心

<font face“黑体” color#CD5C5C size6 魏玛早春 木心 温带每个季节之初 总有神圣气象恬漠地 剀切地透露在风中 冬天行将退尽 春寒嫩生生 料峭而滋润 漾起离合纷纷的私淑记忆 日复一日 默认季节的更替 以春的正式最为谨慎隆重 如果骤尔明暖 鸟雀疏狂飞鸣 必定会吝悔似的剧…

Android ThreadPoolExecutor的基本使用

ThreadPoolExecutor是Java中的一个线程池类&#xff0c;Android中也可以使用该类来管理自己的线程池&#xff0c;它为我们管理线程提供了很多方便。 线程池是一种能够帮助我们管理和复用线程的机制&#xff0c;它可以有效地降低线程创建和销毁的开销。使用线程池可以避免不必要…

计算机组成原理|第二章(笔记)

目录第二章 计算机的发展及应用2.1 计算机的发展史2.1.1 计算机的生产和发展2.1.2 微型计算机的出现和发展2.1.3 软件技术的兴起与发展2.2 计算机的应用2.3 计算机的展望上篇&#xff1a;第一章&#xff1a;计算机系统概论 第二章 计算机的发展及应用 2.1 计算机的发展史 2.1.…

Read book Netty in action(Chapter IX)--Bootstrapping

序言 在深入地学习了ChannelPipeline、ChannelHandler 和EventLoop 之后&#xff0c;你接下来的问题可能是&#xff1a;“如何将这些部分组织起来&#xff0c;成为一个可实际运行的应用程序呢&#xff1f;” 答案是&#xff1f;“引导”&#xff08;Bootstrapping&#xff09;…

基于半车悬架的轴距预瞄与轴间预瞄仿真对比

目录 前言 1. 半车悬架模型 2.轴距预瞄(单点预瞄)和轴间预瞄(两点预瞄)原理与仿真分析 2.1轴距预瞄(单点预瞄) 2.1.1预瞄原理 2.2.轴间预瞄(两点预瞄) 2.2.1预瞄原理 2.3仿真分析 3.总结 前言 对于悬架而言&#xff0c;四个车轮实际的输入信息是受到前后延时以及左右相…