插入排序
-
基本思想:每步将一个待排序的对象,按其关键码大小,插入到前面已经排好序的一组对象的适当位置上,直到对象全部插入为止。
边插入边排序的思想,保证子序列中随时都是排好序的。 -
基本操作:有序插入
- 在有序序列中插入一个元素,保持序列有序,有序长度不断增加。
- 起初,a[0]是长度为1的子序列。然后,逐一将a[1]至a[n-1]插入到有序子序列中。
-
有序插入方法:
- 在插入a[i]之前,数组a的前半段(a[0]到[i-1])是有序段,后半段(a[i]到a[n-1])是停留于输入次序的“无序段”。
- 插入a[i]使a[0]~a[i-1]有序,也就是要为a[i]找到有序位置j( 0 ≤ j ≤ i 0\le j\le i 0≤j≤i),将a[i]插入到a[j]的位置上。
-
插入位置:
- 插在中间:
- 插在最前面:
- 插在最后面:
- 插在中间:
-
根据找插入位置方法的不同,把插入排序分为:
- 顺序法定位插入位置:直接插入法
- 二分法定位插入位置:二分插入排序
- 缩小增量多遍插入排序:希尔排序
直接插入排序
-
直接插入排序——采用顺序查找法查找插入位置。
-
例子说明:如下图,绿色部分示已经排好序的序列,红色部分是待排序的序列。
首先,复制插入的元素,x=a[i],然后从第i-1个元素开始,从后往前查找,若当前元素a[j]比x大,则a[j]往后移一位,即a[j+1]=a[j],若当前元素a[j]不比x大,则说明找到插入位置了,此时插入位置为j+1,即a[j+1]=x。 -
具体步骤:
- 复制插入元素
x=a[i];
- 记录后移,查找插入位置
for (j = i - 1; j >= 0 && x < a[i]; j--) a[j + 1] = a[j];
- 插入到正确位置
a[j+1]=x;
-
直接插入排序,使用哨兵
- 将待排序元素复制为哨兵
L.r[0]=L.r[i];
- 记录后移,查找插入位置
for (j = i - 1; L.r[0].key < L.[j].key; --j) L.r[j + 1] = L.r[j];
- 插入到正确位置
L.r[j+1]=L.r[0];
-
算法描述:
void InsertSort(SqList &L)
{
int i, j;
for (int i = 2; i <= L.length; ++i)
{
if (L.r[i].key < L.r[i - 1].key) //若当前待排序元素值比前一个大,则无需改变其位置
{
L.r[0] = L.r[i]; //复制为哨兵
for (int j = i - 1; L.r[0].key < L.r[j].key; --j)
{
L.r[j + 1] = L.r[j]; //记录后移
}
L.r[j + 1] = L.r[0]; //插入到正确位置
}
}
}
- 性能分析
实现排序的基本操作有两个:
1.“比较”序列中两个关键字的大小;
2.“移动”记录。- 最好的情况:关键字在记录序列中顺序有序。例如,序列如下,{11,25,32,47,56,70,81,85,92,96},
- “比较”次数: ∑ i = 2 n 1 = n − 1 \sum\limits_{{\rm{i}} = 2}^n 1 = n - 1 i=2∑n1=n−1
- “移动”次数:0
- 最坏的情况:关键字在记录序列中逆序有序。
- “比较”次数: ∑ i = 2 n i = ( n + 2 ) ( n − 1 ) 2 \sum\limits_{{\rm{i}} = 2}^n i = \frac{{(n + 2)(n - 1)}}{2} i=2∑ni=2(n+2)(n−1)
- “移动”次数: ∑ i = 2 n ( i + 1 ) = ( n + 4 ) ( n − 1 ) 2 \sum\limits_{{\rm{i}} = 2}^n {(i + 1)} = \frac{{(n + 4)(n - 1)}}{2} i=2∑n(i+1)=2(n+4)(n−1)
- 平均情况:
- “比较”次数: ∑ i = 1 n − 1 ( i + 1 2 ) = ( n + 2 ) ( n − 1 ) 4 \sum\limits_{{\rm{i}} = 1}^{n - 1} {(\frac{{i + 1}}{2})} = \frac{{(n + 2)(n - 1)}}{4} i=1∑n−1(2i+1)=4(n+2)(n−1)
- “移动”次数: ∑ i = 1 n − 1 ( i + 1 2 + 1 ) = ( n + 6 ) ( n − 1 ) 4 \sum\limits_{{\rm{i}} = 1}^{n - 1} {(\frac{{i + 1}}{2} + 1)} = \frac{{(n + 6)(n - 1)}}{4} i=1∑n−1(2i+1+1)=4(n+6)(n−1)
- 时间复杂度:
- 原始数据越接近有序,排序速度越快
- 最坏情况下(输入数据是逆有序的), T w ( n ) = O ( n 2 ) Tw(n)=O(n^2) Tw(n)=O(n2)
- 平均情况下,耗时差不多是最坏情况的一半, T e ( n ) = O ( n 2 ) Te(n)=O(n^2) Te(n)=O(n2)
- 要提高查找速度
- 减少元素的比较次数
- 减少元素的移动次数
- 最好的情况:关键字在记录序列中顺序有序。例如,序列如下,{11,25,32,47,56,70,81,85,92,96},
折半插入排序
上图中,我们是在绿色部分寻找插入位置,而绿色部分是一个已完成排序的一个有序序列,所以在查找插入位置时,还可以考虑折半查找法。
- 查找插入位置时使用折半查找法:
- 算法描述
void BInsertSort(SqList &L)
{
int low, high,mid;
for (int i = 2; i <= L.length; ++i) //依次插入第2到第n个元素
{
L.r[0] = L.r[i]; //当前插入元素存到哨兵位置
low = 1; //采用折半查找法查找插入位置
high = i - 1;
while (low<=high)
{
mid = (low + high) / 2;
if (L.r[0].key < L.r[mid].key)
high = mid - 1;
else
low = mid + 1;
} //循环结束,high+1为插入位置
for (int j = i - 1; j >= high + 1; --j) //移动元素
L.r[j + 1] = L.r[j];
L.r[high + 1] = L.r[0]; //插入到正确位置
}
}
- 性能分析
- 折半查找比顺序查找快,所以折半插入排序就平均性能来说比直接插入排序要快。
- 他所需要的关键码的比较次数与待排序对象序列的初始排序无关,仅依赖于对象个数。在插入第i个对象时,需要经过
⌊
log
2
i
⌋
+
1
\left\lfloor {\log _2^i} \right\rfloor {\rm{ + 1}}
⌊log2i⌋+1次关键码比较,才能确定它应插入的位置。
- 当n很大时,总关键码的比较次数要比直接插入排序的最坏情况要好很多,但比其最坏情况要差。
- 在对象的初始排列已经按关键码排好序或接近有序时,直接插入排序比折半插入排序执行的关键码的比较次数要少。
- 折半插入排序的对象移动次数与直接插入排序相同,依赖于对象的初始排列
- 减少了比较次数,但没有减少移动次数
- 平均性能优于直接插入排序
- 时间复杂度为 O ( n 2 ) O(n^2) O(n2),空间复杂度为 O ( 1 ) O(1) O(1),是一种稳定的排序方法。
希尔排序
-
希尔排序思想的出发点:
- 1.相比直接插入排序,折半插入排序减少了比较的次数,但没有减少移动次数。所以,在这里,能不能比较一次,就移动一大步,而不是移动一步。
- 2.直接插入排序在原序列基本有序时,以及待排序的记录个数较少时,效率较高。
-
基本思想:先将整个待排序序列分割成若干个子序列,分别进行直接插入排序,待整个序列中的记录“基本有序”时,再对全体记录进行一次直接插入排序。
即,- 缩小增量
- 多遍插入排序
-
例子
-
思路:
- 1.定义增量序列
D
k
:
D
M
>
D
M
−
1
>
⋯
>
D
1
=
1
D_k:D_M>D_{M-1}>\cdots>D_1=1
Dk:DM>DM−1>⋯>D1=1
在刚才的例子中: D 3 = 5 , D 2 = 3 , D 1 = 1 D_3=5,D_2=3,D_1=1 D3=5,D2=3,D1=1 - 2.对每个 D k D_k Dk进行“ D k D_k Dk-间隔” 插入排序( k = M , M − 1 , ⋯   , 1 k=M,M-1,\cdots,1 k=M,M−1,⋯,1)
- 1.定义增量序列
D
k
:
D
M
>
D
M
−
1
>
⋯
>
D
1
=
1
D_k:D_M>D_{M-1}>\cdots>D_1=1
Dk:DM>DM−1>⋯>D1=1
-
特点
- 一次移动,移动位置较大,跳跃式地接近排序后的最终位置
- 最后一次只需要少量移动
- 增量序列必须是递减的,最后一个必须是1
- 增量序列应该是互质的
-
算法描述:
void ShellSort(SqList &L, int dlta[], int t)// dlta为增量数组
{
for (int k = 0; k < t; ++k)
ShellInsert(L, dlta[k]); //一次增量为dlta[k]的插入排序
}
void ShellInsert(SqList &L, int dk)
{
for (int i = dk + 1; i <= L.length; ++i)
{
if (L.r[i].key < L.r[i - dk].key)
{
L.r[0] = L.r[i];
for (int j = i - dk; j > 0 && (L.r[0].key < L.r[j].key); j = j - dk)
L.r[j + dk] = L.r[j];
L.r[j + dk] = L.r[0];
}
}
}
- 性能分析
- 时间复杂度与增量序列取值有关
- Hibbard增量序列
- D k = 2 k − 1 D_k=2^{k-1} Dk=2k−1——相邻元素互质
- 最坏情况: T w o r s t = O ( n 3 / 2 ) T_{worst}=O(n^{3/2}) Tworst=O(n3/2)
- 猜想(为证明): T a v g = O ( n 5 / 4 ) T_{avg}=O(n^{5/4}) Tavg=O(n5/4)
- Sedgewick增量序列
- {1,5,19,41,109,…}—— 9 ∗ 4 i − 9 ∗ 2 i + 1 或 4 i − 3 ∗ 2 i + 1 9*4^i-9*2^i+1或4^i-3*2^i+1 9∗4i−9∗2i+1或4i−3∗2i+1
- 猜想(为证明): T a v g = O ( n 7 / 6 )          T w o r s t = O ( n 4 / 3 ) T_{avg}=O(n^{7/6})\;\;\;\;T_{worst}=O(n^{4/3}) Tavg=O(n7/6)Tworst=O(n4/3)
- Hibbard增量序列
- 希尔排序是一种不稳定的排序算法
- 时间复杂度是n和d的函数:
O ( n 1.25 ) 到 O ( 1.6 n 1.25 ) — — 经 验 公 式 O(n^{1.25})到O(1.6n^{1.25})——经验公式 O(n1.25)到O(1.6n1.25)——经验公式
空间复杂度为 O ( 1 ) O(1) O(1)
是一种不稳定的排序方法· - 如何选择最佳的d序列,目前尚未解决。
最后一个增量必须为1,无除了1之外的公因子。
不宜在链式存储结构上实现。
- 时间复杂度与增量序列取值有关