一、排序概念
排序称为分类,就是把一批任意序列的数据记录,按关键字重新排成一个有序的序列。
排序一般又分为以下几种:
(1)稳定排序和不稳定排序
稳定排序:两个相等的数排序前的位置顺序与排序后位置的顺序不变。比如排序前:5,2,5。排序后:2,5,5。
不稳定排序:两个相等的数排序前的位置顺序与排序后位置的顺序颠倒。比如5,8,5,2,9。选择排序中:第一个5先跟2发生了交换。
(2)根据存储设备的不同,分为内部排序和外部排序
内部排序:数据全部存放在计算机的内存储器中进行的排序过程,在此期间没有进行内、外存储器的数据对象。
外部排序:指待排序记录的数量很大,以致内存不能依次容纳全部记录,所以排序的过程中,数据的主要部分存在外存储器上,借助与内存储器逐步调整记录之间的相对位置。在这个过程中,需要不断地在内、外存储器之间进行数据地交换。
稳定排序:直接插入排序、基数排序、归并排序、冒泡排序…
不稳定排序:快速排序、希尔排序、简单选择排序、堆排序…
二、排序的分类
1、直接插入排序:
基本思想:每一个待排序的记录按其排序码关键字的大小插到前面已经排好序的序列。直接插入过程可以理解为:不断建立监察哨,不断更新监察哨的位置
假设一组关键字序列为{48,35,18,45,12,68,33},一共7个记录
其上就是监察哨地位置变化结果。不断与监察范围的元素比较,范围内的数比监察哨大,就交换。
核心代码实现:
void insertsort(int a[],int n){///从小到大排序
for(int i=2;i<=n;i++){
a[0]=a[i];///监察哨
int j=i-1;///监察范围最大值
while(a[0]<a[j]){
a[j+1]=a[j];
j--;
}
a[j+1]=a[0];
}
}///时间复杂度:O(n的平方/2)
2、折半插入排序(二分插入排序):
基本思想:类似二分查找,先取一个序列的中间关键字与当前关健字比较,如果相等则查找成功,否则就改变查找区间。但是排序通过查找元素的插入。注意:运用此算法的前提就是一个有序的序列,所以在直接插入排序的基础上再插入。
核心代码实现:
void insertsort(int a[],int n){///从小到大排序
int low,high;
for(int i=2;i<=n;i++){
a[0]=a[i];///暂存a[i]
low=1;
high=i-1;
while(low<=high){
int mid=(low+high)/2;
if(a[0]<a[mid]) 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];
}
}
3、冒泡排序
基本思想:依次比较两个相邻的元素,如果(从小到大或从大到小)顺序相反就把它们交换。冒泡排序可以理解为,不断交换找最大数(冒泡)、缩小范围继续找次最小,直到所有元素排序完毕。
比如一个无序序列:45 15 68 2.其冒泡排序的简易图如下:
核心代码实现:
void bubblesort(int a[],int n){
for(int i=1;i<=n-1;i++)///不用遍历到n,因为j+1已经访问了
for(int j=1;j<=n-i;j++)///缩小范围
if(a[j+1]<a[j]){
int temp=a[j];
a[j]=a[j+1];
a[j+1]=temp;
}
}///时间复杂度O(n的平方),
4、简单选择排序
基本思想:不断地找出序列中最小的数,然后与当前比较的数进行交换
核心代码实现:
void selectsort(int a[],int n){
for(int i=1;i<=n;i++)
{
int t=i;
for(int j=i+1;j<=n;j++)///找出序列中最小的数
if(a[t]>a[j]) t=j;
int temp=a[i];
a[i]=a[t];
a[t]=temp;
}
}
5、希尔排序
希尔排序又称为缩小增量排序,先将待排序的记录分割成为若干子序列分别进行直接插入排序。
下面是一个序列为(83、72、47、36、87、30、12、67)希尔排序的实现过程:
画的再多还是需要动动脑,根据代码,动笔来捋顺一遍这几幅图?希望以上图对大家的理解有所帮助。
核心代码实现
void shellsort(int a[],int n){///我们不妨回看以下直接插入排序,你会发现相似之处。
int d=n;
d=d/2;
while(d>0){
for(int i=d+1;i<=n;i++){
a[0]=a[i];///暂时记录下来
int j=i-d;
while(j>=0&&a[0]<a[j]){
a[j+d]=a[j];
j=j-d;
}
a[j+d]=a[0];
}
d=d/2;///缩小增量
}
}
6、快速排序
基本思想:以第一个数为基本标准,从左往右找比基准数大的数,从右到左找比基准数小的数,然后两数交换。如果i==j,就找到了基准数要交换的位置。
核心代码实现:
void quicksort(int a[],int left,int right){
if(left>=right) return;
int i=left;
int j=right;
int x = a[left];///基准数
while(i!=j){
while(i<j&&x<=a[j]) j--;///从右往左
while(i<j&&x>=a[i]) i++;///从左往右
if(i<j){///交换两数
int t=a[i];
a[i]=a[j];
a[j]=t;
}
}
a[left]=a[i];///交换基准数
a[i]=x;
quicksort(a,left,i-1);
quicksort(a,i+1,right);
}
7、堆排序
基本思想:将待排序的记录序列构造一个堆(大顶堆或小顶堆),此时,选出堆中最小者或者最大者,然后将它从堆中移走(将它与堆数组的末尾元素交换,此时末尾元素就是最大值或最小值),并将剩余的记录再调整成堆,这样又找出了次小(或次大)的记录,以此类推,直到堆中只有一个记录为止。注意:每一个出堆的顺序就是一个有序序列。
最大堆:每一个结点大于等于它的左右孩子结点,用于升序序列(找出最大值)
最小堆:每一个结点小于等于它的左右孩子结点,用于降序序列(找出最小值)
假设一个序列为{16,20,32,28,46,35},此序列构造成小顶堆或者大顶堆如下:
以最大堆进行升序排序的基本思想:
①初始化堆:将数列a[0…n-1]构造成最大堆。
②交换数据:将a[0]和a[n-1]交换,使a[n-1]是a[0…n-1]中的最大值,然后将a[0…n-2]重新调整为最大堆。以此类推,直到整个数列都是有序的。
假设有下面的一个序列:
其堆排序的执行过程如下:
以此类推…直到整个数列有序。
核心代码实现:
int swap(int *a,int *b){
int temp=*b;
*b=*a;
*a=temp;
}
void maxheap_down(int a[],int start,int end){///大顶堆向下调整算法
int i=start;
int L=2*i+1;///R==L+1=2*i+2;
int temp=a[i];
for(;L<=end;i=L,L=2*i+1){
if(L<end&&a[L]<a[L+1]) L++;
if(temp>=a[L]) break;
else {
a[i]=a[L];
a[L]=temp;
}
}
}
void heapsort(int a[],int n){
for(int i=n/2-1;i>=0;i--) maxheap_down(a,i,n-1);///初始化堆
for(int i=n-1;i>0;i--) {
swap(&a[0],&a[i]);///交换数据,使数组末尾元素是整个数列最大的。
maxheap_down(a,0,i-1);///依次类推0~i-1范围内的最大值
}
}
8、二路归并排序
基本思想:将一个具有n个待排序记录的序列记录的序列看成是n个长度为1的有序列,然后两两归并,得到一个长度为2的有序序列;若n为奇数,则最后一个关键字不参与归并,直接进入下一趟归并。如此重复,直到一个长度为n的有序序列为止。
核心代码实现:
const int maxn=1010;
///将数组a的[L1,R1]与[L2,R2]区间合并为有序区间
void Merge(int a[],int L1,int R1,int L2,int R2){
int i=L1;
int j=L2;
int t[maxn],k=0;///数组t临时存放合并后的数组,如果点比较大我们可以定义为*t,申请空间
while(i<=R1&&j<=R2){
if(a[i]<=a[j]) t[k]=a[i++];
else t[k]=a[j++];
k++;
}
while(i<=R1) t[k++]=a[i++];///将[L1,R1]的剩余元素加入序列temp
while(j<=R2) t[k++]=a[j++];///将[L2.R2]的剩余元素加入序列temp
for(i=0;i<k;i++)
a[L1+i]=t[i];///将合并后的序列赋值回数组a
}
void mergesort(int a[],int left,int right){///递归实现
if(left<right){
int mid=(left+right)/2;
mergesort(a,left,mid);
mergesort(a,mid+1,right);
Merge(a,left,mid,mid+1,right);
}
}
void mergesort(int *a,int n){///非递归实现
for(int step=2;step/2<=n;step*=2)///step为组内元素。step/2为左子区间元素个数
for(int i=0;i<n;i+=step){
int mid=i+step/2-1;
///左子区间[i,mid],右子区间为[mid+1,mid(i+step-1,n-1)],mid(i+step-1,n-1):当i等于n-1
if(mid+1<n) Merge(a,i,mid,mid+1,min(i+step-1,n-1),n);///右子区间存在元素则合并
}
}
9、桶排序
基本思想:把待排序的序列的每个数用一个数组记录下来,数组的下标就是每个数的值,然后统计每个数出现的个数,然后遍历。
假设一个序列:5 3 5 2 8
核心算法实现:
///从大到小排序
int book[1001];///数组大小看题目需求
for(int i=0;i<=1000;i++) ///一个待排序的序列每个元素的值的范围
book[i]=0;
for(int i=1;i<=n;i++){
scanf("%d",&t);
book[t]++;///记录当前的元素t有几个
}
for(int i=1000;i>=0;i--)
for(int j=1;j<=book[i];j++) ///元素个数
printf("%d ",i);
///时间复杂度为O(2*(m+n))
10、基数排序
基本思想:将所有待排序的数统一为相同得数位长度,数位较短得数前面补0;从最地位开始,依次进行排序;从最低位排序一直到最高位排序完成以后,数列就变成一个有序序列
一个含有10个数的待排序的序列为{236,63,970,852,109,621,8,63,505,189}。它的基数排序过程如下:(从左到右)
初始状态–> | 按个位排序–> | 按十位排序–> | 按百位排序 |
---|---|---|---|
236 | 970 | 505 | 008 |
063 | 621 | 008 | 063 |
970 | 852 | 109 | 083 |
852 | 063 | 621 | 109 |
109 | 083 | 236 | 189 |
621 | 505 | 852 | 236 |
008 | 236 | 063 | 505 |
083 | 008 | 970 | 621 |
505 | 109 | 083 | 852 |
189 | 189 | 189 | 970 |
核心算法实现:
int maxbit(int a[],int n){///求数据的最大位数
int d=1;///保存最大位数
int p=10;
for(int i=0;i<n;i++)
while(a[i]>=p){
p*=10;
++d;
}
}
void radixsort(int a[],int n){
int d=maxbit(a,n);
int temp[n];
int count[10];///计数
int k;
int radix=1;
for(int i=1;i<=d;i++){///进行d次排序
for(int j=0;j<10;j++) count[j]=0;///初始化
for(int j=0;j<n;j++) {
k=(a[j]/radix)%10;///取位
count[k]++;
}
for(int j=1;j<10;j++) count[j]=count[j-1]+count[j];
///把temp的位置分给每个桶
for(int j=n-1;j>=0;j--) {
k=(a[j]/radix)%10;///取位
temp[count[k]-1]=a[j];
count[k]--;
}
for(int j=0;j<n;j++) a[j]=temp[j];///把临时数组的内容复制到a中
radix*=10;
}
}
11、计数排序
计数排序是基于非比较的排序算法。核心在于将输入的数据值转化为键存储在额外开辟的数组空间中。
基本步骤:
①扫描整个序列,获取最小值min和最大值max
②开辟新空间的数组count,长度为max-min+1,它是记录的值是序列中某元素出现的次数
核心算法实现:
void Countsort(int a[],int n){
int max=a[0];
int min=a[0];
for(int i=1;i<n;i++){
if(a[i]>max) max=a[i];
if(a[i]<min) min=a[i];
}
int m=max-min+1;///获得申请空间大小
int *count=(int *)malloc(sizeof(int)*m);
memset(count,0,sizeof(int)*m);///初始化
for(int i=0;i<n;i++)count[a[i]-min]++;///统计每一个元素的个数
for(int i=1;i<m;i++) count[i]+=count[i-1];///确定元素位置
int *t=(int *)malloc(sizeof(int)*n);
for(int i=n-1;i>=0;i--){
count[a[i]-min]--;
t[count[a[i]-min]]=a[i];
}
for(int i=0;i<n;i++) a[i]=t[i];
free(count);
free(t);
}
三、各排序的时间复杂度分析
排序算法 | 平均复杂度 | 最好情况 | 最坏情况 | 空间复杂度 | 稳定性 |
---|---|---|---|---|---|
冒泡排序 | O(n*n) | O(n) | O(n*n) | O(1) | 稳定 |
选择排序 | O(n*n) | O(n*n) | O(n*n) | O(1) | 不稳定 |
插入排序 | O(n*n) | O(n) | O(n*n) | O(1) | 稳定 |
希尔排序 | O(n*log n) | O(n*log2 n) | O(n*log2 n) | O(1) | 不稳定 |
归并排序 | O(n*log n) | O(n*log n) | O(n*log n) | O(n) | 稳定 |
快速排序 | O(n*log n) | O(n*log n) | O(n*n) | O(log n) | 不稳定 |
堆排序 | O(n*log n) | O(n*log n) | O(n*log n) | O(1) | 不稳定 |
计数排序 | O(n+k) | O(n+k) | O(n+k) | O(k) | 稳定 |
桶排序 | O(n+k) | O(n+k) | O(n*n) | O(n+k) | 稳定 |
基数排序 | O(n*k) | O(n*k) | O(n*k) | O(n+k) | 稳定 |
四、参考资料
1、《算法笔记》
2、《啊哈算法》
3、《图论及其应用》
4、公众号五分钟学算法
5、《大话数据结构》