插入排序
基本思想是每次将一个待排序的记录按其关键字大小插入到前面已排好序的子序列中,直到全部记录插入完成。
直接插入排序 |
实现图示:
核心代码:
void InsertSort(int A[],int n){
int i,j;
for(i=2;i<=n;i++){ //依次将A[2]~A[N]插入到前面已排序序列
if(A[i] < A[i-1]){ //若A[i]关键码小于其前驱,将A[i]插入有序表,若大于则直接跳过
A[0] = A[i]; //复制为哨兵,A[0]不存放元素
for(j=i-1;A[0]<A[j];j--){ //从后向前查找待插入位置
A[j+1] = A[j]; //向后挪位
}
A[j+1] = A[0]; //复制到插入位置
}
}
}
性能分析:
空间效率 | O(1) |
时间效率 | O(n^2) |
稳定性 | 稳定 |
适用性 | 适用于顺序存储和链式存储的线性表 |
tips:当用链式存储时,可以从前往后查找指定元素的位置。
在最好情况下,元素已经有序,每插入一个元素,只需比较一次而不用移动元素,此时时间复杂度为O(n)
在最坏情况下,元素为逆序,总比较次数达到最大,为
∑
i
=
2
n
i
\sum_{i=2}^n i
i=2∑ni
总的移动次数也达到最大,为
∑
i
=
2
n
i
+
1
\sum_{i=2}^n i+1
i=2∑ni+1
在平均情况下,总的比较次数与总的移动次数均约为
n
2
/
4
n^2/4
n2/4
折半插入排序 |
核心代码:
void InsertSort_BinarySearch(int A[],int n){
int i,j,low,high,mid;
for(i=2;i<=n;i++){
A[0] = A[i];
low = 1; high = i - 1; //设置折半查找范围
while(low<=high){ //折半查找(默认递增有序)
mid = (low + high) / 2; //取中间点
if(A[mid]>A[0]){ //查找左半子表
high = mid - 1;
}else{ //查找右半子表
low = mid + 1;
}
}
for(j=i-1;j>=high+1;j--){ //统一后移元素,空出插入位置
A[j+1] = A[j];
}
A[high+1] = A[0]; //high+1与low指向相同,可替换
}
}
性能分析:
空间效率 | O(1) |
时间效率 | O(n^2) |
稳定性 | 稳定 |
适用性 | 仅适用于顺序存储 |
折半插入排序仅减少了比较元素的次数,约为
O
(
n
log
2
n
)
O(n\log_2n)
O(nlog2n)
元素移动次数并未改变。
希尔排序 |
基本思想是,先将待排序表分割成若干形如L[i,i+d,i+2d,…,i+kd]的“特殊”子表,即把相隔某个“增量”的记录组成一个子表,对各个子表分别进行直接插入排序,当整个表中的元素已呈“基本有序”是,再对全体记录进行一次直接插入排序。
实现图示:
核心代码:
void ShellSort(int A[],int n){
int i,j,dk;
for(dk=n/2;dk>=1;dk=dk/2){ //步长变化
for(i=dk+1;i<=n;i++){ //从第1组第二个数开始
if(A[i]<A[i-dk]){ //需要将A[i]插入有序增量子表
A[0] = A[i]; //暂存在A[0]
for(j=i-dk;j>0&&A[0]<A[j];j-=dk){
A[j+dk] = A[j]; //记录后移,查找插入位置
}
A[j+dk] = A[0];
}
}
}
}
注意:代码实现与图示不同,代码中是所有子表同时进行排序。
性能分析:
空间效率 | O(1) |
时间效率 | O(n^2) |
稳定性 | 不稳定 |
适用性 | 仅适用于顺序存储 |
时间效率:当n在某个特定范围时,希尔排序的时间复杂度约为O(n^1.3)。