插入排序法
基础排序算法的另一种是插入排序,它的时间复杂度跟选择排序法一样都是O(n^2)。
工作原理
通过构建有序序列,对于未排序数据,在已排序序列中从后向前扫描,找到相应位置并插入。插入排序在实现上,通常采用in-place排序(即只需用到 {displaystyle O(1)} {displaystyle O(1)}的额外空间的排序),因而在从后向前扫描过程中,需要反复把已排序元素逐步向后挪位,为最新元素提供插入空间。
例如:中打扑克牌时候整理扑克牌的思想就是插入排序法的核心思想。也就是说,看后面的牌大小,把小牌插入到前面合适的位置,直至牌有序为止。
动画演示:
使用插入排序为一列数字进行排序的过程
;
核心代码如下:
/**
* 实现两数交换
* @param {Array} arr 数组
* @param {Number} i 当前索引位置
* @param {Number} minIndex 带交换的索引位置
*/
const swap = (arr, i, minIndex) => {
[arr[i], arr[minIndex]] = [arr[minIndex], arr[i]];
}
/**
* 插入排序
* @param {Array} arr 无序数组
* @param {Number} n 数组长度
*/
const insertionSort = (arr, n) => {
for (let i = 1; i < n; i++) {
//寻找arr[i]合适的插入位置(在前面)
for (let j = i; j > 0; j--) {
if (arr[j] < arr[j - 1]) {
swap(arr, j, j - 1);
}else{
break;
}
}
}
}
注意:
-
上面的代码中,外层
for
循环索引是从1开始的,因为只考虑长度为1的数组,也就是索引为0的元素本身就是有序的。 -
内层循环索引开始位置是第
i
个,循环结束的条件是j > 0
。这里因为当j
为1
时候,我们只需要比较索引为1
与索引为0
的元素大小即可。如果将结束条件改为j>=0
那么,循环最后一步将会比较0
与-1
位置的元素大小。而负索引在某些语言是合法的,而另一些语言角度是不合法的操作会提示数组越界。 -
这里的内层循环可以做一个小优化就是在于循环终止条件可以改为
j > 0 && arr[j] < arr[j - 1]
/** * 插入排序 * @param {Array} arr 无序数组 * @param {Number} n 数组长度 */ const insertionSort = (arr, n) => { for (let i = 1; i < n; i++) { //寻找arr[i]合适的插入位置(在前面) for (let j = i; j > 0 && arr[j] < arr[j - 1]; j--) { swap(arr, j, j - 1); } } }
思考:
- 既然插入排序内层循环可以提前退出,相较于选择排序算法,理论上插入排序是快于选择排序的,但是测试结果显示: 1000条整型数据下,选择排序的耗时比插入排序的耗时短,而在10000条整型数据下,插入排序的耗时是选择排序的3倍有多。
优化方案
上面提到插入排序理论上是比选择排序的速度快,但是实测结果相反。接下来我们动手优化一下插入排序的算法。
问题分析:
上面版本的插入排序算法,注意内层循环,当每执行一次循环都将交换两个元素的位置。而交换一次位置将对数组产生三次赋值操作(还要算上访问索引的时间)。那么优化点就在于提前缓存变量以及减少内层循环交换次数。
实现思路:内层循环开始前,先缓存当前的比较值也就是缓存arr[i]
,循环终止条件改为arr[j-1] > e
,e
是我们先前缓存好的arr[i]
。这里我们相比未优化的版本,这里我们不贸然直接进行两数交换而是改为比较赋值,所以循环体是一次赋值操作(将位于j-1
的元素后移一个位置),而未优化版本的循环体是三次赋值操作。
const insertionSort = (arr, n) => {
for (let i = 1; i < n; i++) {
const e = arr[i]; //缓存当前值
let j; //保存元素e应该插入的位置
//采用比较的方案,最后才赋值到数组
//相比以前每比较一次有一次交换操作,一次交换操作要三次赋值
//这里只有一次的赋值操作,性能更优
for (j = i; j > 0 && arr[j - 1] > e; j--) {
arr[j] = arr[j - 1];
}
arr[j] = e;
}
}
这个时候有兴趣的朋友可以跑一下优化后的代码,会发现插入排序已经明显快于选择排序,原因插入排序可以提前终止内层循环。相比于选择排序需要将循环完全执行去去找到最小值,插入排序一旦找到合适的位置,就会终止循环。
总结
-
在最优的情况下,也就是说排序内容是完全有序的情况下,插入排序的时间复杂度为O(n)
-
当要排序的数组内容是将近有序的时候,选择插入排序法效率不比高级排序的算法差