概念
插入排序,一般也被称为直接插入排序。对于少量元素的排序,它是一个有效的算法。它的工作原理是通过构建有序序列,对于未排序数据,在已排序序列中从后向前扫描,找到相应位置并插入。
思路
插入排序的过程,就如同打扑克牌时,不断整理放入手中的牌一样,即每次拿到一张牌,按大小顺序(从大到小或从小到大,一般都是这样吧)将其插入到合适的位置。
为了方便理解,这里大致模拟一下摸牌过程(从小到大):
- 第一次摸到一张 9。
- 第二次摸到一张 6,它比 9 小,要放在 9 的前面。
- 第三次摸到一张 10,它比 9 大,要放到 9 的后面。
以此类推(仅为便于理解,所以过程不是很严谨),直到所有的牌摸完。
所以,我们可以将插入排序理解为: 每次将一个新数据插入到有序队列中的合适位置里。
整个过程的步骤如下:
- 假设数组前面 n-1 (其中 n >= 2) 个数已经是排好顺序的,即将其看成是一个有序序列。
- 取出下一个元素,并将已排序的有序序列中的元素从后向前逐一同新取出的元素进行比较。
- 若是已排序的元素大于新取出的元素,则将已排序的元素移到下一位置。
- 重复步骤 3,直到找到已排序的元素小于或者等于新元素的位置。
- 将新元素插入到该位置之后;
- 重复步骤 2 ~ 5。
图解过程
从小到大的插入排序整个过程如图示(图文摘自菜鸟教程):
第一轮: 从第二位置的 6 开始比较,比前面 7 小,交换位置。
第二轮: 第三位置的 9 比前一位置的 7 大,无需交换位置。
第三轮: 第四位置的 3 比前一位置的 9 小交换位置,依次往前比较。
略......
就这样依次比较到最后一个元素。
实现
const list = [7, 6, 9, 3, 1, 5, 2, 4];
const res = insertSort(list);
console.log('输出结果:', res);
function insertSort(data) {
for (let i = 1, len = data.length; i < len; i++) {
let temp = data[i]; // 存储需要插入的数据
let j = i - 1;
// 注意 j--, 是为了从后向前比较,且在循环结束后 j-- 还是会执行一次的
for (; j >= 0; j--) {
// 当前比较的数大于等于前一个数,证明有序,退出循环
if (temp >= data[j]) {
break;
}
// 因为前 i-1 个数都是从小到大的有序序列,所以只要当前
// 比较的数 data[j] 大于 temp,就把这个数后移一位
data[j + 1] = data[j];
}
data[j + 1] = temp; // 将 temp 插入到合适的位置
}
return data;
}
优化
直接插入排序有一种优化算法——拆半插入。它主要思路是: 将排序的数组看成两部分,第一部分是已排序的,第二部分是待排序的。由于第一部分已排好序,所以不用按顺序依次寻找插入点,只需取它们的中间值与待插入元素进行大小比较,然后依据比较结果确定插入位置。
具体步骤如下
- 取已排序数组索引 0 ~ i-1 的中间点 m,将 arr[i] 与 arr[m] 进行比较。若 array[i] < array[m],则说明待插入的元素 array[i] 位于数组的 0 ~ m 索引之间;反之,则位于数组的 m ~ i-1 索引之间。
- 重复步骤 1,每次缩小一半的查找范围,直至找到插入的位置。
- 将数组中插入位置之后的元素全部后移一位。
- 在指定位置插入第 i 个元素即待插入的元素。
代码实现
let arr = [7, 6, 9, 3, 1, 5, 2, 4];
sortInsert(arr);
console.log('输出结果: ', arr);
function sortInsert(arr) {
// 第一个数不需要排序
for (let i = 1, len = arr.length; i < len; i++) {
let current = arr[i], // 带排序的元素
left = 0,
right = i - 1;
// 每次循环,缩小一半的查找范围,直至找到插入的位置
while (left <= right) {
let m = Math.floor((left + right) / 2); // 中间值的索引
// 比较中间值与待插入元素大小,不断缩小左半区或右半区(以 m 为界,分左右半区)。
if (current < arr[m]) {
right = m - 1;
} else {
left = m + 1;
}
}
// 将数组中插入位置之后的元素全部后移一位
for (let j = i - 1; j >= left; j--) {
arr[j + 1] = arr[j];
}
// 在指定位置插入第 i 个元素即待插入的元素
arr[left] = current;
}
}
结果对比
为了便于对比, 在上述排序代码中使用了同样的数组 [7, 6, 9, 3, 1, 5, 2, 4]。
从图文和代码上来看,我们可以知道折半插入排序减少了查询的次数, 但是它的时间复杂度和直接插入排序一样,都是 O(n²)。而且它们之间很类似,每次交换的都是相邻的且值为不同的元素,且不会改变值相同的元素之间的顺序,因此都是稳定的。
最后
关于本文,若是有错误之处,还望各位留言指出。当然,若是觉得还行,就请点个👍,支持一下。
参考文章: