聊一聊插入排序和选择排序
聊一聊插入排序和选择排序。
1 简介
插入排序和选择排序是排序算法中比较基础和简单的两种,其时间复杂度均为$O(N^{2})$,在分析算法时间复杂度时,我们往往会只会分析比较开销,但是交换开销也确实存在。这里我将综合比较开销和交换开销,来分析一下插入排序和选择排序的区别,以及何时选择插入排序?何时该选择选择排序?
我之前的文章 排序算法详解 里给出了几个基本排序算法的JavaScript 版本实现,感兴趣的也可以移步。
2 空间复杂度
插排和选排的均在交换时使用了一个临时空间,其空间复杂度均为$O(1)$。而且插排和选排在排序时同时维护了一个已排序的有序列表和一个待排序的无序列表,不同在于:
- 插排从无序列表中取第一个数,与有序列表的数依次比较,有序列表的已排数据的位置均可能发生变化。
- 选排从无序列表中取最小数,只和无序列表中第一个数交换,此时无序列表第一个数归于有序列表(相当于最后一个数)。
在每一轮排序过程中,均需要有一个临时空间用来存储无序列表提取的这一个数,用于未来的交换。
3 时间复杂度
众所周知,插排和选排的时间复杂度均为$O(N^{2})$。我们在分析时间复杂度的时候,其实都是分析的比较时间复杂度,但是计算机做算法的时候除了比较,还有交换,并不是说交换时间复杂度不重要,而是它大部分情况相对于比较复杂度可以忽略,至于原因,接下来结合比较和交换开销,来分析插排和选排,自然就明白了。
3.1 比较复杂度对比
插入排序
- 最差情况:最差情况下反向有序,每一轮插入,都需要依次比较到有序列表的第一个数,第一轮比较 0 次,第二轮比较 1 次,第 N 轮比较 N-1 次。一共比较$\frac{N \times (N-1)} {2}$次,复杂度$O(N^{2})$
- 最好情况:最好情况下有序,每一轮插入,都只需要比较 1 次,一共比较$N$次,复杂度$O(N)$
- 平均情况:平均情况下,每一轮插入,假设依次比较到有序列表的中间位置,一共比较$\frac {N \times (N-1)} {4}$次,复杂度$O(N^{2})$
选择排序
选择排序比较次数是固定的,每一轮都需要从待排序的无序列表中选取一个最小(或最大)的数,选取中都需要比较到最后一个元素才能得到结果。第一轮比较 N-1 次,第 N 轮比较 0 次。一共比较$\frac {N \times (N-1)} {2}$次,复杂度$O(N^{2})$
因此可以得出结论:在最差情况下,两者复杂度一样。在最好情况下,两者复杂度差异是比较大的(1 个次方),而在平均情况下,插排的比较次数也只是选排的一半。这也是关于插排和选排的通用结论,一般情况下插排优于选排,主要就在于插排是插入有序列表,而选排是需要在无序列表中选择一个最大值(或最小值),想象一下我们斗地主摸牌,摸到一张牌我们是可以马上从小到大判断插入到哪的(这里假设只能从小到大比较),而不必每一张牌都对比一下。
但是上面的结论只讨论了比较复杂度,其实在为什么说平均情况下,插入排序比选择排序快? - 莫涛的回答 - 知乎 和Stack Overflow上,也有人对这种回答中不谈交换表示疑惑,下面我们再来分析一下交换复杂度。
3.2 交换复杂度对比
插入排序
- 最差情况:每一次比较完都需要交换。第一轮交换 0 次,第二轮交换 1 次,第 N 轮交换 N-1 次。一共交换$\frac{N \times (N-1)} {2}$次,复杂度$O(N^{2})$
- 最好情况:每一次比较完都无需交换,总共交换$0$次,复杂度$O(0)$
- 平均情况:每一轮都插入到中间位置,总共交换次数为$\frac{N \times (N-1)} {4}$次,复杂度$O(N^{2})$
选择排序
最差情况:每一轮都需要交换,总共交换$N$次,复杂度$O(N)$
最好情况:每一轮都无需交换,总共交换$0$次,复杂度$O(0)$
平均情况:有一半轮次需要交换,总共交换$\frac N 2$次,复杂度$O(N)$
交换复杂度的对比中我们可以得出:最好情况下两者都无需交换,然而在最差和平均情况下,插入排序的交换次数都高于选择排序,差异为一个次方。可以看出差异还是很大的,单从这样来看,是无法忽略其影响的。
3.3 影响时间复杂度的其他因素
其实,在《算法导论》一书中还提到了一个算法性能分析依赖的以下要素:
- 待排项数
- 这些项已排序程度
- 项值的限制
- 计算机体系结构
- 使用的存储设备种类(主存,磁盘或磁带)
回到这个例子中,我们可以假设忽略硬件的影响、项值无限制。而已排序程度随机(也就是选用平均复杂度,不过一般算法分析都采用最坏情况下的复杂度作为瓶颈进行分析),因此分析要素只剩下待排项数 N,可以使用上面的分析给出结论。
3.4 结论
插入排序和选择排序谁更优?主要在于比较开销和交换开销的量级:
如果两者量级相当,则插入排序优于选择排序
如果比较开销量级小于交换开销,则选择排序优于插入排序
如果比较开销量级大于交换开销,如果差别不大则难以比较,如果差异较大,则可以忽略交换复杂度,此时插入排序优于选择排序
事实上,交换一般直接交换内存地址(指针)而不是交换真实的数据,而比较则需要 CPU 的一些运算。这里的一个回答便给出了自定义赋值函数,如果直接交换数据,当数据量过大,交换开销大大增加,此时插入排序反而不如选择排序,因为其交换次数平均情况下和选择排序不在一个量级。
当然,由于交换排序进行了过多的交换次数(也就是写操作),如果使用 Flash Memory,则需要额外注意,因为 Flash Memory 的擦除次数有限,过多的写操作会消耗其擦除次数,从而消耗 Flash Memory 的寿命。
4 总结 & 参考
一般情况下,插入排序确实优于选择排序,但也有需要注意的点,而不单单是只判断比较复杂度那么简单。需要我们了解清楚再做判断。
文章参考了以下资料: