| 名称 | 就地性 | 内部/外部 | 稳定性 | 时间复杂度 |
| 插入排序 | 就地 | 内部 | 稳定 | O(n^2) |
| 希尔排序 | 就地 | 内部 | 不稳定 | 最坏O(n^2) |
| 冒泡排序 | 就地 | 内部 | 稳定 | O(n^2) |
| 快速排序 | 就地 | 内部 | 不稳定 |
O(nlogn) |
| 选择排序 | 就地 | 内部 | 不稳定 | O(n^2) |
| 堆排序 | 不一定 | 内部 | 不稳定 | O(nlogn) |
| 归并排序 | 非就地 |
2-路归并内部排序 | 稳定 |
O(nlogn) |
| 计数排序 | 非就地 | 内部 | 使用前缀和数组保证稳定 |
O(n+maxx) |
| 基数排序 | 非就地 | 内部 | 使用前缀和数组保证稳定 | O(d * (n+b)) |
| 桶排序 | 非就地 | 内部 | 取决桶内排序的稳定性 | 不唯一 |
排序算法主要有以下几类:插入排序(直接插入排序,希尔排序),交换排序(冒泡排序,快速排序),选择排序(简单选择排序,堆排序),归并排序(2-路归并),统计排序(桶排序,基数排序)
一.插入排序
1.1直接插入排序
插入排序很简单,和“斗地主”时整理牌序的过程很像。主要的实现思想是将数据按照⼀定的顺序⼀个⼀个的插⼊到有序的表中,最终得到的序列就是已经排序好的数据。而直接插⼊排序是插⼊排序算法中的⼀种,采⽤的⽅法是:在添加新的记录时,使⽤顺序查找的⽅式 找到其要插⼊的位置,然后将新记录插⼊。
#include<stdio.h>
#include<stdlib.h>
//就地排序,内部排序,稳定,时间复杂度O(n^2)
int n,a[105];
int main()
{
scanf("%d",&n);
for(int i=1;i<=n;i++)
{
scanf("%d",&a[i]);
}
int k,t;//k用于倒序遍历找第一个比a[i]小或等于(保证稳定性)的数,t用于保存a[i],避免移动时将a[i]值覆盖
// for(int i=2;i<=n;i++)//枚举趟数以及每趟排序的乱序区的左边界
// {//把a[i]加入到有序区中
// //遍历i-1~1,找到第一个比a[i]小或等于(保证稳定性)的数。这里找位置的部分可以优化使用折半查找,但优化效果不明显。
// for(k=i-1;k>=1;k--)
// {
// if(a[k]<=a[i])
// {
// break;
// }
// }
// //把a[i]插入到k+1位置。在此折半查找基础上,还可以引入一个循环数组,实现2-路插入排序,但同样效果来看也是鸡肋的
// t=a[i];
// for(int j=i-1;j>=k+1;j--)
// {
// a[j+1]=a[j];
// }
// a[k+1]=t;
// }
//查找和插入移动精简版代码(边查找边替换)
for(int i=2;i<=n;i++)
{
t=a[i];
for(k=i-1;k>=1;k--)
{
if(a[k]>t)//边比较边移动
{
a[k+1]=a[k];
}else
{
break;
}
}
a[k+1]=t;
}
for(int i=1;i<=n;i++)
{
printf("%d ",a[i]);
}
}
对于直接插入排序,还有两种作用不是特别大的优化方法。就是折半插⼊排序和2-路插⼊排序。
在查找插⼊位置时,采⽤的是顺序查找的⽅式,⽽在查找表中数据本身有序的前提下,可以使⽤折半查找来代替顺序查找,这种排序的算法就是折半插⼊排序算法。2-路插⼊排序算法是在折半插⼊排序的基础上对其进⾏改进,减少其在排序过程中移动记录的次数 从⽽提⾼效率。 具体实现思路为:另外设置⼀个同存储记录的数组⼤⼩相同的数组 d,将⽆序表中第⼀个记录添加 进 d[0] 的位置上,然后从⽆序表中第⼆个记录开始,同 d[0] 作⽐较:如果该值⽐ d[0] ⼤,则添加到其右侧;反之添加到其左侧。
但因这两种方法并不能从本质改变直接插入排序O(n^2)的量级,因此不采用。对于直接插入法的优化,我们选择shell排序,也就是希尔排序。
1.2Shell排序
Shell排序又称缩小增量排序。这里又引出了增量这个概念。首先我们知道,数据量越小,排序肯定越快,数据本身越有序,排的也会更快。那么希尔排序 就是从这两点出发对算法进⾏改进得到的排序算法。
希尔排序的具体实现思路是:先将整个记录表分割成若⼲部分,分别进⾏直接插⼊排序,然后再对整个记录表进⾏⼀次直接插⼊排序。其中组数,其实就是所谓的增量。规定同一组中相邻两个数的下标相差就是增量d,并且这个增量也是变化的。那么如何确定d呢。就是使用增量序列,shell增量序列规定为:n/2,n/4,n/8...1
#include<stdio.h>
#include<stdlib.h>
//特点:就地,内部,不稳定(如果重复数据被分在不同组中就会不稳定),时间复杂度:最坏O(n^2),主要是取决于增量序列设计
int n,a[105];
int main()
{
scanf("%d",&n);
for(int i=1;i<=n;i++)
{
scanf("%d",&a[i]);
}
//shell排序:基于shell增量序列的缩小增量排序
int t,k,j=0;
for(int d=n/2;d>=1;d=d/2)//枚举增量
{
for(int i=1+d;i<=n;i++)//枚举每次排序的位置
{
//找到i位置前面同组中的第一个小于等于a[i]的位置
t=a[i];
for(k=i-d;k>0;k-=d)
{
if(a[k]>t)
{
a[k+d]=a[k];//移动到同组的下一个位置
}
else
{
break;
}
}
//k的同组的下一个位置就是插入的位置
a[k+d]=t;
}
j++;//趟数
printf("第%d趟,增量为%d, 排序后的结果:\n",j,d);
for(int i=1;i<=n;i++)
{
printf("%d ",a[i]);
}
printf("\n");
}
}
//测试样例
10
8 9 1 7 2 3 5 4 6 0
二.交换排序
交换排序也非常简单,就是在排序的过程中不断比较两个关键字,根据比较的结果进行关键字位置的交换,直到排序为止
2.1冒泡排序
这个很熟悉了,c语言第一个算法。指对n个数据进行排序,排n-1趟,同样是把数组分为有序和乱序区。在每一趟排序过程中,从前往后不断比较乱序区中相邻的两个数据,如果a[i]>a[i+1].就交换两个数,最终会把乱序区中最大的数交换到乱序区最后面,在这个过程中有序区则+1,乱序区-1。
#include<stdio.h>
#include<stdlib.h>
//冒泡排序:就地排序,内部排序,稳定,时间复杂度O(n^2)
int n,a[105];
int main()
{
scanf("%d",&n);
for(int i=1;i<=n;i++)
{
scanf("%d",&a[i]);
}
int t=0;
int flag=0;//标记是否真的实现了交换,优化一下
for(int i=1;i<=n-1;i++)//枚举趟数
{
//枚举乱序区数据 1~(n-i+1)
for(int j=1;j<=n-i;j++)
{
flag=0;
if(a[j]>a[j+1])
{
t=a[j];
a[j]=a[j+1];
a[j+1]=t;
flag=1;
}
if(flag==0)
{
break;
}
}
}
for(int i=1;i<=n;i++)
{
printf("%d ",a[i]);
}
printf("\n");
}
2.2快速排序
通过找一个基准数,把比基准数小的放在基准数前面,大的放后面。然后从基准数的位置分开成前后两部分,对前后两部分分别进行快速排序。
那么问题就来了,如何选择基准数以及如何实现把比基准数小的放在基准数前面,大的放后面。
(1)基准数选择:一般选择第一个数,最后一个数或者中间位置的数做基准数。这里以选择第一个数做基准数为例
(2)如何实现把比基准数小的放在基准数前面,大的放后面:有三种方法,分别是挖空法,左右指针法,前后指针法。这里以挖空法为例。
#include<stdio.h>
#include<stdlib.h>
//快速排序:就地排序(空间复杂度O(logn)),内部排序,不稳定,时间复杂度O(nlogn),但存在退化情况,如果待排序的数据本身就是逆序的,快速
//排序就会退化成近似冒泡排序的量级O(n^2)
int n,a[105];
void Quick_Sort(int l,int r)
{
if(l>=r) return;
//l<r
int i=l,j=r;
int p=a[l];//区间第一个位置的数做基准数
while(i<j)
{
//最开始最左边i位置是空的,要用j从后找一个比p小的数
while(i<j&&a[j]>=p) j--;
if(i<j)
{//a[j]<=p
a[i]=a[j];
i++;
}
//j位置空了,用i从前找一个比p大的数
while(i<j&&a[i]<=p) i++;
if(i<j)
{
//a[i]>=p
a[j]=a[i];
j--;
}
}
a[i]=p;//i==j 该位置就是基准数p所在位置
Quick_Sort(l,i-1);
Quick_Sort(i+1,r);
}
int main()
{
scanf("%d",&n);
for(int i=1;i<=n;i++)
{
scanf("%d",&a[i]);
}
Quick_Sort(1,n);
for(int i=1;i<=n;i++)
{
printf("%d ",a[i]);
}
printf("\n");
}
//测试样例
6
5 1 4 2 8 4
同时注意一下快速排序的退化现象即可。
三.选择排序
3.1简单选择排序
执行n-1趟排序,第i趟排序的乱序区是i~n。每趟排序都在乱序区中找到最小的数据,放到乱序区第一个位置即可。这就是选择排序,非常简单。
#include<stdio.h>
#include<stdlib.h>
//简单选择排序:就地排序,内部排序,不稳定,时间复杂度O(n^2)
int n,a[105];
int main()
{
scanf("%d",&n);
for(int i=1;i<=n;i++)
{
scanf("%d",&a[i]);
}
int minn;
int k;//最小数的下标
for(int i=1;i<=n-1;i++)//排序的趟数,也是乱序区的左边界
{//乱序区i~n中找到最小的数,放到i位置
minn=a[i];
k=i;
for(int j=i+1;j<=n;j++)
{
if(a[j]<minn)
{
minn=a[j];
k=j;
}
}
if(k!=i)
{
a[k]=a[i];
a[i]=minn;
}
}
for(int i=1;i<=n;i++)
{
printf("%d ",a[i]);
}
printf("\n");
}
3.2堆排序
用堆对选择排序进行优化就是堆排序。
那这里就要介绍什么是堆。堆是基于完全二叉树的数据结构。根据其约束条件不同,可分为两种:
大顶堆:根结点的值必须⼤于他的孩⼦结点的值,对于⼆叉树中所有⼦树都满⾜此规律。

既然是数据结构,那么也有一些关于堆的操作:
以小顶堆为例,用数组来存储完全二叉树。这里需要回顾应该性质,就是使用数组存储完全二叉树,树的某个节点下标为i,则其父亲下标为i/2,左孩子下标是i*2,右孩子下标为i*2+1.
(1)自初始化法:直接认为存树的数组就是堆,只是开始不满足小顶堆的约束。我们需要在数组本身进行调整。从最小的子树开始调整,从以a[n/2]为根的子树开始倒着遍历调整每棵子树。对于每棵子树,采取自上而下调整,具体过程如下:从当前子树根节点p开始调整,p去和两个孩子比较,如果孩子比p小,则交换p和最小孩子,然后再把进行交换的孩子做新的调整节点,直到p比两个孩子都小或者p为叶子节点为止。
(2)插入建堆(堆的插入):开始认为是空堆,不断插入数据进行调整:先把数据插入到堆的最后面,此时该数据是叶子节点。向上进行调整,直到调整到根节点或者满足小顶堆约束为止。
(3)堆的删除:每次删除堆顶元素,删除之后把最后一个位置数据放到堆顶,从堆顶向下调整
关于堆排序,一般升序用大顶堆,降序用小顶堆(满足就地排序)但不唯一。关于使用小顶堆实现升序(基于堆的删除,是非就地排序),具体过程如下:
1.先根据待排序数据建立小顶堆,堆顶就是最小的
2.循环n次:输出堆顶(堆顶放到有序数组中),执行堆的删除操作
当然也可以用小顶堆实现降序(基于交换,就地排序)
#include<stdio.h>
#include<stdlib.h>
//堆排序:就地排序(不一定,看如何实现,一般采取就地的),内部排序,不稳定,时间复杂度O(nlogn)
void DownAdjust(int a[],int i,int n)
{//向下调整
int now=i;//正在调整的节点
int next;//下一个要调整的节点
int t;
//now是next的父亲
while(now*2<=n)//now*2存在,即now的左孩子存在
{
next=now*2;//next是now的左孩子
if(next+1<=n&&a[next+1]<a[next])
{
next=next+1;//如果右孩子存在并且值比左孩子小,next执行右孩子
}
if(a[next]<a[now])
{//父亲比最小的孩子小,不满足小顶堆
t=a[next];
a[next]=a[now];
a[now]=t;
now=next;//交换之后,孩子成为新的要被调整的节点
}
else
{
break;
}
}
}
//插入建堆,也就是堆的插入
//向上调整
void h_Adjust(int a[],int i)
{
int now=i;//目前要调整的节点
int next;//下一个要调整的节点
int t;
//next是now的父亲
while(now/2>=1)
{
next=now/2;//next是now的父亲
if(a[now]<a[next])
{
t=a[next];
a[next]=a[now];
a[now]=t;
now=next;
}
else
{
break;
}
}
}
//删除堆顶
void delet_Adjust(int a[],int n)
{
// printf("%d\n",a[1]);
a[1]=a[n];
n--;
DownAdjust(a,1,n);
}
int main()
{
int n,a[105],ans[105],k=0;
scanf("%d",&n);
int size=n;//堆中实际元素的个数
for(int i=1;i<=n;i++)
{
scanf("%d",&a[i]);//边读入边插入
h_Adjust(a,i);
}
//自初始化建堆:从最后一个叶子节点父亲为跟的子树开始调整
// for(int i=n/2;i>=1;i--)
// {
// DownAdjust(a,i,n);
// }
// delet_Adjust(a,size);
// size--;
int t;
for(int i=1;i<=n;i++)
{
//升序
// ans[++k]=a[1];
// delet_Adjust(a,size);
// size--;
//小顶堆实现降序
t=a[size];
a[size]=a[1];
a[1]=t;
size--;
DownAdjust(a,1,size);
}
for(int i=1;i<=n;i++)
{
// printf("%d ",ans[i]);
printf("%d ",a[i]);
}
printf("\n");
}
//测试样例
9
53 17 78 9 45 65 87 32 8
最后总结一下堆的常见应用,包括堆排序,优先队列,图的算法优化(如Dijkstra)
四.归并排序(2-路归并)
不断将序列分成两半,直到不能分割为止,每个序列只有一个数据是就停止,在不断的将两个序列合并成一个有序的序列,在合并的过程中进行排序。合并的具体过程:一个序列,从中间位置分开,如果其前后两部分分别有序(和最终结果的顺序一样),只需要将前后两部分重新合并为整体有序即可。这就是归并排序。
#include<stdio.h>
#include<stdlib.h>
//归并排序:非就地排序,2-路归并内部排序,稳定,时间复杂度O(nlogn)
void Merg(int a[],int l,int mid,int r)
{
//合并a数组的[l,mid] [mid+1,r]
int i=l;//前一半的第一个位置
int j=mid+1;//后一半的第一个位置
int t[105];
int k=0;
while(i<=mid&&j<=r)
{
if(a[i]<=a[j])
{
t[k++]=a[i];//从t[0]开始存数
i++;
}
else
{
t[k++]=a[j];
j++;
}
}
//放剩下的
while(i<=mid)
{
t[k]=a[i];
k++;
i++;
}
while(j<=r)
{
t[k]=a[j];
k++;
j++;
}
for(int i=0;i<k;i++)
{
a[l+i]=t[i];
}
}
void MergSort(int a[],int l,int r)
{
if(l<r)//如果[l,r]至少有两个数就进行排序
{
int mid=(l+r)/2;
//[l,mid] [mid+1,r]
MergSort(a,l,mid);//先对前一部分排序
MergSort(a,mid+1,r);//再对后一部分进行排序
Merg(a,l,mid,r);//合并前后两部分
}
}
int main()
{
int n,a[105];
scanf("%d",&n);
for(int i=1;i<=n;i++)
{
scanf("%d",&a[i]);
}
MergSort(a,1,n);
for(int i=1;i<=n;i++)
{
printf("%d ",a[i]);
}
printf("\n");
}
//测试样例
8
2 1 7 6 8 4 3 5
五.统计排序
5.1 计数排序
n个非负整数,统计每个数的个数和这n个数的最大值。就是基数排序。这种排序是线性,但其实就是以空间换时间的排序,会造成极大的空间浪费,并且对小数和负数进行排序必须转为正整数。小数乘相同数量积,负数找到最小的负数,每个数都加其相反数。
#include<stdio.h>
#include<stdlib.h>
//计数排序:非就地排序 O(n+maxx)(取决于数组),内部排序,稳定性无法确定(需要使用一个前缀和数组实现优化),时间复杂度O(n+maxx)
int n,a[1005],t[1005],count[1005],sumcount[1005];
int main()
{
scanf("%d",&n);
int maxx=0;
for(int i=1;i<=n;i++)
{
scanf("%d",&a[i]);
count[a[i]]++;
if(a[i]>maxx) maxx=a[i];
}
//使用前缀和数组实现稳定
sumcount[0]=count[0];
for(int i=1;i<=maxx;i++)
{
sumcount[i]=sumcount[i-1]+count[i];
}
// for(int i=0;i<=maxx;i++)
// {
// int s=count[i];
// for(int j=1;j<=s;j++)
// {
// printf("%d ",i);
// }
// }
int k;
for(int i=n;i>=1;i--)
{
k=sumcount[a[i]];
t[k]=a[i];
sumcount[a[i]]--;
}
for(int i=1;i<=n;i++)
{
printf("%d ",t[i]);
}
return 0;
}
5.2 基数排序
在计数排序的基础上,通过对数位分别排序(从低位到高位)从而得到有序序列。具体过程详见代码,这里主要分析一下基数排序的时间复杂度。
#include<stdio.h>
#include<stdlib.h>
//基数排序:非就地排序 O(n+maxx)(取决于数组),内部排序,稳定性无法确定(需要使用一个前缀和数组实现优化),时间复杂度O(d*(n+b))
int n,a[1005],t[1005],count[1005],sumcount[1005];
void countSort(int d)
{
for(int i=1;i<=n;i++)
{
count[(a[i]/d)%10]++;
}
//使用前缀和数组实现稳定
sumcount[0]=count[0];
for(int i=1;i<10;i++)
{
sumcount[i]=sumcount[i-1]+count[i];
}
int k;
for(int i=n;i>=1;i--)
{
k=sumcount[(a[i]/d)%10];
t[k]=a[i];
sumcount[(a[i]/d)%10]--;
}
}
int main()
{
scanf("%d",&n);
int maxx=0;
for(int i=1;i<=n;i++)
{
scanf("%d",&a[i]);
if(a[i]>maxx) maxx=a[i];
}
for(int i=1;maxx/i>0;i*=10)
{
countSort(i);
}
for(int i=1;i<=n;i++)
{
printf("%d ",t[i]);
}
return 0;
}
//测试样例
9
1 4 1 2 5 2 4 1 8
5.3桶排序
桶排序重要的是它的思想,⽽不是具体实现,桶排序从字⾯的意思上看:
1740

被折叠的 条评论
为什么被折叠?



