插入排序
借用《算法导论》里的例子,就是我们打牌的时候,每新拿一张牌都会把它按顺序插入,这,其实就是插入排序。
齐姐声明:虽然我们用打牌的例子,但是可不能学胡适先生啊。
对于数组来说怎么做呢?
有一个重要的思想,叫做挡板法,就是用挡板把数组分成两个区间:
挡板左边:已排序 挡板右边:未排序
那么排序分三步走:
最初挡板是在数组的最左边,index = 0 的位置,也就是保证了已排序区间里一个数都没有,或者也可以包含一个数啦;
核心思想就是:
依次遍历未排序区间里的元素,在已排序区间里找到正确的位置插入;重复这个过程,直到未排序区间为空。
举个例子:{5, 2, 1, 0}
第一步,挡板最初在这里:
第二步, 把 2 插入已排序区间的正确位置,变成:
重复这个步骤,把 1 排好:
最后把 0 排好:
那代码也很简单:
public void insertionSort(int[] input) {
if (input.length <= 1) {
return;
}
for(int i = 1; i < input.length; i++) {
int tmp = input[i];
int j = i - 1;
while(j >= 0 && input[j] > tmp) {
input[j+1] = input[j];
j --;
}
input[j+1] = tmp;
}
}
我们来分析一下这个算法的时空复杂度。
时间复杂度
关于时间复杂度有两个要点:
是描述随着自变量的增长,所需时间的增长率; 是渐近线复杂度,就是说 不看系数 只看最高阶项
那么我们关心的 worst case 的情况就是:
如果数组是近乎倒序的,每次插入都要在数组的第一个位置插入,那么已排序区间内的所有的元素都要往后移动一位,这一步平均是 O(n),那么重复 n 次就是 O(n^2).
空间复杂度
重点是一个峰值的概念,并不是累计使用的空间。 这里是 O(1) 没什么好说的。
引入一个概念:sorted in place,也就是原地排序。
原地排序就是指空间复杂度为 O(1) 的算法,因为没有占用额外的空间,就是原地打转嘛。
其实 in-place 的思想并不是只在排序算法里有,只不过排序算法是一个最广为人知的例子罢了。本质上就是一个节省使用空间的思想。
但是对于排序算法,只分析它的时空复杂度是不够的,还有另外一个重要指标:
稳定性
这个是排序算法的一个重要指标,意思是元素之间的相对顺序是否保持了不变。
比如说:{5, 2, 2, 1, 0}
这个数组排序完成后这里面的两个 2 的相对顺序没有变,那么这个排序就是一个稳定排序。
那有同学可能就想,顺序变了又有什么关系呢?
其实,在实际工作中我们排序的对象不会只是一个数字,而是一个个的对象 (object),那么先按照对象的一个性质来排序,再按照另一个性质来排序,那就不希望原来的那个顺序被改变了。好像有点抽象,我们举个例子。
比如在股票交易系统里,有买卖双方的报价,那是如何匹配的呢?
先按照价格排序; 在相等的价格中,按照出价的时间顺序来排序。
那么一搬来说系统会维持一个按时间排序的价格序列,那么此时只需要用一个具有稳定性的排序算法,再按照价格大小来排序就好了。因为稳定性的排序算法可以保持大小相同的两个对象仍维持着原来的时间顺序。
那么插入排序是否是稳定性的排序呢?答案是肯定的。因为在我们插入新元素的时候是从后往前检查,并不是像打牌的时候随便插一个位置不能保证相对顺序。
大家可以看下下面的动画[1] 就非常清楚了~
优化
插入排序其实是有很大的优化空间的,你可以搜一下“希尔排序”。
在刚开始学习的时候,深度固然重要,但因为广度不够,如果学的太深可能会很痛苦,一个知识点就无穷无尽的延展,这并不是一个高效的学习方式。
所以如果时间有限,就要做好深度和广度的平衡:
在常用常考的知识点上多花时间精力,追求深度; 在一些拓展性的知识点上点到为止,先知道有这么回事就行。
保持 open minded 的心态,后期就会有质的提高。
如果你喜欢这篇文章,记得给我点赞留言哦~你们的支持和认可,就是我创作的最大动力,我们下篇文章见!
我是小齐,纽约程序媛,终生学习者,每天晚上 9 点,云自习室里不见不散!
更多干货文章见我的 Github: https://github.com/xiaoqi6666/NYCSDE
参考资料
排序动画: https://visualgo.net/en/sorting