内部排序
插入排序
直接插入排序
某一过程的状态
有序序列 | L(i) | 无序序列 |
特征:前部分有序
算法步骤
前$i$元素组成有序区
后$n-i-1$个元素组成无序区
将第$i$个元素按序插入有序区
以此类推
代码
void InsertSort(List L){
for(int i = 2; i <= n; ++i){
if(L[i] < L[i-1]){
L[0] = L[i];
for(int j = i - 1; L[0] < L[j]; --j)
L[j+1] = L[j];
L[j+1] = L[0];
}
}
}
算法性能
- 空间效率:$\text{O}(1)$
- 时间效率:
- 最好情况:$\text{O}(n)$
- 最坏情况:移动$\sum_{i=2}^n(i+1)$
- 平均情况:$\text{O}(n^2)$
- 稳定性:√
- 适用性:基本有序顺序表
折半插入排序
代码
void InsertSort(ElemType A[], int n){
int low, high, mid;
for(int i=0 ; i <= n ; i++){
A[0] = A[i]; // A[i]暂存A[0] 折半查找
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(int j = i-1; j >= high + 1; --j){ // 统一后移
A[j+1] = A[j];
A[high+1] = A[0];
}
}
}
算法性能
减少了比较次数$\text{O}(n\log n)$
- 时间效率:$\text{O}(n^2)$
- 适用性:数据率不大的排序表
希尔排序
void ShellSort(ElemType A[], int n){
for(int dk = n / 2; dk >= 1; dk = dk / 2) // 步长从一半逐渐减少
for(int i = dk + 1; i <= n; ++i) // 步长右移
if(A[i] < A[i - dk]){
A[0] = A[i];
for(int j = i-dk; j>0 && A[0]<A[j]; j -= dk)
A[j+dk] = A[j];
A[j+dk] = A[0];
}
}
算法性能
- 空间效率:$\text{O}(1)$
- 时间效率:$\text{O}(n^2)$
- 稳定性:×
- 适用性:顺序存储线性表
交换排序
冒泡排序
算法步骤
- 检查$D[0],D[1]$,$D[1],D[2]$,$……$,$D[n-i-1],D[n-i]$是否逆序,逆序则交换
代码
void BubbleSort(List *list)
{
int i,j; // i标识每趟排序范围最后一个元素下标,每趟排序下标为0~i
BOOL isSwap = FALSE; // 标记一趟排序中是否发生了元素交换
for(i = list->n-1 ; i > 0 ; i--)
for(j = 0 ; j < i ; j++)
if(list->D[j].key > list->D[j+1].key)
{
Swap(list->D, j, j+1);
isSwap = TRUE;
}
if(!isSwap) // 若本趟排序无元素交换,排序完成
break;
}
算法性能
- 空间效率:$\text{O}(1)$
- 时间效率:
- 比较次数:$\dfrac{n(n-1)}{2}$
- 移动次数:$\dfrac{3n(n-1)}{2}$
- 最坏情况:移动$\sum_{i=2}^n(i+1)$
- 平均情况:$\text{O}(n^2)$
- 稳定性:√
- 适用性:顺序存储线性表
快速排序
特征:第$i$躺有$i$个元素到达最终位置
步骤
待排序序列元素数量小于1,退出
选择分割元素$D_s$,划分为左右子序列
左子序列所有元素小于$D_s$
右子序列所有元素大于$D_s$
子序列快速排序
代码
int Partition(List *list, int low, int high)
{
int i = low;
int j = high+1;
Entry pivot = list->D[low]; // pivot是分割元素
do{
do
i++;
while(list->D[i].key < pivot.key && i <= high); // i前进
do
j--;
while(list->D[i].key > pivot.key && j >= low); // i前进
if(i < j)
Swap(list->D, i, j);
}while(i < j);
Swap(list->D, low, j);
return j; // j是分割元素下标
}
算法性能
- 空间效率:调用递归工作栈
- 最好情况:$\text{O}(\log n)$
- 最坏情况:移动$\text{O}(n)$
- 平均情况:$\text{O}(\log n)$
- 时间效率:
- 平均情况:$\text{O}(\log n)$
- 稳定性:×
- 适用性:顺序存储线性表
选择排序
简单选择排序
void SelectSort(List* list)
{
int minIndex,startIndex = 0;
while(startIndex < list->n-1)
{
minIndex = FindMin(*list, startIndex);
Swap(list->D, startIndex, minIndex);
startIndex++;
}
}
- 空间效率:$\text{O}(1)$
- 时间效率:$\text{O}(n^2)$
- 稳定性:×
堆排序
步骤
交换堆顶元素$D[0]$与堆底元素$D[n-i]$,调整为最大堆
算法性能
- 空间效率:$\text{O}(1)$
- 时间效率:
- 最好情况:$\text{O}(n\log n)$
- 最坏情况:$\text{O}(n\log n)$
- 平均情况:$\text{O}(n\log n)$
- 稳定性:×
归并排序 & 基数排序
归并排序
将两个有序序列合并成一个有序序列则是利用了经典的「归并排序」。
比较次数:$\left[N,2N-1\right]$
步骤
对$[\dfrac{n}{2^{i-1}}]$个有序序列,合并$(D[0],…,D[2^{i-1}-1])$和$(D[2^{i-1}],…,D[2^{i}-1])$
- 若$[\dfrac{n}{2^{i-1}}]$是偶数,合并最后两个有序序列,否则最后一个序列不合并
代码
/*子序列合并*/
void Merge(List *list, Entry *temp, int low, int n1, int n2)
{
int i =low, j = low + n1; // i,j初始时分别指向两个序列第一个元素
while( i <= low + n1 -1 && j <= low + n1 + n2 -1)
{
if(list->D[i].key <= list->D[j].key)
*temp++ = list->D[i++];
else
*temp++ = list->D[j++];
}
while(i <= low + n1 - 1)
*temp++ = list->D[i++]; // 剩余元素直接拷贝至temp
while(j <= low + n1 + n2 -1)
*temp++ = list->D[j++]; // 剩余元素直接拷贝至temp
}
void MergeSort(List *list)
{
Entry temp[MaxSize];
int low, n1, n2, i, size = 1;
while(size < list->n)
{
low = 0; // low是一对待合并序列第一个序列第一个下标
while(low + size < list->n) // 至少两个序列要合并
{
n1 = size;
if(low + size*2 < list->n)
n2 = size; // 计算第二个序列长度
else
n2 = list->n - low -size;
Merge(list, temp+low, low, n1, n2);
low += n1 + n2; // 确定下一对待合并序列中第一个序列第一个元素下标
}
for(i=0 ; i<low ; i++)
list->D[i] = temp[i]; // 复制一趟合并排序结果
size *= 2; // 子序列长度翻倍
}
}
算法性能
- 空间效率:$\text{O}(n)$
- 时间效率:
- 平均情况:$\text{O}(n\log n)$
- 稳定性:√
基数排序
算法性能
- 空间效率:$\text{O}(r)$
- 时间效率:
- 平均情况:$\text{O}(d(n+r))$
- 稳定性:×
外部排序
特点:
- 文件较大,内存放不下
- 算法:归并排序
- 减少内外存读取次数:增大归并路数败者树、减少归并段数置换选择排序
初始归并段个数:$r=$磁盘记录$/$内存工作区容量
归并趟数:$S=\left\lceil\log_mr\right\rceil$
不使用败者树比较次数:$S(n-1)(m-1)=\left\lceil\log_mr\right\rceil(n-1)(m-1)$
使用败者树比较次数:$S(n-1)\left\lceil\log_mr\right\rceil=(n-1)=\left\lceil\log_2r\right\rceil$
缓冲:$m$个输入缓冲区;$1$个输出缓冲区
总结
关键字比较次数:$\left \lceil \log_2(n!) \right \rceil \le t \le \dfrac{n(n-1)}2$
排序算法比较
趟数和初始状态有关:交换类、直接插入排序$n-1$、简单排序$n-1$、基数排序$d$
复杂度 | 插入 | 折半 | 希尔 | 冒泡 | 快速 | 选择 | 堆 | 归并 | 基数 |
---|---|---|---|---|---|---|---|---|---|
最好 | $O(n)$ | $O(n)$ | $O(n)$ | $O(\log n )$ | $O(n^2)$ | $O(n\log n )$ | $O(n\log n )$ | $ | |
最差 | $O(n^2)$ | $O(n^2)$ | $O(n^2)$ | $O(n^2)$ | $O(n^2)$ | $O(n\log n )$ | $O(n\log n )$ | $ | |
平均 | $O(n^2)$ | $O(n^2)$ | $O(n^{1.3})$ | $O(n^2)$ | $O(n\log n )$ | $O(n^2)$ | $O(n\log n )$ | $O(n\log n )$ | $ |
空间复杂度
插入排序 | 插入排序 | 希尔排序 | 冒泡排序 | 快速排序 | 选择排序 | 堆排序 | 归并排序 | 基数排序 |
---|---|---|---|---|---|---|---|---|
$O(1 )$ | $O(1)$ | $O(1 )$ | $O(1 )$ | $O(\log n )$ | $O(1 )$ | $O(1 )$ | $O(n)$ | $ |
稳定性
插入排序 | 希尔排序 | 冒泡排序 | 快速排序 | 选择排序 | 堆排序 | 归并排序 | 基数排序 |
---|---|---|---|---|---|---|---|
√ | × | √ | × | × | × | √ | √ |
一趟确定位置
插入排序 | 希尔排序 | 冒泡排序 | 快速排序 | 选择排序 | 堆排序 | 归并排序 | 基数排序 |
---|---|---|---|---|---|---|---|
× | × | √ | √ | √ | √ | × |
适用场合
插入排序 | 希尔排序 | 冒泡排序 | 快速排序 | 选择排序 | 堆排序 | 归并排序 | 基数排序 |
---|---|---|---|---|---|---|---|
基本有序递增 | 基本有序且简单快速实现 | 非常无序 | 无 | 关键字较多;取排序前$k$个元素 | 多数场合,不要节省空间外部排序 | 不可对浮点数排序 |