插入排序
1 直接插入排序
直接插入排序的核心思想是,待排数列一个一个地插入有序数列,不断扩大有序数列,直到待排数列为空.
1.1 算法过程
(从小到大排序)
1. 单个数字一定有序,因此数组首项可以看做一个有序数列,剩余的项组成待排数列;
2. 从头到尾依次扫描待排数列,将扫描到的每个元素插入有序数列的适当位置.(如果待插入的元素与有序数列中的某个元素相等,则将待插入元素插入到相等元素的后面)
1.2 排序演示
对下面数列进行直接插入排序:
5, 3, 2, 4, 1
将数列首项5
作为有序数列,待排数列为[3,2,4,1]
,取待排数列首项3
作为当前元素,与有序数列[5]
从后往前进行比较
5, 3, 2, 4, 1
↑ ↑
3
小于5
,5
向后移一位,腾出位置给3
,此时有序数列为[3, 5]
,待排数列为[2, 4, 1]
3, 5, 2, 4, 1
取待排数列首项2
作为当前元素,与有序数列[3, 5]
从后往前进行比较
3, 5, 2, 4, 1
↑ ↑
2
小于5
,5
向后移一位,腾出空位.2
继续往前,与3
作比较
3, 空位, 5, 4, 1
↑ 当前元素2
2
小于3
,3
向后移一位,腾出位置给2
,此时有序数列为[2, 3, 5]
,待排数列为[4, 1]
取待排数列首项4
作为当前元素,与有序数列[2, 3, 5]
从后往前进行比较
2, 3, 5, 4, 1
↑ ↑
4
小于5
,5
向后移一位,腾出空位.4
继续往前,与3
作比较
2, 3, 空位, 5, 1
↑ 当前元素4
4
不小于3
,当前元素4
填入空位.此时有序数列为[2, 3, 4, 5]
,待排数列为[1]
以此类推,最后一个元素1
会插入到有序数列的,最终得到的有序数列[1, 2, 3, 4, 5]
即为排序完成的结果.
1.3 代码实现
const arr = [5, 3, 2, 4, 1, 7, 10, 9, 8, 6]
function insertSort(arr) {
// 缓存数组长度
let len = arr.length,
curValue;
// 外层循环,遍历待排数列
for (let i = 1; i < arr.length; i++) {
// curValue 待排数列的当前元素
curValue = arr[i];
// 内层循环,从后往前遍历有序数列
for (let j = i - 1; j >= 0; j--) {
// curValue小于有序数列当前值,则有序数列当前值后移一位,curValue插入
if (curValue < arr[j]) {
arr[j + 1] = arr[j];
arr[j] = curValue;
} else {
// 如果curValue不小于有序数列当前值,则不再往后比较,直接将curValue插入当前位置
break;
}
}
}
return arr;
}
1.4 时间复杂度
- 最好情况下:原数列有序,此时内层循环只走一次,整体复杂度取决于外层循环,时间复杂度是
O(n)
. - 最坏情况下:原数列逆序,此时内层循环每次都要比较和移动有序数列里所有的元素,时间复杂度是两层循环的
O(n^2)
; - 平均时间复杂度:
O(n^2)
.
1.5 稳定性
从插入排序的排序过程分析,从未排序数列取首项,与有序数列从后往前比较时,如果两个元素相等,则有序数列的值不腾出位置.两个元素的先后位置并未改变,因此插入排序是稳定的.
2 希尔排序
希尔排序是对直接插入排序的一种改进.
2.1 算法过程
(从小到大排序)
1. 选择一个递减的增量序列t1, t2, ... , tk,其中序列尾项,即tk必为1
2. 以t1为增量,将待排数列划分为t1个子序列,分别对这t1个子序列进行直接插入排序,得到新的待排数列
3. 以t2为增量,将待排数列划分为t2个子序列,分别对这t2个子序列进行直接插入排序,得到新的待排数列
4. 以此类推,直到以tk为增量,即增量为1,此时待排数列本身进行直接插入排序,得到排序后的数列
2.2 排序演示
算法过程讲起来比较抽象,来看一个希尔排序的例子.
对下面的数列进行希尔排序
3, 4, 6, 9, 5, 7, 8, 1, 2, 10
确定增量序列,一般是以待排数列的长度来决定.
这里,我们以待排数列长度除以2
并向下取整,确定增量序列的首项,即
⌊10 / 2⌋ = 5
再根据t(i) = ⌊t(i-1) / 2⌋
得到增量序列为
5, 2, 1
开始第一轮排序,增量为5
,待排数列被划分为5
组a,b,c,d,e
,其中a
组子数列为[3, 7]
,b
组子数列为[4, 8]
,c
组子数列为[6, 1]
,d
组子数列为[9, 2]
,e
组子数列为[5, 10]
3, 4, 6, 9, 5, 7, 8, 1, 2, 10
a b c d e a b c d e
分别对a,b,c,d,e
五组子数列进行直接插入排序,得到新的待排数列
3, 4, 1, 2, 5, 7, 8, 6, 9, 10
开始第二轮排序,增量为2
,待排数列被划分为2
组a,b
,其中a
组子数列为[3, 1, 5, 8, 9]
,b
组子数列为[4, 2, 7, 6, 10]
3, 4, 1, 2, 5, 7, 8, 6, 9, 10
a b a b a b a b a b
分别对a,b
两组子数列进行直接插入排序,得到新的待排数列
1, 2, 3, 4, 5, 6, 8, 7, 9, 10
开始第三轮排序,增量为1
,直接对待排数列进行直接插入排序,得到排序后的数列
1, 2, 3, 4, 5, 6, 7, 8, 9, 10
2.3 代码实现
const arr = [3, 4, 6, 11, 9, 5, 7, 8, 1, 2, 10];
const arr = [3, 4, 6, 11, 9, 5, 7, 8, 1, 2, 10];
function shellSort(arr) {
// 缓存数组长度
let len = arr.length;
// 初始化增量序列首项
let increment = Math.floor(len / 2);
// 外层循环增量序列
for (increment; increment > 0; increment = Math.floor(increment / 2)) {
// 巧妙地将划分子数列和对子数列进行直接插入排序结合
for (let i = increment; i < len; i++) {
let curValue = arr[i];
// 外部定义变量j,这样不用在每一次循环都把curValue插入,只需要在循环结束后插入
let j;
// 将arr[j] > curValue写在循环的判断条件中,这样当不满足该条件时,循环就会被break,不会再往下进行比较(实际上相当于前面直接插入排序的if-else判断)
for (j = i - increment; j >= 0 && arr[j] > curValue; j -= increment) {
arr[j + increment] = arr[j];
}
arr[j + increment] = curValue;
}
}
return arr;
}
2.4 性能分析
由希尔排序的算法过程可以知道,希尔排序的执行时间依赖于增量序列.好的增量序列有如下特征:
- 最后一个增量必须为
1
; - 应该尽量避免序列中的值(尤其是相邻的值)互为倍数的情况;
希尔排序性能是优于直接插入排序的,为什么这么说?我们知道直接插入排序有两个特征:
- 当
n
值较小时,n
和n^2
的差别较小,即直接插入排序的最好时间复杂度O(n)
和最坏时间复杂度O(n^2)
差别不大. - 原始待排数列基本有序时,直接插入排序所需的比较和移动次数较少;
希尔排序开始时,增量较大,分组较多,每组子数列的项数较少,因此各组子数列直接插入排序较快,这对应了直接插入排序的第一个特征.
希尔排序到后面增量越来越小,分组数较少,各组子数列的项数较多,但是由于前面的排序已经使得这些子数列基本有序,所以新的一趟直接插入排序也较快,这对应了直接插入排序的第二个特征.
2.5 稳定性
从希尔排序的排序过程可以看到,划分子数列并对子数列进行直接插入排序时,是很有可能打乱两个相等元素的先后顺序的,因此希尔排序是不稳定的.