目录
引言:本博客将逐个分析插入排序,冒泡排序,归并排序,堆排序,快速排序,计数排序,基数排序等常见排序算法
二.时间复杂度为n*logn的算法-归并排序,堆排序,快速排序
引言:本博客将逐个分析插入排序,冒泡排序,归并排序,堆排序,快速排序,计数排序,基数排序等常见排序算法
(首先排序的意思是是将一组"无序"的记录序列调整为"有序"的记录序列,接下来的算法统一以完成升序为目标)
一.时间复杂度为n*n的算法-插入排序与冒泡排序
1.插入排序
(1).什么是插入排序?
扑克牌斗地主什么的玩过吧,多数人都是这么去整理手牌的:最开始手上有0张牌,此时可以看作已经是有序的,接下来每发给我一张牌,我就把这张牌插入到我已有牌中的正确位置上,这样我手上的牌就一直是有序的,最后也是有序的.
(2).插入排序实现思路
我们的目标是让手中的牌升序排列,因为手上已有的牌已经是升序了,所以对于新来的牌x,我们只需要将它不断的与未比较的牌里最大的那张y比较,比y大或等于则x牌就该插在这儿,比y小则y牌后移,x继续与未比较的牌中最大的比较,因为已经是升序故而就是y牌前一张,
(3).模版代码
void insert_sort(int* a,int len){ //排序[begin,end)内的数字为升序
for(int i=1;i<len;i++){
int key=a[i];
int j=i-1;
while(j>=0&&a[j]>key){ //大于key的全部后移一位
a[j+1]=a[j];
j--;
}
a[j+1]=key; //插入到该位置
}
}
2.冒泡排序
(1).什么是冒泡排序
用来排序的一个n*n时间复杂度算法
(2).冒泡排序实现思路
实现思路正如它的名字,冒泡,不断的比较相邻的两个数字,将二者中较小的那个放到前面,这样子的比较会做大约n*n/2次,故而时间复杂度是n*n
(3).模版代码
void bubble_sort(int* a,int len){ //排序指针a后len个数字为升序
for(int i=0;i<len-1;i++){
for(int j=len-1;j>i;j--){ //将小的数字往前冒
if(a[j]<a[j-1]){
int t=a[j-1];
a[j-1]=a[j];
a[j]=t;
}
}
}
}
二.时间复杂度为n*logn的算法-归并排序,堆排序,快速排序
1.归并排序
(1).什么是归并排序?
利用归并的思想,将当前无序的n个数字不断递归二分,直到区间大小为一时,此时自然已经是有序的了,然后递归回溯,将两个已经有序的区间归并.最终整个区间都有序化.
(2).归并排序实现思路
使用递归来实现,两个函数,一个函数名为merge_sort,如模版代码中所写,思维很简单,l<r时也就是r-l>0时也就是区间内至少有两个元素时,将区间二分,递归继续进行merge_sort,到最深处返回回来后,对这两个子区间执行merge函数,merge函数就是用于将两个区间合并,由于两个区间都已经是有序的,此时我们进行排序就可以建立两个指针pos1,pos2,分别分别指向两区间的开始处,然后不断的将两指针之间较小的那个放进一个新的temp数组然后后移此指针.最后将temp数组覆盖到对应区间的a数组中,即可完成合并.
(3).模版代码
void merge(int *a,int l,int r,int mid){
int temp[r-l],tempnum=0;
int pos1=l,pos2=mid+1;
while(pos1<=mid&&pos2<=r){
if(a[pos1]<=a[pos2]) temp[tempnum++]=a[pos1++];
else temp[tempnum++]=a[pos2++];
}
while(pos1<=mid) temp[tempnum++]=a[pos1++];
while(pos2<=r) temp[tempnum++]=a[pos2++];
for(int i=0;i<tempnum;i++) a[l+i]=temp[i];
}
void merge_sort(int *a,int l,int r){ //排序a数组中[l,r]区间内的数为升序
if(l<r){
int mid=(l+r)/2;
merge_sort(a,l,mid);
merge_sort(a,mid+1,r);
merge(a,l,r,mid);
}
}
2.堆排序
(1).什么是堆排序
一种n*logn的排序算法,利用堆这种数据结构对数据的高效管理特性.
(2).实现思路
建立一个小根堆,将数字依次压进堆中,再依次取出,取出时就是有序的了,这里使用数组原地建堆
(3).模版代码
#include <stdio.h>
int myswap(int *a,int *b){
int temp=*a;
*a=*b;
*b=temp;
}
//升序
void heap_sort(int *a,int l,int r){
//原数组上建堆
for(int i=r-(r-l)/2;i>=l;--i){
int now=i,t=a[i];
do{
int pa=(now-l-1)/2+l;
if(a[pa]>=t) break;
a[now]=a[pa];
now=pa;
}while(now-l);
a[now]=t;
}
//不断拿出最大值放到后面
for(int i=r;i>l;i--){
//交换堆首宇堆尾
myswap(a+i,a+l);
//调整堆首
int largest=l;
while(1){
int temp=largest,lchild=(largest-l)*2+1+l,rchild=(largest-l)*2+2+l;
if(lchild<i&&a[lchild]>a[largest]) largest=lchild;
if(rchild<i&&a[rchild]>a[largest]) largest=rchild;
if(temp==largest) break;
myswap(a+temp,a+largest);
}
}
}
const int maxn=1005;
int a[maxn];
int main(){
int t;
scanf("%d",&t);
for(int i=0;i<t;i++){
int n;
scanf("%d",&n);
for(int j=0;j<n;j++) scanf("%d",&a[j]);
heap_sort(a,0,n-1);
for(int j=0;j<n-1;j++) printf("%d ",a[j]);
printf("%d\n",a[n-1]);
}
return 0;
}
3.快速排序
(1).什么是快速排序?
一个最常见的用于排序的n*logn算法,c++中qsort底层就是快速排序.
(2).实现思路
在n个未排序数字中选取一个数作为"基数",我选择左边界l上的数,令l=左边界,r=右边界,r向左找小于基数的数字,l向右找大于l的数然后找到的这两个数字对换,循环进行这个过程,最后 r>l 时自然n个数就都被处理完了,left左边的全部小于等于基数,右边的全部大于等于基数,然后再对左边和右边分别快排.
(3).模版代码
void quick_sort(int *a,int l,int r){
if(l>=r) return;
int key=a[l],left=l,right=r;
while(left<right){
while(a[right]>=key&&left<right) right--;
while(a[left]<=key&&left<right) left++;
if(left<right){
int t=a[left];
a[left]=a[right];
a[right]=t;
}
}
a[l]=a[left];
a[left]=key;
quick_sort(a,l,left-1);
quick_sort(a,left+1,r);
}
或者
#include <algorithm>
sort(a,a+n);
三.针对特殊输入时间复杂度为n的算法-计数排序,基数排序
1.计数排序(输入数据区间小适用)
(1).什么是计数排序
这是一种针对特殊输入理论上可以达到n时间复杂度的算法.这个特殊输入就是待排序数字皆在一个大小为k的数值区间内,而这个k越小,计数排序越高效,因为实际上计数排序的时间复杂度为O(n+k),不过由于说好了是针对特殊输入的故而k因较小而被忽略.
(2).计数排序实现思路
计数排序的思路正如其名-计数,计算每个数字的出现次数,因为我们知道所有这n个待排序数字都是在某一区间上的,比如[l,r],故而我们可以定义这么一个数组count,大小正好为r-l+1,最开始全部赋值为0,这样我们就可以把输入中每一个可能出现的数字对应到数组中对应的一位,就像输入一个3时我就把count[3]+1,这样最后每一个count[i]的值就代表着 i 在这n个数字中出现过几次.
光说肯定不好理解,现在我们来看看对于最简单的一组数字 3,2,3,1,5,2 ,我事先告诉你数字最大为6,故而最开始count数组如下
count 0 1 2 3 4 5 6
0 1 2 2 0 1 0 (就是每个i在这6个数字中的出现次数对吧)
最后一步自然就是读取count数组构建排序结果了,如代码,以一个pos即时保存当前排序构建到哪儿了,从0到k读count数组,读到count[i]不为0就往排序结果中加count[i]个i,然后pos也加count[i],继续往后读.
什么的是计数排序最简单的思路,我实现的模版代码做了一点优化,支持自定义输入数据区间.
(3).模版代码
const int tmin=0; //输入数据最小界
const int tmax=1000001; //输入数据最大界
const int k=tmax-tmin+1;//区间大小
int count[k]; //这里count[i]存的是输入数据中i+tmin出现的次数
void jishu_sort(int *a,int l,int r){
for(int i=0;i<k;i++) count[i]=0;
for(int i=l;i<=r;i++) count[a[i]-tmin]++;
int pos=0;
for(int i=0;i<k;i++){
if(count[i]){
for(int j=0;j<count[i];j++) a[pos+j]=i+tmin;
pos+=count[i];
}
}
}
2.基数排序(输入数据大时更加合适)
(1).什么是基数排序?
我们都知道每个数字都是由个位百位千位构成,故而赫尔曼·何乐礼思考出这么一种排序算法,以每一位为基数做排序,
(2).基数排序实现思路
其实基数排序有两种实现方式,最高位优先法与最低位优先法,简称MSD法和LSD法主体思维相差不大故而下面我以MSD作实现.)
我们可以先通过最大位数将这n个数进行排序,每位数就是关键码,关键码的取值范围只会是0,1,2,3...9这是个数,故而我们可用计数排序以n的时间复杂度将之排序,然后对关键码为0,为1,为2...的分别进行更小数位上的基数排序,最终个位也排完时基数排序结束,排序完成.
这个算法设待排序列为n个记录,d个关键码,关键码的取值范围为radix,则进行链式基数排序的时间复杂度为O(d(n+radix)),
(3).模版代码
#include <stdio.h>
const int k=10;//关键码区间大小
void radix_sort(int* a,int l,int r,int key){ //排序a数组区间[l,r)内的数字
if(key<1||r-l+1<2) return;
int count[k]; //这里count[i]存的是输入数据中i出现的次数
for(int i=0;i<k;i++) count[i]=0;
for(int i=l;i<=r;i++) count[a[i]/key%10]++;
for(int i=1;i<k;i++) count[i]+=count[i-1];
int tcount[k]; //count复制数组
for(int i=0;i<k;i++) tcount[i]=count[i];
int temp[r-l]; //暂存排序结果
for(int i=l;i<=r;i++) temp[--tcount[a[i]/key%10]]=a[i]; //一种计数排序的思维
for(int i=0;i<r-l+1;i++) a[l+i]=temp[i]; //上传排序结果到a
//对关键码0-9分出的区间分别进行基数排序
radix_sort(a,0,count[0]-1,key/10);
for(int i=1;i<k;i++) radix_sort(a,count[i-1],count[i]-1,key/10);
}
int main(){
int a[10]={13,55,25,36,66,38,51,29,114,157};
int key=1;
for(int i=0;i<10;i++) while(key<a[i]) key*=10;
radix_sort(a,0,9,key/10);
for(int i=0;i<10;i++) printf("%d ",a[i]);
printf("\n");
return 0;
}