排序的基本概念
算法的稳定性
- 关键字相同的元素经过排序后相对顺序是否会改变
内部排序与外部排序
- 内部排序:数据都在内存中
- 外部排序:数据太多,无法全部放入内存
插入排序
直接插入排序
- 每次将一个待排序的记录按其关键字大小插入到前面已排好序的子序列中
- 空间复杂度:
O(1)
- 平均时间复杂度:
O(n^2)
- 算法稳定性:稳定
void InsertSort(int A[],int n){
int i,j,temp;
for(i=1;i<n;i++){
if(A[i]<A[i-1]){
temp=A[i];
for(j=i-1;j>=0&&A[j]>temp;--j){
A[j+1]=A[j]; // 检查前面比当前元素大的右移
}
A[j+1]=temp;
}
}
}
折半插入排序
- 先折半查找找到应该插入的位置,再移动元
- 仅适用于顺序表
- 平均时间复杂度:
O(n^2)
- 移动元素次数变少了,但关键字对比的次数依然是
O(n^2)
数量级
- 移动元素次数变少了,但关键字对比的次数依然是
- 算法稳定性:稳定
- 当
A[mid]==A[0]
时,应继续在mid
所指位置右边寻找插入位置
- 当
希尔排序
- 先追求表中元素部分有序,再逐渐逼近全局有序
- 先将待排序表分割成若干形如
L[i,i+d,i+2d,...,i+kd]
的“特殊”子表,对各个子表分别进行直接插入排序,缩小增量d
,重复上述过程,直到d=1
为止 - 算法稳定性:不稳定
void ShellSort(int A[],int n){
int i,j,temp,d;
for(d=n/2;d>=1;d=d/2){
for(i=d;i<n;i++){
if(A[i]<A[i-d]){
temp=A[i];
for(j=i-d;j>=0&&A[j]>temp;j-=d){
A[j+d]=A[j]; // 检查前面比当前元素大的右移
}
A[j+d]=temp;
}
}
}
}
交换排序
冒泡排序
- 从后往前(或从前往后)两两比较相邻元素的值,若为逆序,则交换它们
- 平均时间复杂度:
O(n^2)
- 算法稳定性:稳定
void BubbleSort(int A[],int n){
int i,j,temp,flag;
for(i=0;i<n-1;i++){
flag=false;
for(j=n-1;j>i;j--){
if(A[j]<A[j-1]){
temp=A[j];
A[j]=A[j-1];
A[j-1]=temp;
flag=true;
}
}
if(flag==false)
return; //本趟遍历后没有发生交换,说明已经有序
}
}
快速排序
- 在待排序表中任选一个元素
pivot
作为枢轴,通过一趟排序将待排序表分为小于pivot
和大于等于pivot
两部分,pivot
放到最终位置上,然后分别递归地对两个子表重复该过程 - 平均时间复杂度:
O(nlog_2n)
- 快速排序是所有内部排序算法中平均性能最优的排序算法
- 稳定性:不稳定
在快速排序第n趟完成时,会有n个以上的数出现在最终位置,即左边的数都比它小,右边的数都比它大
int Partition(int A[],int low,int high){
int pivot=A[low];
while(low<high){
while(low<high&&A[high]>=pivot){
--high;
}
A[low]=A[high]; // 比枢轴小的元素移动到左边
while(low<high&&A[low]<=pivot){
++low;
}
A[high]=A[low]; // 比枢轴大的元素移动到右边
}
A[low]=pivot;
return low;
}
void Quicksort(int A[],int low,int high){
if(low<high){ //递归跳出条件
int pivotpos=Partition(A,low,high);
Quicksort(A,low,pivotpos-1);
Quicksort(A,pivotpos+1,high);
}
}
选择排序
简单选择排序
- 每一趟在待排元素中选取关键字最小的元素加入有序子序列
- 平均时间复杂度:
O(n^2)
- 稳定性:不稳定
void Selectsort(int A[],int n){
int i,j,temp,min;
for(i=0;i<n-1;i++){
min=i;
for(j=i+1;j<n;j++){
if(A[j]<A[min]){
min=j; //得到最小关键字的索引
}
}
if(min!=i){
temp=A[min];
A[min]=A[i];
A[i]=temp;
}
}
}
堆排序
- 每一趟在待排元素中选取关键字最小(或最大)的元素加入有序子序列
- 大根堆:最大元素存放在根节点,且其任一非根节点的值小于等于其双亲节点值
- 建立大根堆:
- 把所有非终端节点都检查一遍,是否满足大根堆的要求,如果不满足,则进行调整
- 在顺序存储的完全二叉树中,非终端节点编号i<=n/2向下取整
- 检查当前节点是否满足根>=左、右,若不满足,将当前节点与更大的一个孩子互换
- i的左孩子–2i
- i的右孩子–2i+1
- i的父节点–i/2向下取整
- 若元素互换破坏了下一级的堆,则采用相同方法继续向下调整
- 堆排序:每一趟将堆顶元素加入有序子序列(与待排序序列中最后一个元素交换)并将待排元素序列再次调整为大根堆
- 平均时间复杂度:
O(nlog_2n)
- 稳定性:不稳定
//建立大根堆
void HeadAdjust(int A[],int k,int len){
int i;
A[0]=A[k]; //A[0]不存放元素
for(i=2*k;i<=len;i*=2){
if(i<len&&A[i]<A[i+1]){
i++; //获取左右孩子中更大的
}
if(A[0]>A[i])
break;
else{
A[k]=A[i];
k=i; //向下继续筛选
}
}
A[k]=A[0]; //注意此时k值已经改变,为该节点最终位置
}
void BuildMaxHeap(int A[],int len){
int i;
for(i=len/2;i>0;i--){ //从后往前调整所有非终端节点
HeadAdjust(A,i,len);
}
}
// 堆排序
void HeapSort(int A[],int len){
BuildMaxHeap(A,len);
int temp;
for(int i=len;i>1;i--){
temp=A[i];
A[i]=A[1];
A[1]=temp;
HeadAdjust(A,1,i-1); //把剩余待排元素整理为大根堆
}
}
归并排序
- 把两个或多个已经有序的序列合并成一个
- 空间复杂度:O(n)
- 平均时间复杂度:
O(nlog_2n)
- 稳定性:稳定
int *B=(int *)malloc((n+1)*sizeof(int)); // 辅助数组B
void Merge(int A[],int low,int mid,int high){
for(int k=low;k<=high;k++){
B[k]=A[k];
}
int i,j,k;
for(i=low,j=mid+1,k=i;i<=mid&&j<=high;k++){
if(B[i]<=B[j]){
A[k]=B[i++];
}else{
A[k]=B[j++];
}
}
while(i<=mid)
A[k++]=B[i++];
while(j<=high){
A[k++]=B[j++];
}
}
void MergeSort(int A[],int low,int high){
if(low<high){
int mid=(low+high)/2;
MergeSort(A,low,mid);
MergeSort(A,mid+1,high);
Merge(A,low,mid,high); //归并
}
}
基数排序
- 空间复杂度:O®
- 时间复杂度:O(d(n+r))
- n:长度为n的线性表
- d:关键字的个数
- r:基数,每个关键字的取值范围
- 基数排序擅长解决的问题
- 数据元素可以方便地拆分为d组,且d较小
- 每组关键字的取值范围不大,即r较小
- 数据元素个数n较大
- 稳定性:稳定
外部排序算法
基本概念
- 外存排序:数据元素太多,无法一次全部读入内存进行排序
- 外部排序原理:使用归并排序,最少只需在内存中分配3块大小的缓冲区即可对任意一个大文件进行排序
步骤
- 构造初始归并段:“归并排序”要求各个子序列有序,每次读入k个块的内容,进行内部排序后写回磁盘,生成r个初始归并段
- 进行S趟k路归并
S = ⌈log_kr⌉
- k路归并:
- 把k个归并段的块读入k个输入缓冲区
- 用“归并排序”的方法从k个归并段中选出几个最小的记录暂存到缓冲区中
- 当输出缓冲区满时,写出外存
外部排序时间开销:读写外存的时间+内部排序时间+内部归并时间
优化思路:
- 多路归并,增加归并路数k
- 需要增加相应的输入缓冲区
- 每次从k个归并段中选一个最小元素需要(k-1)次关键字对比,使用败者树,对比次数减少为
⌈log_2k⌉
- 减少初始归并段数量r,使用选择-置换排序