基础排序算法总结:从逆序对的角度
从逆序对inversion的角度,重新审视基础排序算法,选择、插入、冒泡、希尔,他们到底什么关系?
首先证明:
在数组中交换一个逆序对,一定会使总的逆序对减小
Given an array of N keys, let a[i] and a[j] be an inversion (i < j but a[i] > a[j]). Prove or disprove: swapping a[i] and a[j] strictly decreases the number of inversions
处在i,j两侧的数不会因为这次交换产生逆序对的变动,只需要考虑nums[i+1:j]
中的数,自然地,把这个区间的数根据nums[i], nums[j]
分成三堆(忽略相等的情况,对逆序对没有影响),分别统计这三堆因为这次交换产生的逆序对变动就可以。
可以看到,交换一个inversion,至少减少一个inversion。当j-i=1
,交换相邻的inversion,确定地减少一个inversion。交换不相邻的inversion,inversion的总数有可能减少得更快。
选择排序
思路:选择最小元素,放入有序数组的空位。
选择方法:n次比较+1次交换
。
特点:过程中没有利用计算产生的新信息,即有序数组。
性能:固定的$N^2/2$次比较和$N$次交换
稳定:否
使用场景:交换操作非常昂贵,逆序对比较多
1 |
|
插入排序
思路:将一个新元素,插入进一个有序数组,原数组中所有元素插入完毕,排序结束。
插入方法: 比较+交换 or 比较+移动 or 二分+移动
特点:过程中利用了计算产生的新信息,即有序数组
性能:见下列代码中的讨论。实际上移动本质也是交换,而且也是相邻inversion的交换,所以总的交换(移动)次数等于总的逆序对个数。
稳定:是
使用场景:小规模数组,流式输入数据,逆序对较少,比如:
- 数组中每个元素离他的最终位置都不远
- 一个有序的大数组接一个小数组
- 数组中只有几个元素位置不正确
1 |
|
冒泡排序
思路:结构上同选择排序,选择最大元素,放入有序数组空位。
选择方法: 比较+交换
特点:过程中利用了计算产生的新信息,但没有利用有序数组,利用的剩下的、经过交换的部分有序数组
性能:在比较中使用了交换,所以最终总的交换(移动)次数等于总的逆序对个数。但是比较仍然和选择相同,$N^2/2$。不过可以通过加入一个flag,标记本轮没有发生交换后提早退出。
稳定:是
所以冒泡排序实际上处于一个很尴尬的位置。交换虽然没有多做,但是有很多无意义的比较,因为要从部分有序数组里面寻找逆序对,会比较很多已经有序的数对。尽管设置flag可以减少很多,但是在最后一轮中,如果逆序对出现在靠后的位置,前面还是做了一些多余的比较,依然达不到插入那样节省。
1 |
|
希尔排序
思路:利用插入排序,在更大跨度上做排序,最后使用跨度为1的插入排序保证全部有序。
基础方法:生成递增序列+插入排序
特点 & 性能:希尔排序就是插入排序的直接拓展,由inversion的证明可知,希尔排序通过大跨度扫描,交换inversion,可以更快地减少总的inversion,为接下来的小跨度扫瞄交换创造有利条件。和归并、快排的性能差距在常数级别
稳定:否
除了上述的原理,还有实现上有两个地方注意:
- 实现数组h-有序时,相当于需要对h个子数组进行排序。与其事先计算出每个子数组的index,不如扫描所有未排定的数,也就是从index=h的开始,根据它的index倒推回去,index=h表示它属于
h%h=0
号子数组,index=h+1的表示它属于1号子数组,依次类推,各自在自己的子数组里比较、插入 - 使用binary-search优化时,mid的计算涉及到等差数列的上下中位数的计算,因为还有其他题目涉及,我总结到这篇笔记里
1 |
|
总结
插入排序还是本体,无论从思想上还是实现上,其他方法都可借鉴、对比
基础排序实现时,关注点不妨放在“位置上”,一个萝卜一个坑,思路会更清晰一点。
本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!