归并排序是最有效的排序算法之一。它的时间复杂度为O(n log n),是所有通用排序算法中最快的算法之一。归并排序背后的理念是分而治之--将一个大问题分解成几个更小的、更容易解决的问题,然后将这些解决方案归并为一个最终结果。归并排序的口号是先分割,后归并。在本章中,你将从头开始实现归并排序。让我们从一个例子开始。
假设你有一堆没有分类的扑克牌。
归并排序算法的工作原理如下。
打开启动器的playground,开始实施。
在playground的Source文件夹中,创建一个名为MergeSort.swift的新文件。在该文件中写下以下内容。
这里,你把数组分成了两半。分割一次是不够的。然而,你必须不断地递归分割,直到你不能再分割为止,也就是每个细分部分只包含一个元素的时候。
要做到这一点,请更新mergeSort如下。
你在这里做了两个改变。
在这种情况下,基本情况是当数组只有一个元素时。
在你的代码编译之前,还有更多的工作要做。现在你已经完成了分割的部分,是时候关注归并了。
你的最后一步是归并左边和右边的数组。为了保持干净,你将为此创建一个单独的归并函数。
归并函数的唯一职责是接收两个排序的数组,并在保留排序顺序的情况下将其归并。在mergeSort函数的下面添加以下内容。
以下是发生的情况。
leftIndex和rightIndex变量在你解析这两个数组时跟踪你的进度。
结果数组将容纳归并后的数组。
从头开始,你依次比较左右两个数组中的元素。如果你已经到了两个数组的末尾,就没有其他东西可以比较了。
两个元素中较小的那个进入结果数组。如果这两个元素相等,就可以把它们都加起来。
第一个循环保证左边或右边是空的。由于两个数组都是排序的,这就保证了剩下的元素大于或等于当前结果中的元素。在这种情况下,你可以不通过比较来追加其余的元素。
通过调用merge来完成mergeSort函数。因为你以递归方式调用mergeSort,算法会在归并前对两半进行分割和排序。
这段代码是归并排序算法的最终版本。下面是对归并排序的关键程序的总结。
归并排序的策略是分而治之,这样你就能解决很多小问题,而不是一个大问题。
它有两个核心职责:一个是递归地分割初始数组的方法,一个是归并两个数组的方法。
归并函数应该接收两个排序的数组并产生一个单一的排序数组。
最后--是时候看看这个动作了。回到playground的主页面,用下面的方法测试你的归并排序。
example(of: "merge sort") {
let array = [7, 2, 6, 3, 9]
print("Original: \(array)")
print("Merge sorted: \(mergeSort(array))")
}
会得到这样的输出。
---Example of merge sort--
Original: [7, 2, 6, 3, 9]
Merge sorted: [2, 3, 6, 7, 9]
归并排序的最佳、最差和平均时间复杂度都是O(n log n),这还不算太糟。如果你很难理解n log n是怎么来的,可以想想递归是怎么工作的。
一般来说,如果你有一个大小为n的数组,层数是log2(n)。当你递归时,你把一个数组分成两个更小的数组。这意味着一个大小为2的数组需要一个递归级别,一个大小为4的数组需要两个级别,一个大小为8的数组需要三个级别,以此类推。如果你有一个1,024个元素的数组,需要十级递归一分为二才能降到1024个单元素数组。
单个递归的成本是O(n)。一个递归级别将归并n个元素。如果有许多小的归并或一个大的归并,这并不重要;每一级归并的元素数量仍然是n。
这使得总成本达到O(log n) × O(n) = O(n log n)。
前一章的排序算法是就地的,并使用swapAt来移动元素。相比之下,归并排序需要分配额外的内存来完成其工作。有多大?有log2(n)级的递归,每一级都要使用n个元素。这使得总的空间复杂性为O(n log n)。归并排序是标志性的排序算法之一。它相对简单易懂,可以作为分而治之算法工作原理的一个很好的介绍。归并排序是O(n log n),而这个实现需要O(n log n)的空间。如果你巧妙地记账,你可以通过丢弃没有被积极使用的内存,将所需的内存减少到O(n)。
归并排序属于分而治之算法的范畴。
有许多归并排序的实现,根据不同的实现,你可以有不同的性能特征。
上一章 | 目录 | 下一章 |
---|