数据结构——十大排序算法
排序分类如下:
(默认排序结果是非递减有序序列)
(本文章是整理学习笔记,引用自2022王道考研书)
一、直接插入排序
算法思想:
有序序列 L [1……i-1] | L [i] | 无序序列 L [i+1……n] |
---|---|---|
例如:2,3 | 5 | 9,4,1,8,7,6,0 |
- 将待排序的表L从i(初始化i=0)下标位置分为有序表L[1……i]和L[i+1……n];
- 查找L [i] 在有序序列L [1……i-1]中的插入位置下标 k ;
- 将无序表L[i+1……n]中所有元素依次后移一个位置; //将k位置空出来
- 将L[i]复制到L[k];
代码实现:
void InsertSort(ElemType A[],int n){
int i,j;
for(i=2;i<=n;i++){ //将A[2]~A[n]插入到之前的有序序列之中,下标从1到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]<=A[0]时跳出循环,此时A[j]理应排在A[0]之前
//插入位置找到,下标为j+1;
A[j+1]=A[0]; //复制A[0]到插入位置
}
}
}
性能分析:
空间:仅用了常数个辅助单元,因而空间复杂度为O(1)
时间:最好情况:表中元素已经有序,时间复杂度O(n);最坏情况:表中元素刚好逆序,时间复杂度O(n^2);
总比较次数(不包括与哨兵的比较1次)为n*n(n-1)/2;总移动次数为
平均情况:时间复杂度O(n^2),总的比较次数和总移动次数约为n^2/4
稳定性:稳定
适用性:顺序存储和链式存储
二、折半插入排序
算法思想:
- 其实折半插入排序就是直接插入排序的改良版。算法思想一致.
只是将查找插入位置的方法由原来的从后往前顺序查找换成了二分法查找
代码实现:
void InsertSort(ElemType A[], int n){
int i,j,low,high,mid;
for(i=2;i<=n,i++){ //将A[2]~A[n]插入到之前的有序序列之中,下标从1到n;
if(A[i]<A[i-1]){ //为了减少比较,不用这个语句也行
A[0]=A[i];
low=1;high=i-1; //对1到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]; //复制到插入位置
}
}
}
性能分析:
时间复杂度与直接插入排序一致;
稳定性:稳定;适用顺序存储线性表;
数据量不大时,折半插入排序性能很好;
比较次数改变,约为O(nlog2(n)),**仅与表中元素个数n有关**;
三、希尔排序(缩小增量排序)
算法思想:
-
将表中间隔某个“增量”d的元素组成若干个子表
(例如,{2,3,5,9,4,1,8,7,6,0},选定增量为3,得到子表1 {2,9,8,0},子表2 {3,4,7},子表3 {5,1,6}。)
-
对各个子表分别进行直接插入排序,再对全表L进行一次直接插入排序
-
希尔提出选择d1=n/2,d(i+1)=d(i)/2(下边界)——3/2取下边界为1,并且最后一个增量为1;
就是增量不断减半取下界,直到d=1
代码实现:
void ShellSort(ElemType A[],int n){
for(int dk=n/2;dk>=1;dk=dk/2){ //增量的改变
for(int i=dk+1;i<=n;i++){ //对子表的直接插入排序.相当于原来的增量为1现在变为了dk
//子表的下标变为了i,i+dk,i+2dk,i+3dk…………
if(A[i]<A[i-dk]){ //和其他插入排序一样,减少比较次数
A[0]=A[i]; //存放A[i],作用没变
for(int j=i-dk;j>0&&A[j]>A[0];j-=dk){
A[j+dk]=A[j]; //后移元素
}
A[j+dk]=A[0]; //复制到插入位置
}
}
}
}
性能分析:
空间复杂度:O(1)
时间复杂度:依赖于增量序列的函数。无法分析。当n有特定范围时,最坏时间复杂度为O(n^2)
稳定性:不稳定
适用性:仅**顺序存储**线性表
四、冒泡排序
算法思想:
-
为体现冒泡,从后面往前面两两比较相邻的元素,将较小的元素送到无序序列的最前面,
也就是有序序列的最后面
-
设置i指针指向有序序列的最后一个元素,j指向无序序列的最后一个元素,默认第一个为有序序列
-
A[j]与A[j-1]比较,若A[j]<A[j-1],交换A[j]与A[j-1]的位置
-
当j<=i时跳出循环,不发生交换,结束一次冒泡过程。当i指到最后一个元素时结束冒泡排序过程
代码实现:
void BUbbleSort(ElemType A[],int n){
for(int i=0;i<n;i++){
bool flag=false; //不发生交换的标志
for(int j=n-1;j>i;j--){
if(A[j]<A[j-1]){
swap(A[j],A[j-1]); //交换
flag=true;
}
}
if(flag==false)
return 0; //不发生交换,说明已然有序
}
}
性能分析:
空间复杂度:O(1)
初始序列有序时,最好时间复杂度O(n)
初始序列逆序时,最坏时间复杂度O(n^2),比较次数n*(n-1)/2,移动次数3n(n-1)/2;
平均时间复杂度O(n^2);
稳定性:稳定。
五、快速排序
算法思想:
- 基于分治法,在表L中任取一元素pivot将表分为左子表L[1……k-1]和右子表L[k+1……n],L[k]=pivot
- 左子表中所有元素均小于pivot,右子表中所有元素均大于pivot,这个过程为一趟快速排序
- 用同样的方法对左子表和右子表进行快速排序,若待排序列中只有一个元素则结束排序
- 根据严蔚敏版教材《数据结构》,假设每次pivot为当前表的第一个元素,pivot=A[low]
代码实现:
void QuickSort(ElemType A[],int low,int high){
if(low<high){ //递归跳出的条件
int pivotpos=Partition(A,low,high); //划分点的下标
Partition(A,1,pivotpos-1); //对两子表快速排序
Partition(A,pivotpos+1,high);
}
}
int Partition(ElemType A[],int low,int high){
ElemType pivot=A[low];
while(low<high){
while(low<high && A[high] >=pivot) --high;
A[low]=A[high]; //high指针左移遇到的小于pivot的值将其放到low的位置上
while(low<high && A[low] <=pivot) ++low;
A[high]=A[low]; //low指针右移遇到的大于pivot的值将其放到high的位置上
}
A[low]=pivot; //循环结束,low和high都指向k,左边小,右边大,pivot放到应该的位置
return low;
}
性能分析:
空间: 需要一个递归工作栈,其容量与递归调用的最大深度一致。
最好情况是O(log2(n));最坏情况是O(n);平均情况为O(log2(n))
时间:最好情况与平均情况为O(nlog2(n));最坏情况为O(n^2);
稳定性:不稳定。
六、简单选择排序
算法思想:
- 设置两个指针i和j,i遍历所有元素,刚开始设置A[i]为A[i……n-1]中最小元素;
- j是工作指针,从i+1开始寻找A[i……n-1]序列中最小的元素,记录下标min
- 将A[min]与A[i]交换,i继续往后遍历,当min==i时,到达最后一个,不用交换,结束排序。
代码实现:
void SelectSort(ElemType A[],int n){
for(int i=0;i<n,i++){
min=i; //min指向A[i……n-1]中最小的元素
for(int j=i+1;j<n,j++){
if(A[j]<A[min]) min=j;
}
if(min!=i) swap(A[i],A[min]);
}
}
性能分析:
空间:O(1)
时间:最好情况移动0次,最坏移动不超过3(n-1);比较次数始终是n(n-1)/2次;
时间复杂度始终是O(n^2)
稳定性:不稳定。
七、堆排序
算法思想:
建堆过程:从第一个非叶子结点(下标为len/2取下界)从下往上建成大根堆(下标逐渐减一直到0)
调整过程:调整以A[k]为根的子树为大根堆(代码注释有)
排序过程:
1、首先将待排序列建成初始堆。
2、将堆底元素与堆顶元素交换,破坏了以堆顶为根的大堆根(堆顶最大,堆底最小)
3、对堆顶到堆底的堆进行调整
4、设置指针i从len开始不断往上,将以1为堆顶,i为堆底的堆调整为大堆根(重复第2和3步);
5、堆底指针与堆顶重合等于1,结束排序。A[1]到A[len]从大到小;
代码实现:
void BuildMaxHeap(ElemType A[],int len){ //建堆
for(int i=len/2;i>0;i--){
HeadAdjust(A,i,len);
}
}
void HeadAdjust(ElemType A[],int k,int len){
//该函数将A[k]为根的子树进行调整为大根堆
A[0]=A[k]; //A[0]暂存调整堆的大小
for(int i=2*k;i<len;i*=2){
if(i<len&&A[i]<A[i+1]) i++; //i指向k左右子结点较大的元素
if(A[0]>=A[i]) break; //若根节点比子节点大,则不用调整
else{
A[k]=A[i];
k=i; //方便继续往下调整子树
}
}
A[k]=A[0]; //A[0]存放该调整树的最大值
}
void HeapSort(ElemType A[],int len){ //堆排序
BuiliMaxHead(A,len); //将待排序列建堆
for(i=len;i>1;i--){ //n-1次的交换和建堆过程
Swap(A[i],A[1]); //输出堆顶元素(和堆底元素交换)————最小值与最大值的交换
HeadAdjust(A,1,i-1); //调整,把剩余i-1个元素整理成堆
}
}
//此次排序为逆序(从大到小);
性能分析:
堆排序适合关键字较多的情况。例如一亿中选出前100个最大值
空间:O(1)
时间:建堆时间为O(n);最好、最坏、平均情况都是O(nlog2(n))
稳定性:不稳定
这是王道书上的我的理解;堆排序更多解释参考:堆排序
八、归并排序
算法思想:
- 将前后相邻的两个有序表归并为一个有序表
- 递归调用第一步
代码实现:
ElemType *B=(ElemType *)malloc((n+1)*sizeof(ElemType)); //辅助数组B
void Merge(ElemType A[],int low,int mid,int high){
//将A表中的两段合并成一段有序表
for(int k=low;k<=high;k++){ //将A中内容复制给B
B[k]=A[k];
}
for(int i=low,j=mid+1,k=i;i<=mid&&j<=high;k++){
if(B[i]<=B[j]) A[K]=B[i++]; //较小者复制到A中
else A[k]=B[j++];
}
while(i<=mid) A[k++]=B[i++]; //表一未检测完,复制
while(j<=high) A[k++]=B[j++]; //表二未检测完,复制
}
void MergeSort(ElemType 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(n)
时间:O(nlog2(n))
稳定性:稳定。
(后面两个排序较为复杂,需要图解,看链接)
九、基数排序
链接
十、多路归并排序
链接