Swift源码分析-Sort排序
Swift源码分析 - Sort
当我们在Swift中使用排序的时候,我们通常会这样使用:
1 | let array = [1, 3, 5, 7, 9, 0, 8] |
或者这样使用:
1 | var array = [1, 3, 5, 7, 9, 0, 8] |
速度非常的快,比快速排序,插入排序,堆排序等,都快很多.
当我看到Swift的源码的时候才知道为什么排序速度这么快.
IntroSort(内省算法)
将sort.swift.gyb文件转换成sort.swift文件
源码中很多文件不是用Swift写的,而是用Python写的,这个时候我们就要用工具进行转换了,转换的方法很简单.(源码中已经自带转换工具),当然,如果你已经了解Python,那么你直接去源码路径查看就可以。
转换方法:
/swift/stdlib/public/core/Sort.swift.gyb
终端切换到工具目录下:
cd /swift/utils
./gyb --line-directive '' -o sort.swift sort.swift.gyb
Analysis
我看到了很熟悉的排序算法:
1 | func _heapSort |
应该是堆排序,插入排序,快速排序.
- 堆排序的优点:在任何情况下时间复杂度都是一样,稳定性高
- 插入排序优点:在数量很少的情况下可以很快的完成
- 快速排序优点:在数量很大的时候速度是最快的,但是不稳定
这样可以推断出IntroSort算法其实是三者排序算法的综合应用.
但是这个文件中并没有sort函数,根据推断,我们在使用函数的时候都是在一个Collection中,也就是数组中,于是在源码中成功找到文件:
CollectionAlgorithms.swift.gyb
sort - CollectionAlgorithms.swift
而下面这个两个扩展中的sort函数, 都是创建新数组,然后修改返回,所以不会修改原数组,也就是我们经常使用的函数(默认MutableCollection
和Sequence
都实现了一样的方法):
1 | extension Sequence where Self.Iterator.Element : Comparable { |
Note: 由于sorted默认是从小到大排序,所以需要在第一个扩展中加上限定,使得元素可比较,但是第二个采用自定义比较方法,所以不需要加上扩展条件
让我们回到核心代码上:
第一个:
1 | public mutating func sort() { |
第二个:
1 | public mutating func sort( |
分析:
相同点:两个函数前面都加了一个关键字mutating表示是需要修改内容的因为这个扩展的不是Collection,而是MutableCollection.
不同点:
- 第一个函数也就是不给定参数,默认排序为从小到大.
- 给定参数,按参数(其实就是闭包)进行排序.
_introSort(&self, subRange: startIndex..<endIndex)
开始调用introSort算法了
那这个闭包是干嘛的?
1 | (Iterator.Element, Iterator.Element) -> Bool |
得到两个泛型的值,返回一个Bool值?很熟悉?在Swift中,<
号和>
号等都是函数,都是通过判断两个数,之后返回Bool类型,这就是为什么我们可以使用<
号或者>
号来进行简写sort函数了,这个在之后会详解
_introSort - Sort.swift
在源码中有这样一个函数
1 | public // @testable |
泛型符号是C,支持MutableCollection
, RandomAccessCollection
协议(A collection that supports efficient random-access index traversal.Random-access collections can measure move indices any distance and can measure the distance between indices in O(1) time)
let depthLimit = 2 * _floorLog2(Int64(count))
一个计算深度限制的方法,具体的意思在下面会说
下面是解释:
大致意思是这样的:快速排序很快,但是最坏的情况下时间复杂度却很不好,这取决于partition的位置,当partition的位置小于限制深度的时候就采用堆排序来维持一个Nlog(N)的时间复杂度.
之后将参数传递给:
1 | func _introSortImpl<C>( |
这才是整个算法最核心的部分,也就是算法选择的部分.自动选择最优算法来进行排序,根据不同的场景来切换排序算法,达到最优的效果.
1 | // Insertion sort is better at handling smaller regions. |
从这句话我们可以看到限制深度保持在20,这正是插入排序的优点.
1 | if depthLimit == 0 { |
当深度等于0的时候采用堆排序.
1 | // Partition and sort. |
现在就是开始快速排序了,并且每趟快速排序完成之后都会重新将两个子序列进行_introSort
,这样就达到了不同情况选择不同的排序算法:
1 | func _partition<C>( |
看看如何将一个数组分成两半的.
1 | // The first element is the pivot. |
这是偷懒么?优化不够啊!就不使用三点取中?,直接用第一个元素作为中轴?这也许有原因的:要么偷懒.要么就是没必要(多一个判断就多一次消耗时间,我认为这没有必要)
1 | // Loop invariants: |
正常的快速排序.
默认的排序说完了,该说说这个了:
1 | (Iterator.Element, Iterator.Element) -> Bool |
这个闭包的执行是在快速排序,插入排序,堆排序中进行使用的,目的就是判断是升序,还是降序的.
我们就拿快速排序讲解吧(其实很简单):
1 | if !areInIncreasingOrder(elements[lo], pivot) { break FindLo } |
在之前的判断中是这样的:
1 | if !(elements[lo] < pivot) { break FindLo } |
直接就是小于,而这个闭包其实也就是判断两个数大小的然后返回Bool值,这样就可以控制升序降序了.
补充:
在Swift源码中我发现这样几句话:
The sorting algorithm is not stable. A nonstable sort may change the relative order of elements for which
areInIncreasingOrderdoes not establish an order.(排序算法是一个不稳定的算法,不稳定是相对于提供的areInIncreasingOrder闭包)
我们在使用sort函数的时候都是sort(<) 或者 sort(>) ,而不是用 sort(<=) 和 sort(>=)
原因:
可以看出结果都是一样,但是运行的效率是不一样的.
同样的数组,同样的结果,却不同的执行次数.所以在写代码的时候要注意了,千万不要加=号
关于限制深度的问题
(在2000年6月,SGI的C++标准模板库的stl_algo.h中的不稳定排序算法采用了Musser的introsort算法。在此实现中,切换到插入排序的数据量阀值为16个)
1 | // Insertion sort is better at handling smaller regions. |
说明在数量小于20的情况下采用插入排序,所以在Swift中阀值应该是20
1 | if depthLimit == 0 { |
在深度限制为0的时候采用堆排序,说明平衡已经被破坏,数据依旧很大,但是快速排序的效率低下,改用堆排序来维持O(logN)的时间复杂度