今天分享的内容是一种排序算法--插入排序。插入排序是一个基本难度的算法,相较于其他的更快的算法,像快速排序算法,有个优点,就是稳定。稳定是一个很重要的性质。
如果两个数值相等的元素,在排序之后,这两个元素的相对位置不会发生变化,那么这个算法就是稳定的。这是很重要的。
假设现在有这么一个场景,现在多条学生成绩的数据,最初是按照学生的姓名首字母排序的,现在按照学生的成绩排序,要求相同成绩的学生之间,依旧按照字母排序。如果排序不是稳定的,那么这种要求就难以保证,除非相同成绩的学生之间再来一次排序😄
什么是插入排序
插入排序是一种最简单直观的排序算法,它的工作原理是通过构建有序序列,对于未排序数据,在已排序序列中从后向前扫描,找到相应位置并插入。 插入排序和冒泡排序一样,也有一种优化算法,叫做拆半插入。
具体来说,插入排序的实现思路如下:
- 将第一个元素看做一个有序序列,把第二个元素到最后一个元素当成是未排序序列。
- 从头到尾依次扫描未排序序列,将扫描到的每个元素插入有序序列的适当位置。
- 如果待插入的元素与有序序列中的某个元素相等,则将待插入元素插入到相等元素的后面。(稳定性的考量)
实现快速排序
javascript
const data = [23, 54, 2, 23, 1, 2, 65, 343, 9, 213];
const insertSort = (array) => {
for (let i = 1; i < array.length; i++) {
if (array[i - 1] > array[i]) { // 如果前一个元素大于当前元素
const temp = array[i]; // 保存当前元素
let j = i - 1;
for (; j >= 0 && array[j] > temp; j--) { // 从右向左搜索放置项目的正确位置
array[j + 1] = array[j]; // 将前一个元素复制到后一个元素上
}
array[j + 1] = temp; // 找到当前元素应处于的位置
}
}
};
这是一段插入排序的代码。插入排序就是从选择其中一个数,插入前面的有序数组。
代码中从数组中的第二个位置开始遍历,往前面的有序数组进行插入。假设遍历到了第 k 个, 第 k-1 个元素比第 k 个大,就需要发生数据的挪动。接下来的 for 循环就是找前面所有的比第 k 个元素大的元素,有一个算一个,全部往后移一位。虽然会覆盖第 k 个位置,不过没关系,第 k 个元素用 temp
保存了起来。等找完了,就将第 k 个元素,放到前面空出来的位置。这样有序数组的长度就增加了一。
接着 i++
,继续遍历下一个元素,即将 k+1 个元素插入到前面的有序数组中
等将所有的元素都插入到有序数组中,array
数组自然就是有序的了
测试代码:
javascript
insertSort(data);
console.log(data);
// [
// 1, 2, 2, 9, 23,
// 23, 54, 65, 213, 343
// ]
测试成功,输出结果是有序的
有个关键的地方需要注意:在判断array[j] > temp
中,>
不能写成>=
, 如果加了=
,就会使得大小相同的元素的相对位置会发生变化,算法就不稳定了
优化
在插入时,需要找到第 k 个元素插入的位置。查找的对象是有序的,那就可以在此做文章了。
查找有序的数组,可以使用折半查找的算法。折半查找,又称二分查找,实现思路如下:
- 初始状态下,搜索区域为整个查找表,用
left
记录搜索区域内第一个元素的位置,用right
记录搜索区域内最后一个元素的位置。 - 借助
⌊ (left + right) / 2⌋
公式,找到搜索区域内的中间元素。 - 如果中间元素等于目标元素,则查找成功;否则,根据中间元素与目标元素的大小关系,确定新的搜索区域,重复步骤 2 直到查找成功或失败。
javascript
const halfFind = (array, value, left, right) => {
if (left > right) return left;
const mid = Math.floor((left + right) / 2);
if (value < array[mid]) {
right = mid - 1;
return halfFind(array, value, left, right);
}
if (array[mid] <= value) {
left = mid + 1;
return halfFind(array, value, left, right);
}
};
const insertSortHalf = (array) => {
for (let i = 1; i < array.length; i++) {
if (array[i - 1] > array[i]) {
const temp = array[i];
const mid = halfFind(array, temp, 0, i - 1);
for (let j = i - 1; j >= mid; j--) {
array[j + 1] = array[j];
}
array[mid] = temp;
}
}
};
这里定义了insertSortHalf,接收一个数组作为参数,函数中会利用折半查找对数组 array 进行排序。
排序的过程和上面一样,不同的是找插入位置的过程,假设已经利用halfFind
找到了,就将 mid`` 到 i-1 的所有元素,都往后移动一位,然后将 第 k 个元素放到 mid
的位置上。
halfFind
,作用是找到第 k 个元素插入的位置。过程很简单,代码也很简单,就不啰嗦了。和上面描述折半查找的步骤不同的,我们需要找的是**最后一个大于第 k 个元素的位置,而不是等于第 k 个元素的位置。**所以当array[mid] == value
时,我仍然对halfFind
进行递归调用,直到left > right
,才返回 left
。
这个地方有些难理解,可以自己举个例子
这是和一般折半查找不同的地方需要注意。
算法复杂度分析
对于一个混乱序列的数组,遍历的每个数都要查找在前面有序数组的位置,查找和挪动元素的复杂度 n, 又因为有 n 个元素,所以插入排序的复杂度是 O(n^2)
;对于一个有序(升序)数组,查找和挪动元素的复杂度 1,又因为有 n 个元素,所以插入排序的复杂度是 O(n)
;对于一个有序(降序)的数组,是最差的情况,插入排序的复杂度是 O(n^2)
。
所以平均而言,插入排序的复杂度就是O(n^2)
总结
这篇文章分享了一个基础的排序算法--插入排序,讲解了两种版本的代码,两种版本都要注意算法的稳定性。文末分析了算法的复杂度,虽然介绍地有些粗糙,但大致过程就是这样了
你觉得这篇文章怎么样?我每天都会分享一篇算法小练习,喜欢就点赞+关注吧