活动地址:CSDN21天学习挑战赛
学习的最大理由是想摆脱平庸,早一天就多一份人生的精彩;迟一天就多一天平庸的困扰。各位小伙伴,如果您:
想系统/深入学习某技术知识点…
一个人摸索学习很难坚持,想组团高效学习…
想写博客但无从下手,急需写作干货注入能量…
热爱写作,愿意让自己成为更好的人…
…
直接插入排序
直接插入排序(Straight Insertion Sort)是一种最简单的排序方法,它的基本操作是将一个记录插入到已排好序的有序表中,从而得到一个新的、记录数增 1 的有序表。
1. 排序实例
例如,已知待排序的一组记录的初始排列如下所示:
R ( 49 ) , R ( 38 ) , R ( 65 ) , R ( 97 ) , R ( 76 ) , R ( 13 ) , R ( 27 ) , R ( 49 ‾ ) , ⋯ ( 10 − 4 ) \qquad R(49), R(38), R(65), R(97), R(76), R(13), R(27), R(\overline{49}), \cdots \quad(10-4) R(49),R(38),R(65),R(97),R(76),R(13),R(27),R(49),⋯(10−4)
假设在排序过程中,前 4 个记录已按关键字递增的次序重新排列,构成一个含 4 个记录的有序序列
{ R ( 38 ) , R ( 49 ) , R ( 65 ) , R ( 97 ) } ( 10 − 5 ) \qquad \{R(38), R(49), R(65), R(97)\} \qquad\qquad\qquad\qquad\qquad\qquad\qquad (10-5) {R(38),R(49),R(65),R(97)}(10−5)
现要将式 ( 10 − 4 ) (10-4) (10−4) 中第 5 个(即关键字为 76 的)记录插入上述序列,以得到一个新的含 5 个记录的有序序列,则首先要在式 ( 10 − 5 ) (10-5) (10−5) 的序列中进行査找以确定 R ( 76 ) R(76) R(76) 所应插入的位置,然后进行插入。假设从 R ( 97 ) R(97) R(97) 起向左进行顺序査找,由于 65 < 76 < 97 65 < 76 < 97 65<76<97, 则 R ( 76 ) R(76) R(76) 应插入在 R ( 65 ) R(65) R(65) 和 R ( 97 ) R(97) R(97) 之间,从而得到下列新的有序序列
{ R ( 38 ) , R ( 49 ) , R ( 65 ) , R ( 76 ) , R ( 97 ) } ( 10 − 6 ) \qquad \{R(38), R(49), R(65), R(76), R(97)\} \qquad\qquad\qquad\qquad\qquad\quad\; (10-6) {R(38),R(49),R(65),R(76),R(97)}(10−6)
称从式(10-5) 到式(10-6) 的过程为一趟 直接插入排序。
一般情况下,第 i i i 趙直接插入排序的操作为:在含有 i − 1 i - 1 i−1 个记录的有序子序列 r [ 1.. i − 1 ] r[1..i - 1] r[1..i−1] 中插入一个记录 r [ i ] r[i] r[i] 后,变成含有 i i i 个记录的有序子序列 r [ 1.. i ] r[1..i] r[1..i];并且,和顺序查找类似,为了在查找插入位置的过程中避免数组下标出界,在 r [ O ] r[O] r[O] 处设置监视哨。在自 i − 1 i - 1 i−1 起往前搜索的过程中,可以同时后移记录。
2. 排序过程
整个排序过程为进行 n − 1 n - 1 n−1 趟插入,即:
- 先将序列中的第 1 个记录看成是一个有序的子序列
- 然后从第 2 个记录起逐个进行插入,直至整个序列变成按关键字非递减有序序列为止。
其算法如算法 10.1 所示
算法 10.1
void InsertSort (Sqlist & L){
//对顺序表工作直接插入排序。
for(i = 2; i <= L.length; ++i){
if(LT(L.r[i].key, L.r[i - 1].key)){ //“<”,需将 L.r[i] 插入有序子表
L.r[0] = L.r[i]; //复制为哨兵
L.r[i] = L.r[i - 1];
for(j = i - 2; LT(L.r[0].key,L.r[j].key); -- j);
L.r[j + 1] = L.r[j]; //记录后移
L.r[j + 1] = L.r[O]; //插入到正确位置
}
}
}//InsertSort
3. 排序复杂度分析
从上面的叙述可见,直接插入排序的算法简洁,容易实现,那么它的效率如何呢?
空间复杂度
从空间来看,它只需要一个记录的辅助空间。
时间复杂度
从时间来看,排序的基本操作为:比较两个关键字的大小
和 移动记录
。
先分析一趟插入排序的情况: 算法 10.1 中里层的 for 循环的次数取决于待插记录的关键字与前 i − 1 i - 1 i−1 个记录的关键字之间的关系。
若 L . r [ i ] . k e y < L . r [ 1 ] . k e y L.r[i].key < L.r[1].key L.r[i].key<L.r[1].key,则内循环中,待插记录的关键字需与有序子序列 L . r [ 1.. i − 1 ] L.r[1.. i - 1] L.r[1..i−1] 中 i i i 个记录的关键字和监视哨中的关键字进行比较,并将 L . r [ 1.. i − 1 ] L.r[1..i - 1] L.r[1..i−1] 中 i − 1 i - 1 i−1 个记录后移。则在整个排序过程(进行 n − 1 n - 1 n−1 趟插入排序)中:
当待排序列中记录按关键字非递减有序排列(以下称之为“正序”)时:
- 所需进行关键字间比较的次数达最小值 n − 1 n - 1 n−1(即 ∑ i = 2 n 1 \begin{aligned} \sum_{i=2}^{n} 1 \end{aligned} i=2∑n1),记录不需移动;
当待排序列中记录按关键字非递增有序排列 (以下称之为“逆序”) 时:
- 总的比较次数达最大值 ( n + 2 ) ( n − 1 ) / 2 (n + 2)(n - 1)/2 (n+2)(n−1)/2 (即 ∑ i = 2 n i \begin{aligned}\sum_{i=2}^{n} i \end{aligned} i=2∑ni), 记录移动的次数也达最大值 ( n + 4 ) ( n − 1 ) / 2 (n + 4)(n - 1) / 2 (n+4)(n−1)/2 (即 ∑ i = 2 n ( i + 1 ) \begin{aligned}\sum_{i=2}^{n} (i + 1)\end{aligned} i=2∑n(i+1) )。
若待排序记录是随机的,即待排序列中的记录可能出现的各种排列的概率相同时:
- 则可取上述最小值和最大值的平均值, 作为直接插人排序时所需进行关键字间的比较次数和移动记录的次数,约为 n 2 / 4 n^2 / 4 n2/4。由此,直接插入排序的时间复杂度为 O ( n 2 ) O(n^2) O(n2)。
∑ i = 2 n \begin{aligned} \sum_{i=2}^{n} \end{aligned} i=2∑n 可看做从 2 2 2 到 n n n 公差为 0 的等差数列, S n = n ( a 1 + a n ) 2 \begin{aligned} S_n = \frac{n (a_1 + a_n)}{2} \end{aligned} Sn=2n(a1+an),则有:
∑ i = 2 n 1 = ( n − 1 ) ( 1 + 1 ) 2 = n − 1 \qquad\begin{aligned} \sum_{i=2}^{n} 1 = \frac{(n - 1) (1 + 1)}{2} = n - 1\end{aligned} i=2∑n1=2(n−1)(1+1)=n−1∑ i = 2 n i = ( n − 1 ) ( 2 + n ) 2 = ( n + 2 ) ( n − 1 ) 2 \qquad\begin{aligned} \sum_{i=2}^{n} i = \frac{(n - 1) (2 + n)}{2} = \frac{(n + 2)(n - 1)}{2} \end{aligned} i=2∑ni=2(n−1)(2+n)=2(n+2)(n−1)
∑ i = 2 n ( i + 1 ) = ( n − 1 ) ( 2 + 1 + n + 1 ) 2 = ( n + 4 ) ( n − 1 ) 2 \qquad\begin{aligned} \sum_{i=2}^{n} (i + 1) = \frac{(n - 1)(2 + 1 + n + 1)}{2} = \frac{(n + 4)(n - 1)}{2} \end{aligned} i=2∑n(i+1)=2(n−1)(2+1+n+1)=2(n+4)(n−1)
从上面内容中可见,直接插入排序算法简便,且容易实现。当待排序记录的数量 n n n 很小时,这是一种很好的排序方法。但是,通常待排序序列中的记录数量 n n n 很大,则不宜采用直接插人排序。由此需要讨论改进的办法。在直接插入排序的基础上,从减少“比较”和“移动”这两种操作的次数着眼,可得下列各种插入排序的方法。
1. 折半插入排序
由于插入排序的基本操作是在一个有序表中进行査找和插入,则从上面的内容中可知,这个 “查找” 操作可利用 “折半査找” 来实现,由此进行的插人排序称之为折半插入排序(Binary Insertion Sort),其算法如算法 10.2 所示
算法 10.2
//对顺序表 L 作折半插入排序。
void BInsertSort(Sqlist & L){
for (i = 2; i <= L.length; ++i){
L.r[0] = L.r[i]; //将 L.r[i] 暂存到 L.r[0]
low = 1;
high = i - 1;
while (low <= high){ //在 r[low..high] 中折半查找有序插入的位置
m = (low + high) /2; //折半
if (LT(L.r[0].key, L.r[m].key))
high = m - 1; //插入点在低半区
else
low = m + 1; //插入点在高半区
}// while
for (j = i - 1; j >= high + 1; --j)
L.r[j + 1] = L.r[j]; //记录后移
L.r[high + 1] = L.r[0]; //插人
}// for
}// BInsertSort
从算法 10.2 容易看出,折半插入排序所需附加存储空间和直接插入排序相同,从时间上比较,折半插人排序仅减少了关键字间的比较次数,而记录的移动次数不变。因此,折半插入排序的时间复杂度仍为 O ( n 2 ) O(n^2) O(n2)。