Swift语言有着优秀的函数式编程能力,面试的时候面试官都喜欢问我们快速排序,那么用Swift如何实现一个快速排序呢?首先扩展Array类:
extension Array {
var decompose : (head: T, tail: [T])? {
return (count > 0) ? (self[0], Array(self[1..<count])) : nil
}
}
属性decompose的作用是返回数组中的第一个元素和剩下的元素,注意这个属性是可选型的,当count为0的时候返回nil,count是Array的属性。使用扩展的原因是这种拆分可以实现非常多的操作,一劳永逸。
然后实现快速排序的方法:
func qsortDemo(input: [Int]) -> [Int] {
if let (pivot, rest) = input.decompose {
let lesser = rest.filter { $0 < pivot }
let greater = rest.filter { $0 >= pivot }
return qsortDemo(lesser) + [pivot] + qsortDemo(greater)
} else {
return []
}
}
可以发现使用Swift实现快速排序的代码非常的简洁。首先调用待排序序列的decompose属性,使用一个元组来保存数组的第一个元素和首先的数组,由于依旧是采用递归的方式,所以使用可选绑定来做边界判断。在可选绑定内部使用了filter方法来分割元素,省去了比较移动元素的复杂过程,得到的lesser是小于pivot的数组、greater是大于pivot的数组,在返回时使用了数组的拼接并对拆分的数组进行递归,结构非常的简单,至此一个快速排序的过程就结束了。
让我们在storyboard中做个性能测试:
var a:[Int] = [1,2,4,6,2,4,3,7,8]
qsortDemo(a)
数组a在快排中的效率如下:
可以看到可选绑定中的return执行了9次等于a中元素的个数,和预想的一样,这是因为每一个元素是在这个return中确定自身的位置的,所以执行次数应该为n。那么为什么else中的语句执行了n+1次呢?想知道每一个元素在一次递归中发生了什么,可以把让a中只有一个元素模拟一次递归发生的事情,结果如下图:
打开decompose的执行记录:
可以看到decompose被执行了三次,第一次是[1]来访问,返回了([1],[]),此时在可选绑定中,lesser和greater都是[],在return中递归的时候lesser和greater会继续访问decompose此时返回了两个nil,所以对应的可选绑定判断为假直接运行else中的return[],整个过程结束。
else条件返回的是[],[]加入到数组中不会起作用,所以可以作为边界返回值。
把a中的元素扩充到两个。
decompose中的执行记录为:
很好理解了,第一次拆分得到[1]和[2],pivot为[1],lesser为[],而greater为[2]。在return时lesser访问decompose得到nil,可选绑定为假执行else中的语句,此时greater又成了一个元素的数组,步骤同上。所以在这个快排的递归过程中每次只有最后一个元素的lesser和greater会同时为[],其他元素都只有一边为[],这也就解释了为什么return会出现n+1的执行次数。
观察一下两个filter,这种拆分方法需要多余的空间来保存lesser和greater,点开追踪可以看到lesser和greater中的追踪轨迹是相反的,这很好理解。另外filter是系统API,并不知道内部的实现方法,但是可以看到在判断[2]中的元素的时候被调用了三次,应该与内部机制有关,虽然看起来执行的次数变多了,但是免去了传统快速排序中的元素交换位置的操作,效率高低并不好说。总之写了这么多最后的效果就一个:排序。
在看完这段代码后我做了如下思考:既然是排序,那么必然可以使用系统的sorted方法(以前的sort方法),效果如何呢?让我们用第一个例子来试试,只需要一行代码:
let b = a.sorted{$0<$1}
效果如何呢?请看下图:
没错,整个方法只有15次比较!效率非常的惊人,sorted的实现是由苹果的工程师在底层实现的,我想他们一定用了什么好办法来提升效率。不信?来看下面的例子,我们都知道快速排序的最坏情况出现在递归时对数组的不均衡划分上,比如修改数组a为:
var a:[Int] = [1,1,1,1,1,1,1,1,1]
数组的整体大小没有发生变化,运行效率如图:
可以看到算法的主要耗时部分lesser和greater的执行此时由之前的35次变为45次了,那么sorted方法的执行效率又如何呢?
你没有看错!对于快排最头疼的顺序性数组,sorted的重复次数只有n次!说明在面对这种类型的数组的时候sorted方法进行过判断,直接输出了。当然闭包中的语句一定要合适,“千万不要使用等于号!”,比如改写a:
var a:[Int] = [1,1,2,2,3,1,1,1,1]
没有等于号的情况:
如果你写上等于号:
OMG!效果一样的前提下效率差了好多。
另外一种极端情况,完全逆序一个数组:
当然快排的时间和完全相同的元素一样:
如果觉得数量级太小不过瘾,那么来个大号的数组:
现在修改a为500个随机的100以内的正整数:
var a:[UInt32] = []
for _ in 0..<500{
a.append(arc4random() % 100)
}
同时比较两种排序方式,下面是快排的:
下面是sorted的效率:
大家可以试试,规模越大的数组效率差别越明显,sorted以肉眼可见的速度秒杀了快排!
掌声在哪里?