文章目录
一、排序的基本概念
算法的稳定性
- 关键字相同的元素经过排序后相对顺序是否会改变
内部排序与外部排序
- 内部排序:数据都在内存中----关注时间、空间复杂度、稳定性
- 外部排序:数据太多,无法全部放入内存----还需关注磁盘读取次数
二、插入排序
直接插入排序
- 每次将一个待排序的记录按其关键字大小插入到前面已排好序的子序列中
- 平均时间复杂度:
O(n^2)
- 算法稳定性:稳定
c
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;
}
}
}
希尔排序
- 先追求表中元素部分有序,再逐渐逼近全局有序
- 仅适用于顺序表
- 算法过程:
- 相隔增量
d
的元素组成同一个子表,对各个子表分别进行直接插入排序,缩小增量d
,直到d=1
为止
- 相隔增量
- 算法稳定性:不稳定
- 相同关键字划分到不同子表,可能改变相对次序
c
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)
- 算法稳定性:稳定
c
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
划分待排序表 - 算法过程
- 两个指针
low
和high
向中间移动,将小于pivot
的元素放到左边,大于pivot
的元素放到右边,pivot
元素放到最终位置上,再对左右递归
- 两个指针
- 快速排序是所有内部排序算法中平均性能最优的排序算法
- 空间复杂度:
- 最好
O(log_2n)
- 最坏
O(n)
- 平均
O(log_2n)
- 最好
- 平均时间复杂度:
O(nlog_2n)
- 稳定性:不稳定
c
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)
- 稳定性:不稳定
c
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⌋
- 由后向前检查所有非终端节点,若不满足根>=左、右,将当前节点与更大的一个孩子互换
- 若元素互换破坏了下一级的堆,则采用相同方法继续向下调整
- 建堆的时间复杂度:
O(n)
- 在顺序存储的完全二叉树中,非终端节点编号
- 输出堆顶元素:最后一个元素替换堆顶
- 算法过程
- 首先建立大根堆,不断输出堆顶元素,并将剩余元素序列再次调整为大根堆
- 堆的插入删除
- 插入:插入到堆底后"上升"
- 删除:用堆底元素替换后"下坠"
- 平均时间复杂度:
O(nlog_2n)
- 稳定性:不稳定
c
//建立大根堆
void BuildMaxHeap(int A[],int len){
int i;
for(i=len/2;i>0;i--){ //从后往前调整所有非终端节点
HeadAdjust(A,i,len);
}
}
//将以k为根的子树调整为大根堆
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]; //A[i]调整到双亲结点上
k=i; //向下继续筛选
}
}
A[k]=A[0]; //注意此时k值已经改变,为该节点最终位置
}
// 堆排序
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); //把剩余待排元素整理为大根堆
}
}
五、归并排序
二路归并排序
- 将相邻的两个有序表合并成一个
归并排序
- 将n个待排元素分成两个长度相等的子序列,分别对两个子序列递归调用归并方法,再将两有序子序列二路归并排序
- 空间复杂度:
O(n)
- 平均时间复杂度:
O(nlog_2n)
- 稳定性:稳定
c
int *B=(int *)malloc((n+1)*sizeof(int)); // 辅助数组B
void Merge(int A[],int low,int mid,int high){// mid分割两个待归并序列
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); //归并
}
}
六、基数排序
多关键字排序
- 按照关键字各位大小排序
n
:待排元素个数d
:单个元素分量的个数rd
:基数,每个分量的取值范围- 单逻辑关键字:文件中任意一关键字
k
均由d
个分量构成,且每个分量都是单独的关键字 - 排序方法:
- 最高位优先法
MSD
--逐层分割成若干子序列 - 最低位优先法
LSD
--不必分成若干子实现排序序列,不通过关键字比较,通过"分配"+"收集"实现排序
- 最高位优先法
链式基数排序
- 算法过程:
- 将关键字拆分为
d
位(或组) - 按照各个关键字位权重递增的次序(
LSD
方法),做d
趟分配O(n)
和收集O(rd)
- 将关键字拆分为
- 空间复杂度:
O(rd)
- 时间复杂度:
O(d(n+rd))
- 稳定性:稳定
七、内部排序算法的比较
- 各算法性质比较表
排序算法 | 最好时间复杂度 | 平均时间复杂度 | 最坏时间复杂度 | 空间复杂度(平均) | 是否稳定 |
---|---|---|---|---|---|
直接(折半)插入排序 | O(n) | O(n2) | O(n2) | O(1) | 稳定 |
希尔排序 | - | - | - | O(1) | 不稳定 |
冒泡排序 | O(n) | O(n2) | O(n2) | O(1) | 稳定 |
快速排序 | O(nlog2n) | O(nlog2n) | O(n2) | O(log2n) | 不稳定 |
简单选择排序 | O(n2) | O(n2) | O(n2) | O(1) | 不稳定 |
堆排序 | O(nlog2n) | O(nlog2n) | O(nlog2n) | O(1) | 不稳定 |
归并排序 | O(nlog2n) | O(nlog2n) | O(nlog2n) | O(n) | 稳定 |
基数排序 | O(d(n+rd)) | O(d(n+rd)) | O(d(n+rd)) | O(rd) | 稳定 |
-
规律总结
- 每次排序确定一个最终位置:插入排序,冒泡排序,选择排序,快速排序
- 算法复杂度与初始状态无关:选择排序、堆排序、归并排序、基数排序
- 比较次数与初始状态无关:选择排序、基数排序
- 移动次数与初始状态无关:归并排序、基数排序
- 排序趟数与初始状态有关:快速排序、冒泡排序
-
根据关键字分布情况选择排序方法
- n较小,基本有序:直接插入排序
- n较小,数据项较多,所占存储空间较大:简单选择排序