★★★★★排序
当数据量非常大时,排序的效率就非常重要;所以以下学习的排序的数据起步量都是10000个以上
各种排序的统一函数名规范:
void X_Sort( ElementType A[],int N );
排序名称 待排元素 数组大小
放在数组里
元素类型包括所有能排大小的数据结构,比如数字,字符串等等,都可以用排序算法;
.默认为将整数从小到大排序:为了简单起见,所有排序算法的理解和代码都是以整数为例;
.输入的N为正整数;
.只讨论基于比较的排序(> = < 有定义):对于要排序的数据类型,有明确的大小相等的定义;
.只讨论内部排序:假设内存空间充足,所有数据能一次性的导入内存空间;排序过程在内存中完成;
外部排序:数据量大于内存,需要分若干次数据的导入导出才能完成的排序;
.稳定性:任意两个相等的数据,排序前后的相对位置不发生改变;
.没有一种排序时任何情况下都表现最好的:每一种算法都有它存在的理由
当数据具有某种特征的时候,某一种算法可能是最好的
测试用例说明:
给定N个(长整型范围内的)整数,要求输出从小到大排序后的结果。
本题旨在测试各种不同的排序算法在各种数据情况下的表现。各组测试数据特点如下:
- 数据0:只有1个元素;
- 数据1:11个不相同的整数,测试基本正确性;
- 数据2:10^3个随机整数;
- 数据3:10^4个随机整数;
- 数据4:10^5个随机整数;
- 数据5:10^5个顺序整数;
- 数据6:10^5个逆序整数;
- 数据7:10^5个基本有序的整数;
- 数据8:10^5个随机正整数,每个数字不超过1000。
输入格式:
输入第一行给出正整数N(≤105),随后一行给出N个(长整型范围内的)整数,其间以空格分隔。
输出格式:
在一行中输出从小到大排序后的结果,数字间以1个空格分隔,行末不得有多余空格。
输入样例:
11 4 981 10 -17 0 -20 29 50 8 43 -5
输出样例:
-20 -17 -5 0 4 8 10 29 43 50 981
代码长度限制 16 KB
时间限制 10000 ms
内存限制 64 MB
※简单排序:
▶♥ 冒泡排序
20 把数据想象成泡泡,大数据=>大泡泡,小数据=>小泡泡;
12 不断地做从上到下扫描,每当看到更大的大泡泡时,就把它往下压;
39 每次从上到下扫描一遍:如果A[i] > A[i+1];Swap(A[i],A[i+1]);
9 所以每一次扫描就是只将最大的泡泡交换到最底下
60 当扫描一遍发现已经都排好序了,即没有发生Swap,那就跳出循环
8
42 void Bubble_Sort( ElementType A[],int N )
3 {
28 int i,Tag = 1; //存在需要排序的情况
while(Tag){
Tag = 0; //假设已经都排好了
for(i=0;i<N;i++){
if(A[i] > A[i+1]){
Swap( &A[i],&A[i+1] );
Tag = 1; //有Swap发生,还需要扫描
}
}
}
}
代码优化:由于每一次扫描一定是将最大的泡泡压到了最底下,所以下一次扫描时,只需扫描到倒数第二的位置就可以啦
以此类推,每扫描一遍,下次需要扫描的长度就-1;
void Bubble_Sort( ElementType A[],int N ) void Bubble_Sort( ElementType A[],int N )
{ {
int i,Tag = 1; int i,P,flag;
while(Tag){ for(P=N-1;P>0;P--){
Tag = 0; flag = 0;
for(i=0;i<N-1;i++){ for(i=0;i<P;i++){
if(A[i] > A[i+1]){ if(A[i] > A[i+1]){
Swap( &A[i],&A[i+1] ); Swap( &A[i],&A[i+1]);
Tag = 1; flag = 1;
} }
} }
if(flag == 0){
N--; break;
} }
} }
}
时间复杂度:最好情况 T=O(N)
最坏情况 T=O(N^2)
冒泡排序的缺点:时间复杂度达到O(N^2)
优点:①如果待排元素放在链表里,同样适用
②具有稳定性
.
测试结果:
代码:
#include <stdio.h>
void Swap( int* a,int* b );
void Print( int A[],int N );
void Bubble_Sort( int A[],int N );
int main()
{
int N;
scanf("%d",&N);
int i,A[N];
for(i=0;i<N;i++){
scanf("%d",&A[i]);
}
Bubble_Sort( A,N );
Print( A,N );
return 0;
}
void Bubble_Sort( int A[],int N )
{
int i,P,IsAllInOrder;
for(P=N-1;P>0;P--){
IsAllInOrder = 1;
for(i=0;i<P;i++){
if(A[i]>A[i+1]){
Swap( &A[i],&A[i+1] );
IsAllInOrder = 0;
}
}
if(IsAllInOrder){
break;
}
}
}
void Swap( int* a,int* b )
{
int Temp = *a;
*a = *b;
*b = Temp;
}
void Print( int A[],int N )
{
int i;
for(i=0;i<N-1;i++){
printf("%d ",A[i]);
}
printf("%d",A[N-1]);
}
▶♥ 插入排序
将排序的过程模拟为抓牌的过程,
抓第一张,放在A[0],不动
抓第二张 和前面的比较 如果前面的比较大 那就后移一位 即A[1] = A[0];
抓第三张 和第二张比较 如果第二张比较大 那就后移一位 即A[2] = A[1];再和第一张比较 第一张大 那就A[1] = A[0];然后A[0] = 这张牌
... ... 手里这张牌大 那就A[1] = 这张牌
如此循环 直到抓完最后一张牌
抓第一张牌10 放手里 10
抓第二张牌8 对比 8 10
抓第三张牌J 对比 8 10 J
抓第四张牌3 一张张 对比 3 8 10 J
void Insertion_Sort( ElementType A[],int N )
{
for(P=1;P<N;P++){//从第二张牌来时比较 直到最后一张
Temp = A[P]; //抓起来放手里
for(i=P;i>0;i--){//依次和前面的牌比较
if(A[i-1] > Temp){ //如果不符合顺序
A[i] = A[i-1]; //后移一位
}else{ //否则
A[i] = Temp; //新牌落座
break; //不用再比了,跳出循环
}
}
}
}
代码优化:
void Insertion_Sort( ElementType A[],int N )
{
for(P=1;P<N;P++){
Temp = A[P]; /* 摸下一张牌 */
for(i=P;i>0 && A[i-1]>Temp;i--){
A[i] = A[i-1]; /* 移出空位 */
}
A[i] = Temp; /* 新牌落座 */
}
}
时间复杂度:最好情况 T=O(N)
最坏情况 T=O(N^2)
优点:程序简单,步骤少,具有稳定性
.
测试结果:
比冒泡好很多;
代码:
#include <stdio.h>
void Print( int A[],int N );
void Insertion_Sort( int A[],int N );
int main()
{
int N;
scanf("%d",&N);
int i,A[N];
for(i=0;i<N;i++){
scanf("%d",&A[i]);
}
Insertion_Sort( A,N );
Print( A,N );
return 0;
}
void Insertion_Sort( int A[],int N )
{
int P,i,Temp;
for(P=1;P<N;P++){
Temp = A[P];
for(i=P;i>=1 && A[i-1]>Temp;i--){
A[i] = A[i-1];
}
A[i] = Temp;
}
}
void Print( int A[],int N )
{
int i;
for(i=0;i<N-1;i++){
printf("%d ",A[i]);
}
printf("%d",A[N-1]);
}
※排序进阶:
※时间复杂度下界
.对于下标i<j,如果A[i]>A[j],则称(i,j)是一对逆序对(inversion)
.问题:序列{34,8,64,51,32,21}中有多少逆序对?
(34,8) (34,32) (34,21) (64,51) (64,32) (64,21) (51,32) (51,21) (32,21) 9对
简单排序(比如冒泡排序 插入排序 Low选择排序) 每次都只是交换一个逆序对,所以交换的次数都是9次
.交换2个相邻元素正好消去1个逆序对
.插入排序:T(N,I)=O(N+I) I是逆序对的个数
口 如果序列基本有序,则插入排序简单且高效
.定理:任意N个不同元素组成的序列平均具有N(N-1)/4个逆序对
.定理:任何仅以交换相邻两元素来排序的算法,其平均时间复杂度为Ω(N^2) Ω指的是下界
.这意味着:要提高算法效率,我们必须
==>每次消去不止1个逆序对!
==>每次交换相隔较远的2个元素!
.
▶♥ 希尔排序 (by Donald Shell)
利用插入排序的简单,同时克服插入排序每次只交换相邻两个元素的这个缺点
例如: 81 94 11 96 12 35 17 95 28 58 41 75 15
先做5-间隔排序 即(81 35 41) (94 17 75) (11 95 15) (96 28) (12 58)
35 17 11 28 12 41 75 15 96 58 81 94 95
再做3-间隔排序 28 12 11 35 15 41 58 17 94 75 81 96 95
最后1-间隔排序
.定义增量序列Dm>D(m-1)>...>D1=1
.对每个Dk进行"Dk-间隔"排序(k=M,M-1,...,1)
.注意到:更小间隔的排序没有将上一步的结果变坏
"Dk-间隔"有序的序列,在执行"Dk-1-间隔"排序后,仍然是"Dk-间隔有序"的
原始希尔排序 Dm=N/2,Dk=D(K+1)/2
void Shell_Sort( ElementType A[],int N )
{
for(D=N/2;D>0;D/=2){
/* 插入排序 */
for(P=D;P<N;P++){
Temp = A[P];
for(i=P;i>=D && A[i-D]>Temp;i-=D){
A[i] = A[i-D];
}
A[i] = Temp;
}
}
}
希尔排序不具有稳定性
最坏情况:T=Θ(N^2) Θ既是上界又是下界 O是一个上界(有可能达不到) Ω是一个下界
例如: 1 9 2 10 3 11 4 12 5 13 6 14 7 15 8 16
按8-间隔 4-间隔 2-间隔排序后没有任何变化
问题点:增量元素不互质,则小增量可能根本不起作用
更多增量序列:
◎Hibbard增量序列
.Dk = 2^k-1 ————————相邻元素互质
.最坏情况:T=Θ(N^3/2)
.猜想:Tavg = O(N^%/4)
◎Sedgewick增量序列
.{1,5,19,41,109,...} ——————9*4^i-9*2^i+1 或 4^i-3*2^i+1
.猜想:Tavg = O(N^7/6),Tworst = (N^4/3)
希尔排序本质上还是插入排序,只不过先用增量调整了不相邻的逆序对。
Sedgewick增量序列:int Sedgewick[] = {260609,146305,64769,36289,16001,8929,3905,2161,929, 505,209, 109, 41, 19, 5, 1, 0};
即使一个简单的算法,但是关于它的时间复杂度分析可能非常的难
void Shell_Sort( ElementType A[],int N )
{ /* 希尔排序—— 用Sedgewick增量序列 */
int Si,D,P,i;
ElementType Temp;
/* 这里只列出一小部分增量 */
int Sedgewick[] = {929,505,209,109,41,19,5,1,0};
for(Si=0;Swdgewick[Si]>=N;Si++);//初始增量Sedgewick[Si]不能超过待排序列长度
for(D=Sedgewick[Si];D>0;D=Sedgewick[++Si]){
for(P=D;p<N;P++){
Temp = A[P];
for(i=P;i>=D && A[i-D]>Temp;i-=D){
A[i] = A[i-D];
}
A[i] = Temp;
}
}
}
.
总过程:
分解步骤:
第一步:增量为4
第二步:增量为2
第三步:增量为1
测试结果:
1.原始希尔排序:
相比插入排序,提升很大,特别是对逆序序列和随机序列
代码:
#include <stdio.h>
void Print( int A[],int N );
void Shell_Sort_Original( int A[],int N );
void Insertion_Sort( int A[],int N,int D );
int main()
{
int N;
scanf("%d",&N);
int i,A[N];
for(i=0;i<N;i++){
scanf("%d",&A[i]);
}
Shell_Sort_Original( A,N );
Print( A,N );
return 0;
}
void Shell_Sort_Original( int A[],int N )
{
int D;
for(D=N/2;D>0;D/=2){
Insertion_Sort( A,N,D );
}
}
void Insertion_Sort( int A[],int N,int D )
{
int P,i,Temp;
for(P=D;P<N;P++){
Temp = A[P];
for(i=P;i>=D && A[i-D]>Temp;i-=D){
A[i] = A[i-D];
}
A[i] = Temp;
}
}
void Print( int A[],int N )
{
int i;
for(i=0;i<N-1;i++){
printf("%d ",A[i]);
}
printf("%d",A[N-1]);
}
2.Hibbard增量——希尔排序:
速度相比与原始的希尔排序提升了一点,主要是为了避免原始希尔排序遇到的特殊尴尬情况
代码:
#include <stdio.h>
#include <math.h>
void Print( int A[],int N );
void Shell_Sort_Hibbard( int A[],int N );
void Insertion_Sort( int A[],int N,int D );
int main()
{
int N;
scanf("%d",&N);
int i,A[N];
for(i=0;i<N;i++){
scanf("%d",&A[i]);
}
Shell_Sort_Hibbard( A,N );
Print( A,N );
return 0;
}
void Shell_Sort_Hibbard( int A[],int N )
{
int K,D;
for(K=0;((int)pow(2,K)-1)<=N;K++);
K--;
for(;K>0;K--){
D = (int)pow(2,K)-1;
Insertion_Sort( A,N,D );
}
}
void Insertion_Sort( int A[],int N,int D )
{
int P,i,Temp;
for(P=D;P<N;P++){
Temp = A[P];
for(i=P;i>=D && A[i-D]>Temp;i-=D){
A[i] = A[i-D];
}
A[i] = Temp;
}
}
void Print( int A[],int N )
{
int i;
for(i=0;i<N-1;i++){
printf("%d ",A[i]);
}
printf("%d",A[N-1]);
}
3.Sedgewick增量——希尔排序:
Sedgewick增量的希尔排序 表现优异
代码:
#include <stdio.h>
void Print( int A[],int N );
void Shell_Sort_Sedgewick( int A[],int N );
void Insertion_Sort( int A[],int N,int D );
int main()
{
int N;
scanf("%d",&N);
int i,A[N];
for(i=0;i<N;i++){
scanf("%d",&A[i]);
}
Shell_Sort_Sedgewick( A,N );
Print( A,N );
return 0;
}
void Shell_Sort_Sedgewick( int A[],int N )
{
int Sedgewick[] = {260609,146305,64769,36289,16001,8929,3905,
2161,929, 505,209, 109, 41, 19, 5, 1, 0};
int Si,D;
for(Si=0;Sedgewick[Si]>=N;Si++);
for(;Sedgewick[Si]>0;Si++){
D = Sedgewick[Si];
Insertion_Sort( A,N,D );
}
}
void Insertion_Sort( int A[],int N,int D )
{
int P,i,Temp;
for(P=D;P<N;P++){
Temp = A[P];
for(i=P;i>=D && A[i-D]>Temp;i-=D){
A[i] = A[i-D];
}
A[i] = Temp;
}
}
void Print( int A[],int N )
{
int i;
for(i=0;i<N-1;i++){
printf("%d ",A[i]);
}
printf("%d",A[N-1]);
}
▶♥ 选择排序
从0到N-1 每次选择一个下标,这个下标所存的元素去和其他所有的元素对比,如果比这个小,那就交换过来
所以选择每完成一趟,一定是将最小的元素选择出来了,并放在在正确的位置;
即第i次选择,一定将第i小的元素选择出来了,放在了第i位上.
粗暴方法之Low写法:
void Selection_Sort( ElementType A[]),int N )
{
for(i=0;i<N-1;i++){
for(j=i+1;j<N;j++){
if(A[j] < A[i]){
Swap( &A[j],&A[i] );
}
}
}
} 这样实际的交换次数并没有减少 和冒泡排序 插入排序 需要交换的次数是一样的 不推荐!!!
时间复杂度:T=Θ(N^2)
✔进阶
每次只做比较 不交换,直到确定真正的最小元,再交换到前面去
遍历数组,找出最小元,和对应位置作比较,更小就交换
void Selection_Sort( ElementType A[],int N )
{
for(i=0;i<N;i++){
/* 从A[i]到A[N-1]中找最小元,并将其位置赋给MinPosition */
MinPosition = ScanForMin( A,i,N-1 );
/* 将未排序部分的最小元换到有序部分的最后位置 */
Swap( &A[i],&A[MinPosition] );
}
} 当Swap之前先判断A[i]和A[MinPosition]的大小 就具有稳定性
不比较直接交换就 不具有稳定性
最坏情况:for循环 每次都需要交换 那么需要交换N-1次
时间复杂度:外层循环是固定的 T=Θ(N)
关键取决于寻找最小元函数
如果采用简单粗暴的方法:也是一个遍历整个数组 那么T=Θ(N^2)
粗暴方法之优秀写法:
void Selection_Sort( ElementType A[],int N )
{
for(i=0;i<N-1;i++){
MinID = i;
for(j=i+1;j<N;j++){
if(A[j] < A[MinID]){
MinID = j;
}
}
Swap( &A[i],&A[MinID] );
}
} 这样 每次交换可能会消掉好几个逆序对 比上面的 和 冒泡 插入 要效率高 且具有稳定性
.
测试结果:
1.选择排序之粗暴+Low:
也就比冒泡好那么一点点,对于基本有序的序列还不如冒泡,以后千万别这么写了
代码:
#include <stdio.h>
void Swap( int* a,int* b );
void Print( int A[],int N );
void Selection_Sort_Low( int A[],int N );
int main()
{
int N;
scanf("%d",&N);
int i,A[N];
for(i=0;i<N;i++){
scanf("%d",&A[i]);
}
Selection_Sort_Low( A,N );
Print( A,N );
return 0;
}
void Selection_Sort_Low( int A[],int N )
{
int i,j;
for(i=0;i<N-1;i++){
for(j=i+1;j<N;j++){
if(A[i] > A[j]){
Swap( &A[i],&A[j] );
}
}
}
}
void Swap( int* a,int* b )
{
int Temp = *a;
*a = *b;
*b = Temp;
}
void Print( int A[],int N )
{
int i;
for(i=0;i<N-1;i++){
printf("%d ",A[i]);
}
printf("%d",A[N-1]);
}
2.选择排序之正常写法:
不是很好,不如普通的插入排序
代码:
#include <stdio.h>
void Swap( int* a,int* b );
void Print( int A[],int N );
void Selection_Sort( int A[],int N );
int main()
{
int N;
scanf("%d",&N);
int i,A[N];
for(i=0;i<N;i++){
scanf("%d",&A[i]);
}
Selection_Sort( A,N );
Print( A,N );
return 0;
}
void Selection_Sort( int A[],int N )
{
int i,j,MinID;
for(i=0;i<N-1;i++){
MinID = i;
for(j=i+1;j<N;j++){
if(A[j] < A[MinID]){
MinID = j;
}
}
if(A[MinID] < A[i]){
Swap( &A[i],&A[MinID]);
}
}
}
void Swap( int* a,int* b )
{
int Temp = *a;
*a = *b;
*b = Temp;
}
void Print( int A[],int N )
{
int i;
for(i=0;i<N-1;i++){
printf("%d ",A[i]);
}
printf("%d",A[N-1]);
}
选择排序之进阶
▶♥ 堆排序
从上面的选择排序可以看出,如果能快速的找出最小元,那么效率就会大大提高;
如何快速找到最小元? 想到什么?没错!!! ———————堆
堆排序是在选择排序的基础上进行的改进
◎堆排序1:
耿直BOY算法:
void Heap_Sort( ElementType A[],int N )
{
BuildHeap(A);//将数组调整为最小堆
for(i=0;i<N;i++){
TempA[i] = DeleteMinHeap( A );//弹出最小元素 存放在临时数组里
}
for( i=0;i<N;i++){ //将排好序的序列倒回原数组里
A[i] = TempA[i];
}
} 时间复杂度: 调整堆 O(N)
弹出堆顶O(logN)
倒回元素O(N)
整体是T=O(NlogN)
缺点:需要临时一个大小等于原数组空间的数组,如果数据比较大,将占用太多空间;如 2GB内存最多只能排1GB,排不了2GB的数据
需要额外O(N)空间,且复制元素也需要时间
不具有稳定性
学霸算法:
将数组调整为最大堆,每次将堆顶元素(最大值),换到最下面去,同时堆的规模-1;
void Heap_Sort( ElementType A[],int N )
{
for(i=N/2;i>=0;i--){ //BuildMaxHeap;
PercDown( A,i,N );
}
for(i=N-1;i>0;i--){ //DeleteMaxHeap;
Swap( &A[0],&A[i] );
PercDown( A,0,i );
}
} 优点:高效,适合解决top(K)问题,冒泡排序找top太慢了,插入排序必须等排完才能找到
缺点:不具有稳定性
.定理:堆排序处理N个不同元素的随机排序的平均比较次数是 2NlogN-O(NlogN)
.虽然堆排序给出最佳平均时间复杂度,但实际效果不如用Sedgewick增量序列的希尔排序
void Heap_Sort( ElementType A[],int N )
{
int i;
for(i=N/2;i>=0;i--){
PercDown( A,i,N );
}
for(i=N-1;i>0;i--){
Swap( &A[0],&A[i] );
PercDown( A,0,i );
}
}
void PercDown( ElementType A[],int i,int N )
{/*将N个元素的数组以A[i]为根的子堆调整为最大堆*/
int Parent,Child;
ElementType Temp = A[i];//取出根结点存放的值
for(Parent=i;(Parent*2+1)<N;Parent=Child){
Child = Parent*2+1;
if(Child != N-1 && A[Child]<A[Child+1]){
Child++;
}
if(Temp >= A[Child]){
break;
}else{
A[parent] = A[Child];
}
}
A[Parent] = Temp;
}
.
测试结果:
1.堆排序之将原数组调整为最小堆:
比不上采用增量序列的希尔排序,跟原始的希尔排序差不多,但是空间多了不少;所以以后需要的话还是采用最大堆的方式;
代码:
#include <stdio.h>
void Print( int A[],int N );
void Heap_Sort_MinHeap( int A[],int N );
void PercDown_MinHeap( int A[],int i,int N );
int main()
{
int N;
scanf("%d",&N);
int i,A[N];
for(i=0;i<N;i++){
scanf("%d",&A[i]);
}
Heap_Sort_MinHeap( A,N );
Print( A,N );
return 0;
}
void Heap_Sort_MinHeap( int A[],int N )
{
int i;
for(i=N/2-1;i>=0;i--){
PercDown_MinHeap( A,i,N );
}
int TempA[N];
int TempN = N;
for(i=0;i<TempN;i++){
TempA[i] = A[0];
A[0] = A[N-1];
PercDown_MinHeap( A,0,N-1 );
N--;
}
for(i=0;i<TempN;i++){
A[i] = TempA[i];
}
}
void PercDown_MinHeap( int A[],int i,int N )
{
int Temp = A[i];
int Parent,Child;
for(Parent=i;(Parent*2+1)<N;Parent=Child){
Child = Parent*2;
if(Child != N-1 && A[Child]>A[Child+1]){
Child++;
}
if(Temp <= A[Child]){
break;
}else{
A[Parent] = A[Child];
}
}
A[Parent] = Temp;
}
void Print( int A[],int N )
{
int i;
for(i=0;i<N-1;i++){
printf("%d ",A[i]);
}
printf("%d",A[N-1]);
}
2.堆排序之将原数组调整为最大堆:
对比小顶堆,数据量大的时候空间优化还是很明显的,但是不具有稳定性,空间和时间都是比希尔排序差那么一点
代码:
#include <stdio.h>
void Swap( int* a,int* b );
void Print( int A[],int N );
void Heap_Sort( int A[],int N );
void PercDown( int A[],int i,int N );
int main()
{
int N;
scanf("%d",&N);
int i,A[N];
for(i=0;i<N;i++){
scanf("%d",&A[i]);
}
Heap_Sort( A,N );
Print( A,N );
return 0;
}
void Heap_Sort( int A[],int N )
{
int i;
for(i=N/2-1;i>=0;i--){
PercDown( A,i,N );
}
for(i=N-1;i>0;i--){
Swap(&A[0],&A[i]);
PercDown( A,0,i );
}
}
void PercDown( int A[],int i,int N )
{
int Temp = A[i];
int Parent,Child;
for(Parent=i;(Parent*2+1)<N;Parent=Child){
Child = Parent*2;
if(Child != N-1 && A[Child]<A[Child+1]){
Child++;
}
if(Temp >= A[Child]){
break;
}else{
A[Parent] = A[Child];
}
}
A[Parent] = Temp;
}
void Swap( int* a,int* b )
{
int Temp = *a;
*a = *b;
*b = Temp;
}
void Print( int A[],int N )
{
int i;
for(i=0;i<N-1;i++){
printf("%d ",A[i]);
}
printf("%d",A[N-1]);
}
▶♥ 归并排序
归并的核心:两个有序子列的归并
例如: 1 13 24 26 2 15 27 38 将这两个已经排好序的序列合并成一个序列
算法实现:每个数组一个指针变量(下标)Aptr,Bptr, 比较这两个指针所指的元素大小,
小的元素复制到新的数组中,然后其指针后移一位,继续比较,直到其中一个序列比较完
将另一个序列剩下的没有比较的元素复制到新数组中
核心代码:
/* L=左边序列的起始位置 R=右边序列的起始位置 RightEnd=右边终点位置 两个序列紧邻着放在一个数组里*/
void Merge( ElementType A[],ElmentType TempA[],int L,int R,int RightEnd )
{
LeftEnd = R - 1; //左边终点位置 两个序列紧邻着放在一个数组里
Temp = L; //存放合并后的数组起始位置
NumElements = RightEnd - L + 1;
while(L<=LeftEnd && R<=RightEnd){
if(A[L]<=A[R]){
TempA[Temp++] = A[L++];
}else{
TempA[Temp++] = A[R++];
}
}
while(L<=LeftEnd){ //直接复制左边剩下的
TempA[Temp++] = A[L++];
}
while(R<=RightEnd){ //直接复制右边剩下的
TempA[Temp++] = A[R++];
}
for(i=0;i<NumElements;i++,RightEnd--){ //倒回到原数组
A[RightEnd] = TempA[RightEnd];
}
} T=O(N) 具有稳定性
知道了核心的有序两个子序的归并,现在看如何将一个无序的序列归并排序
◎1.递归算法
分而治之 口|口|口口|口口口口|口口口口口口口口|口口口口口口口口口口口口口口口口
将序列分成左右两子序列 递归将左右归并排序
左右两个子序又分别分成左右两个子序
...
最后分成两个子序都是只有一个元素的序列
void MSort( ElementType A[],ElementType TmpA[],int L,int RightEnd )
{
int Center;
if(L<RightEnd){
Center = (L + RightEnd) / 2;
MSort( A,TmpA,L,Center );
MSort( A,TmpA,Center+1,RightEnd );
Merge( A,TmpA,L,Center+1,RightEnd );
}
}
时间复杂度: T(N)=T(N/2)+T(N/2)+O(N) ==> T(N)=O(NlogN) 没有最坏 最好 平均 任何情况下都是O(NlogN)
最后统一函数接口:
void Merge_Sort( ElementType A[],int N )
{
ElementType* TmpA = NULL;
TmpA = (ElementType*)malloc(N*sizeof(ElementType));
if(TmpA != NULL){
MSort( A,TmpA,0,N-1);//
free(TmpA);
}else{
Error("空间不足");
}
}
. 递归算法 需要占用系统堆栈 加上需要额外的操作 导致递归往往较慢
◎2.循环迭代算法(非递归算法)
A 口口口口口口口口口口口口口口口口口口口口口口口口口口口口口口口口口口口
↘↙↘↙ ↘↙↘↙
1 ----————----————----————----————----————----————----————----————
↘↙ ↘↙ ↘↙ ↘↙
2 --------————————--------————————---------————————--------————————
↘ ↙ ↘ ↙ ↘ ↙ ↘ ↙
3 ----------------————————————————-----------------————————————————
↘ ↙ ↘ ↙
i --------------------------------——————————————————————————————————
↘ ↙
------------------------------------------------------------------
将相邻的两个元素做一次归并得到若干个有序的序列,
再将相邻的两个序列做一次归并
......
直到归并成一个有序的序列
☼并不需要每一次归并都开一个对应大小的数组,只需开一个跟原数组同等大小的临时数组TmpA就行
第一次A归并到TmpA,第二次TmpA归并到A,第三次A归并到TmpA....
额外空间复杂度是O(N)
核心算法:循环中的某一趟递归 length=当前有序子列的长度 从1开始 翻倍增长
void Merge_pass(ElementType A[],ElementType TmpA[],int N,int length )
{
for(i=0;i<=N-2*length;i+=2*length){
Merge1( A,TmpA,i,i+length,i+2*length-1 );//原Merge函数上减去从TmpA倒回A的步骤
}
if(i+length<N){ //归并最后2个子列 奇数个元素时尾巴要特殊处理
Merge1( A,TmpA,i,i+length,N-1 );
}else{ //最后只剩1个子列
for(int j=i;j<N;j++) TmpA[j] = A[j];
}
}
统一函数接口:
void Merge_Sort( ElementType A,int N )
{
int length = 1;//初始化子序列长度
ElementType* TmpA = NULL;
TmpA = malloc(N*sizeof(ElementType));
if(TmpA != NULL){
while(length < N){
Merge_pass( A,TmpA,N,length );
length *= 2;
Merge_pass( TmpA,A,N,length );
length *= 2;
}
free(TmpA);
}else{
Error("空间不足")
}
} 优点:T=O(NlogN) 具有稳定性
缺点:需要额外空间O(N) 和 数组之间来回复制导元素
因此,归并排序一般不用于内排序,往往使用于外排序
测试结果:
1.归并排序之递归算法:
归并排序 各方面都表现优异 且具有稳定性,额外空间占用可以接受
代码:
#include <stdio.h>
#include <stdlib.h>
void Print( int A[],int N );
void Merge_Sort( int A[],int N );
void MSort( int A[],int TmpA[],int Left,int Right );
void Merge( int A[],int TmpA[],int Left,int Right,int RightEnd );
int main()
{
int N;
scanf("%d",&N);
int i,A[N];
for(i=0;i<N;i++){
scanf("%d",&A[i]);
}
Merge_Sort( A,N );
Print( A,N );
return 0;
}
void Merge_Sort( int A[],int N )
{
int* TmpA = malloc(N*sizeof(int));
MSort( A,TmpA,0,N-1 );
free(TmpA);
}
void MSort( int A[],int TmpA[],int Left,int RightEnd )
{
int Center;
if(Left < RightEnd){
Center = (Left+RightEnd)/2;
MSort( A,TmpA,Left,Center );
MSort( A,TmpA,Center+1,RightEnd );
Merge( A,TmpA,Left,Center+1,RightEnd );
}
}
void Merge( int A[],int TmpA[],int L,int R,int RightEnd )
{
int LeftEnd = R-1;
int Tmp = L;
int Num = RightEnd-L+1;
while(L<=LeftEnd && R<=RightEnd ){
if(A[L] <= A[R]){
TmpA[Tmp++] = A[L++];
}else{
TmpA[Tmp++] = A[R++];
}
}
while(L<=LeftEnd){
TmpA[Tmp++] = A[L++];
}
while(R<=RightEnd){
TmpA[Tmp++] = A[R++];
}
int i;
for(i=0;i<Num;i++){
A[RightEnd] = TmpA[RightEnd];
RightEnd--;
}
}
void Print( int A[],int N )
{
int i;
for(i=0;i<N-1;i++){
printf("%d ",A[i]);
}
printf("%d",A[N-1]);
}
2.归并排序之循环算法:
归并排序的循环算法 空间够的情况下 相当不错
代码:
#include <stdio.h>
#include <stdlib.h>
void Print( int A[],int N );
void Merge_Sort( int A[],int N );
void Merge_pass( int A[],int TmpA[],int N,int length );
void Merge1( int A[],int TmpA[],int Left,int Right,int RightEnd );
int main()
{
int N;
scanf("%d",&N);
int i,A[N];
for(i=0;i<N;i++){
scanf("%d",&A[i]);
}
Merge_Sort( A,N );
Print( A,N );
return 0;
}
void Merge_Sort( int A[],int N )
{
int* TmpA = malloc(N*sizeof(int));
int length = 1;
while(length<N){
Merge_pass( A,TmpA,N,length );
length *= 2;
Merge_pass( TmpA,A,N,length );
length *= 2;
}
free(TmpA);
}
void Merge_pass( int A[],int TmpA[],int N,int length )
{
int i,j;
for(i=0;i<N-2*length;i+=2*length){
Merge1( A,TmpA,i,i+length,i+2*length-1 );
}
if(i+length < N){
Merge1( A,TmpA,i,i+length,N-1 );
}else{
for(int j=i;j<N;j++)
TmpA[j] = A[j];
}
}
void Merge1( int A[],int TmpA[],int L,int R,int RightEnd )
{
int LeftEnd = R-1;
int Tmp = L;
int Num = RightEnd-L+1;
while(L<=LeftEnd && R<=RightEnd ){
if(A[L] <= A[R]){
TmpA[Tmp++] = A[L++];
}else{
TmpA[Tmp++] = A[R++];
}
}
while(L<=LeftEnd){
TmpA[Tmp++] = A[L++];
}
while(R<=RightEnd){
TmpA[Tmp++] = A[R++];
}
}
void Print( int A[],int N )
{
int i;
for(i=0;i<N-1;i++){
printf("%d ",A[i]);
}
printf("%d",A[N-1]);
}
▶♥ 快速排序
传说中,在现实应用中最快的一种排序算法.
但总是存在最坏情况,使得它的表现非常糟糕;
不过大多数情况下,对于大规模的随机数据,快速排序还是相当出色的
前提是要把每个细节处理好!
◆算法概述:
口.核心思想 【分而治之】
在一堆整数里面选择一个作为主元(pivot) ,然后将数据分为两大块;左边的数据都比主元小,右边的都比主元大
然后递归的治理左边和右边
最后将这3块数据集合并起来,完成快速排序;
例如: 13 81 第一步选择主元: 假如选择了 65
92 43 65 第二部, 分 13 43 81
31 57 26 31 57 26 65 92 75
75 0 0
第三步, 治 0 13 26 31 53 57 65 75 81 92
第四步, 合 0 13 26 31 53 57 65 75 81 92
伪码描叙:
void Quicksort( ElementType A[],int N )
{
if(N < 2) return;
pivot = 从A[]中选一个主元;
将S = { A[] \ pivot } 分成2个独立子集;
A1 = { a∈S | a <= pivot } 和
A2 = { a∈S | a >= pivot };
A[] = Quicksort(A1,N1) ∪ { pivot } ∪ Quicksort(A2,N2);
}
算法实现的细节1:怎么选主元? 如果选的主元将集合分为其中一个元素过少,而另一个元素过多, 快速排序快不起来
比如每次都是选了最小的元素作为主元;
细节2:选好主元之后如何分? 如果分的过程太耗时,整个算法也快不起来
快速排序算法的最好情况:每次正好中分 ==> T(N)=O(NlogN)
口.如何选主元:
1.令pivot = A[0] ? 不行! 比如对基本有序的数组,这样的主元将导致左右严重失衡
1 2 3 4 5 6 ... ... N-1 N
2 3 4 5 6 ... ... N-1 N
3 4 5 6 ... ... N-1 N
...
T(N)=O(N)+T(N-1)
=O(N)+O(N-1)+T(N-2)
=O(N)+O(N-1)+O(N-2)+ ... + O(2)+O(1)
=O(N^2)
用递归的方式,实现了1个时间复杂度为O(N^2)的算法,简直是一个递归的囧!
2.随机取pivot? 但是rand()函数不便宜
3.一个经典的取主元的方法:
取头、中、尾这2个数的中位数
比如 8 12 3 的中位数 8
也有取5位数 7位数的中位数的方法
ElementType Median3( ElementType A[],int Left,int Right )
{
int Center = ( Left + Right ) / 2;
if(A[Left] > A[Center]){
Swap( &A[Left],&A[Center] );
}
if(A[Left] > A[Right]){
Swap( &A[Left],&A[Right] );
}
if(A[Center] < A[Right]){
Swap( &A[Center],&A[Right] );
} //这3步if走完 一定是A[Left] <= A[Center] <= A[Right];
Swap( &A[Center],&A[Right-1] ); //为了后续操作方便,将pivot藏在右边
/* Left位置的元素一定比主元小,Right位置的元素一定比主元大,Right-1位置上的是主元
后面分的时候只要考虑Left+1 到 Right-2 就行了*/
return A[Right-1]; //返回主元pivot
}
口.子集划分:
【左右指针法】
Median3取主元后 假如取了主元6,放在了Right-1的位置上
Left 8 1 4 9 0 3 5 2 7 6 Right
两个指针i,j i→ ←j pivot
先i所指的元素和主元比较 j
如果比主元小 后移一位 继续比较
如果比主元大 暂停
Left 2 1 4 9 0 3 5 8 7 6 Right
去比较j所指的元素 i→ ←j pivot
如果比主元大 前移一位 继续比较 i j
如果比主元小 暂停
Left 2 1 4 5 0 3 9 8 7 6 Right
交换i和j所指的元素 i→ j pivot
Left 2 1 4 5 0 3 9 8 7 6 Right
然后继续上面的步骤 j i pivot
当i > j时,循环跳出; Left 2 1 4 5 0 3 6 8 7 9 Right
主元换到i位置,子集划分完毕 j i
pivot
可以看出:快速排序每次选定主元,根据该主元划分子集完毕后,主元就放到了正确的位置
以后都不会再动了.
这就是快速排序之所以快的原因.
口.特殊情况分析 :
当有元素等于主元时,该如何操作?
a.不停下,不交换
缺点:可能i j停下的位置靠后,划分的两个子集元素个数差距较大
比如一个全是1 的序列 i会一直移动到最后,j没机会移动
优点:避免了很多无用的交换
b.停下来,做交换
缺点:做了很多无用的交换
优点:最后i j停下的位置上相对更好,主元换到i位置后 划分的两个子集元素个数不会差距太大
考虑到时间复杂度 还是选择b 停下来作交换吧.
口.小规模数据的处理:
考虑到快速排序时用递归的方式实现,将占用额外的系统堆栈的空间,每一次调用系统堆栈的时候
都有一大堆的Push Pop额外操作
所以当数据规模比较小时(例如N不到100),可能还不如简单的插入排序快
解决方案:
.当递归到数据规模比较小时,停止递归,直接调用简单排序(例如插入排序)
.在程序中定义一个阈值Cutoff,递归到小于这个阈值时,调用简单排序
————不同的Cutoff将对效率有不同的影响
◆算法实现:
void Quicksort( ElementType A[],int Left,int Right )
{
if(Cutoff <= Right-Left){ //元素过多,进入快排
Pivot = Median3( A,Left,Right );//选主元
i = Left; j = Right-1;
while(1){ //将序列中比主元小的移到左边,大的移到右边
while(A[++i] < Pivot);
while(A[++i] < Pivot);
if(i< j)
Swap( &A[i],&A[j] );
else
break;
}
Swap( &A[i],&A[Right-1] ); //将主元换到正确位置
Quicksort( A,Left,i-1 ); //递归解决左边
Quicksort( A,i+1,Right ); //递归解决右边
}else //元素太少,直接简单排序
Insertion_Sort( A+Left,Right-Left+1 ); //这里的A+Left 需要理解下: 想想以前学的指针 int* p;p++
}
最后统一函数接口:
void Quick_Sort( ElementType A[],int N )
{
Quicksort( A,0,N-1 );
}
最后:快速排序不具有稳定性.
/* 快速排序 - 直接调用库函数 */
#include <stdlib.h>
/*---------------简单整数排序--------------------*/
int compare(const void *a, const void *b)
{ /* 比较两整数。非降序排列 */
return (*(int*)a - *(int*)b);
}
/* 调用接口 */
qsort(A, N, sizeof(int), compare);
/*---------------简单整数排序--------------------*/
/*--------------- 一般情况下,对结构体Node中的某键值key排序 ---------------*/
struct Node {
int key1, key2;
} A[MAXN];
int compare2keys(const void *a, const void *b)
{ /* 比较两种键值:按key1非升序排列;如果key1相等,则按key2非降序排列 */
int k;
if ( ((const struct Node*)a)->key1 < ((const struct Node*)b)->key1 )
k = 1;
else if ( ((const struct Node*)a)->key1 > ((const struct Node*)b)->key1 )
k = -1;
else { /* 如果key1相等 */
if ( ((const struct Node*)a)->key2 < ((const struct Node*)b)->key2 )
k = -1;
else
k = 1;
}
return k;
}
/* 调用接口 */
qsort(A, N, sizeof(struct Node), compare2keys);
/*--------------- 一般情况下,对结构体Node中的某键值key排序 ---------------*/
.
测试结果:
快速排序 名不虚传~!
代码:
#include <stdio.h>
#include <stdlib.h>
#define Cutoff 500
void Swap( int* a,int* b );
void Print( int A[],int N );
void Quick_Sort( int A[],int N );
void Quicksort( int A[],int Left,int Right );
int Median3( int A[],int Left,int Right );
void Insertion_Sort( int A[],int N );
int main()
{
int N;
scanf("%d",&N);
int i,A[N];
for(i=0;i<N;i++){
scanf("%d",&A[i]);
}
Quick_Sort( A,N );
Print( A,N );
return 0;
}
void Quick_Sort( int A[],int N )
{
Quicksort( A,0,N-1 );
}
void Quicksort( int A[],int Left,int Right )
{
if(Right-Left > Cutoff ){
int pivot,i,j;
pivot = Median3( A,Left,Right );
i = Left;
j = Right-1;
while(1){
while(A[++i] < pivot);
while(A[--j] > pivot);
if(i < j){
Swap( &A[i],&A[j] );
}else{
break;
}
}
Swap( &A[i],&A[Right-1] );
Quicksort( A,Left,i-1 );
Quicksort( A,i+1,Right );
}else{
Insertion_Sort( A+Left,Right-Left+1 );
}
}
int Median3( int A[],int Left,int Right )
{
int Center = (Left+Right)/2;
if(A[Left] > A[Center]){
Swap( &A[Left],&A[Center] );
}
if(A[Left] > A[Right]){
Swap( &A[Left],&A[Right] );
}
if(A[Center] > A[Right]){
Swap( &A[Center],&A[Right] );
}
Swap( &A[Center],&A[Right-1] );
return A[Right-1];
}
void Insertion_Sort( int A[],int N )
{
int P,i,Tmp;
for(P=1;P<N;P++){
Tmp = A[P];
for(i=P;i>=1 && A[i-1]>Tmp;i--){
A[i] = A[i-1];
}
A[i] = Tmp;
}
}
void Swap( int* a,int* b )
{
int Temp = *a;
*a = *b;
*b = Temp;
}
void Print( int A[],int N )
{
int i;
for(i=0;i<N-1;i++){
printf("%d ",A[i]);
}
printf("%d",A[N-1]);
}
▶♥ 桶排序(进阶的计数排序)
假设某省的一次模拟考,考生为N(很大),他们的数学成绩都是在0到150之间的整数(假设不存在小数的情况),
于是只有M=151个成绩值.如何在线性时间内将学生按数学成绩排序呢?
当数据具有这种量很大,但是值的可能性比较少的特性时,我们就可以用桶排序(计数排序),在线性时间内完成排序
算法概述:
.为每一种可能的值建一个桶(计数数组)
.遍历整个数据,根据元素的值,将元素放入对应的桶(计数数组)中
.遍历每个桶,输出桶中的元素,结果就是已经排好序了的
建M个桶(计数数组)(链表) 0 1 2 3 4 ... ... 149 150
将元素放入对应的桶中 count 口 口 口 口 口 口 口
↓ ↓ ↓ ↓ ↓ ↓ ↓
● 口 口 口 口 口 口
↓ ↓ ↓ ↓ ↓ ↓
● 口 ● 口 口 口
↓ ↓ ↓ ↓
● ● 口 ●
↓
●
void Bucket_Sort( ElementType A[],int N )
{
count[]初始化;
while(读入每个学生的数学成绩)
将该生插入count[grade]链表;
for(i=0;i<M;i++){
if(count[i]不为空)
输出整个count[i]链表;
}
} 时间复杂度T(N,M)=(N+M) 当M远小于N时,就是接近线性时间了;
桶排序具有稳定性
拓展:
挑战名企面试官
某名企的一道面试题: 从1000个数字中找出最大的10个数字,最快的算法是————
A.归并排序
B.快速排序
C.堆排序
D.选择排序
根据堆排序的特性,很容易4种选项中只能是C ————但是这个答案真的对吗?
——————不对,如果这1000个数据的取值范围很小,那么就能用桶排序在线性时间内完成;
简单计数排序图示:
桶排序图示:
仔细观察 数据8:10^5个随机正整数,每个数字不超过1000。
N=10^5个数据 只有取值M=1000种可能,M<<N;
符合桶排序的特征,所以可以用桶排序(这里只需用简单的计数排序)来高效排序
测试结果:
比前面任何一种排序都要快好多~!
代码:
#include <stdio.h>
#define MaxNum 1000
void Print( int A[],int N );
void Count_Sort( int A[],int N );
int main()
{
int N;
scanf("%d",&N);
int i,A[N];
for(i=0;i<N;i++){
scanf("%d",&A[i]);
}
Count_Sort( A,N );
Print( A,N );
return 0;
}
void Count_Sort( int A[],int N )
{
int i,Count[MaxNum] = {0}; //根据取值可能值建桶 这里是0-999
for(i=0;i<N;i++){ //遍历A数组,根据每个元素的值计数(入桶)
Count[A[i]]++;
}
int j,C;
i=0;
for(C=0;C<MaxNum;C++){ //遍历每个桶
for(j=0;j<Count[C];j++){ //每个桶的元素倒回A数组
A[i++] = C;
}
}
}
void Print( int A[],int N )
{
int i;
for(i=0;i<N-1;i++){
printf("%d ",A[i]);
}
printf("%d",A[N-1]);
}
以上是取值之范围远小于数据个数 M<<N, 那么当M远大于N时M>>N,能不能线性时间内完成呢?
▶♥ 基数排序
假设我们有N=1000个整数,每个整数的值在0到9999之间(于是有M=10000种情况).还有可能在线性时间内完成排序吗?
算法概述:
口基数:十进制的基数有10个 0 1 2 3 4 5 6 7 8 9
八进制的基数有2个 0 1 2 3 4 5 6 7
十六进制的基数有16个 0 1 2 3 4 5 6 7 8 9 A B C D E F
二进制的基数有2个 0 1
扑克牌的花色基数有4个 梅花 方片 红心 黑桃
扑克牌的点数基数有13个 2 3 4 5 6 7 8 9 10 J Q K A
.一个元素可能有多个位数,每个位数都有基数:比如一个3位数 有百位 十位 个位 (每位的基数都有10个,即需要建10个桶)
比如扑克牌有2个位数 花色(需要建4个桶)和点数(需要建13个桶)
.将核心排序的位称为主位:比如3位数 排序 核心主位是百位 然后次位是十位 最次位是个位
比如一副新的扑克牌 排序的核心主位是花色 然后次位是点数大小
☼这种充分利用元素的基数特性来建桶的排序,称为基数排序
.根据考虑的位数的先后不同,分为"主位优先"(MSD)(Most Significant Digit) 和"次位优先"(LSD)(Least Significant Digit)
来看具体的例子: 序列 64 8 216 512 27 729 0 1 343 125
采用LSD算法,即先按个位排好序,再按十位排好序,最后按百位排好序;完成后输出的序列即使有序序列
第一步 根据十进制的基数特性 建10个桶 Bucket 0 1 2 3 4 5 6 7 8 9
第二步 按个位入桶 组成一个序列 Pass1 0 1 512 343 64 125 216 27 8 729
第三步 按十位入桶 组成一个序列 Pass2 0 512 125 343 64
1 216 27
8 729
第四步 按百位入桶 输出排好序的序列 Pass3 0 125 216 343 512 729
1
8
27
64
时间复杂度T=O(P(N+B)) P为位数个数
B为每个位数对应的基数个数(建桶个数)
当基数个数B元小于N,且P很小的时候,几乎就是线性时间了
口多关键字的排序
例如: 一副扑克牌就是按2种关键字排序的
K0[花色] 主位 ♣ < ♦ < ♥ < ♠
K1[面值] 次位 2 < 3 < 4 < 5 < 6 < 7 < 8 < 9 < 10 < J < Q < K < A
有序结果 2♣ ... A♣ 2♦ ... A♦ 2♥ ... A♥ 2♠ ... A♠
方法1:MSD 根据主位花色建4个桶 将牌分好
再利用插入排序将每个桶中的牌排好序
方法2:LSD 根据次位面值建13个桶 将牌入桶 整理好一个序列
再根据主位花色建4个桶 将牌入桶 这样就已经排好序了 不用再调用排序算法
以上两个例子都是LSD比MSD好,那么是不是任何时候LSD都比MSD好呢?
————不难看出 MSD是根据主位建一次桶,然后对每个桶内的元素调用其他排序算法进行排序
而LSD 是根据每个位数建桶,即需要多次合并再建桶,但是不需要额外调用其他排序算法
所以当位数比较多的时候,MSD比LSD要好;位数较少时,LSD比MSD要好
最后 基数排序是在桶排序的基础上改良而来,同样具有稳定性
/* 基数排序 —— 次位优先 */
/* 假设元素最多有MaxDigit个关键字,基数全是同样的Radix*/
#define MaxDigit 4
#define Radix 10
/* 桶元素结点*/
typedef struct Node *PtrToNode
struct Node{
int Key;
PtrToNode Next;
};
/* 桶头结点 */
struct HeadNode{
PtrToNode Head,Tail;
};
typedef struct HeadNode Bucket[Radix];
int GetDigit( int X,int D )
{/* 默认次位D=1,主位D<=MaxDigit */
int d,i;
for(i=1;i<=D;i++){
d = X%Radix;
X /= Radix;
}
return d;
}
void LSDRadixSort( ElementType A[],int N )
{/* 基数排序 - 次位优先 */
int D,Di,i;
Bucket B;
PtrToNode tmp,p,List = NULL;
for(i=0;i<Radix;i++){ //初始化每个桶为空链表
B[i].Head = B[i].Tail = NULL;
}
for(i=0;i<N;i++){ //将原始序列逆序存入初始链表List
tmp = (PtrToNode)malloc(sizeof(struct Node));
tmp->Key = A[i];
tmp->Next = List;
List = tmp;
}
/* 下面开始排序 */
for(D=1;D<MaxDigit;D++){ //对数据的每一位循环处理
/* 下面是分配的过程 */
p = List;
while(p){
Di = GetDigit( p->Key,D ); //获得当前元素的当前位数数字
/* 从List中摘除 */
tmp = p;
p = p->Next;
/* 插入B[Di]号桶尾 */
tmp->Next = NULL;
if(B[Di].Head == NULL){
B[Di].Head = B[Di].Tail = tmp;
}else{
B[Di].Tail->Next = tmp;
B[Di].Tail = tmp;
}
}
/* 下面是收集的过程 */
List = NULL;
for(Di=Raidx-1;Di>0;Di--){ //将每个桶的元素顺序手机入List
if(B[Di].Head){ //如果桶不为空
/* 整桶插入List表头 */
B[Di].Tail->Next = List;
List = B[Di].Head;
B[Di].Head = B[Di].Tail = NULL; //清空桶
}
}
}
/* 将List倒入A[] 并释放空间*/
for(i=0;i<N;i++){
tmp = List;
List = List->Next;
A[i] = tmp->Key;
free(tmp);
}
}
/* 基数排序 —— 主位优先 */
void MSDRadixSort( ElementType A[],int N )
{ /* 统一接口 */
MSD( A,0,N-1,MaxDigit);
}
void MSD( ElementType A[],int L,int R,int D )
{ /* 核心递归函数:对A[L] ... A[R]的第D位进行排序 */
int Di,i,j;
Bucket B;
PtrToNode tmp,p,List = NULL;
if(D == 0) return; //递归终止条件
for(i=0;i<Radix;i++){ //初始化每个桶为空链表
B[i].Head = B[i].Tail = NULL;
}
for(i=0;i<N;i++){ //将原始序列逆序存入初始链表List
tmp = (PtrToNode)malloc(sizeof(struct Node));
tmp->Key = A[i];
tmp->Next = List;
List = tmp;
}
/* 下面是分配的过程 */
p = List;
while(p){
Di = GetDigit( p->Key,D ); //获得当前元素的当前位数数字
/* 从List中摘除 */
tmp = p;
p = p->Next;
/* 插入B[Di]号桶尾 */
tmp->Next = NULL;
if(B[Di].Head == NULL) B[Di].Tail = tmp;
tmp->Next = B[Di].Head;
B[Di].Head = tmp;
}
/* 下面是收集的过程 */
i = j = L; //i, j记录当前要处理的A[]的左右端下标
for(Di=0;Di<Radix;Di++){ //对于每一个桶
if(B[Di].Head){ //将非空的桶整桶倒入A[],递归排序
p = B[Di].Head;
while(p){
tmp = p;
p = p->Next;
A[j++] = tmp->Key;
free(tmp);
}
/* 递归对该桶数据排序,位数-1 */
MSD( A,i,j-1,D-1);
i=j; //为下一个桶对应A[]的左端
}
}
}
.
数据8 由于给出了数据是最大999的正整数 即最大是3位数,那么可以通过LSD的基数排序(3趟桶排序)来完成;但是由于不符合M>>N ,可以预计效率不高
测试结果:
符合预期~!
f代码:
#include <stdio.h>
#include <stdlib.h>
#define MaxDigit 3
#define Radix 10
//桶元素
typedef struct _Node Node;
struct _Node{
int Element;
Node* Next;
};
//桶头结点
typedef struct _HeadNode{
Node* Head;
Node* Tail;
}HeadNode;
void Print( int A[],int N );
void LSDRadix_Sort( int A[],int N );
int GetDigit( int Element,int D );
int main()
{
int N;
scanf("%d",&N);
int i,A[N];
for(i=0;i<N;i++){
scanf("%d",&A[i]);
}
LSDRadix_Sort( A,N );
Print( A,N );
return 0;
}
void LSDRadix_Sort( int A[],int N )
{
HeadNode Bucket[Radix]; //建桶
int i;
for(i=0;i<Radix;i++){ //初始化每个桶为空链表
Bucket[i].Head = Bucket[i].Tail = NULL;
}
Node *NewNode,*List = NULL;
for(i=0;i<N;i++){
NewNode = (Node*)malloc(sizeof(Node));
NewNode->Element = A[i];
NewNode->Next = List;
List = NewNode;
}
int D,Di;
Node *p,*tmp;
for(D=1;D<=MaxDigit;D++){
p = List;
while(p){
Di = GetDigit( p->Element,D );
tmp = p;
p = p->Next;
tmp->Next = NULL;
if(Bucket[Di].Head == NULL){
Bucket[Di].Head = Bucket[Di].Tail = tmp;
}else{
Bucket[Di].Tail->Next = tmp;
Bucket[Di].Tail = tmp;
}
}
List = NULL;
for(Di=Radix-1;Di>=0;Di--){
if(Bucket[Di].Head){
Bucket[Di].Tail->Next = List;
List = Bucket[Di].Head;
Bucket[Di].Head = Bucket[Di].Tail = NULL;
}
}
}
for(i=0;i<N;i++){
tmp = List;
List = List->Next;
A[i] = tmp->Element;
free(tmp);
}
}
int GetDigit( int Element,int D )
{
int Di,i;
for(i=1;i<=D;i++){
Di = Element % Radix;
Element /= Radix;
}
return Di;
}
void Print( int A[],int N )
{
int i;
for(i=0;i<N-1;i++){
printf("%d ",A[i]);
}
printf("%d",A[N-1]);
}
▶♥ 总结:排序算法的比较
排序方法 平均时间复杂度 最坏情况下时间复杂度 额外空间复杂度 稳定性
简单选择排序 O(N^2) O(N^2) O(1) 不稳定(交换之前先做一次比较,就是稳定)
冒泡排序 O(N^2) O(N^2) O(1) 稳定
直接插入排序 O(N^2) O(N^2) O(1) 稳定
以上三种统称简单排序,程序简单好写
希尔排序 O(N^d)(d<=2) O(N^2) O(1) 不稳定
堆排序 O(NlogN) O(NlogN) O(1) 不稳定
快速排序 O(NlogN) O(N^2) O(logN) 不稳定
归并排序 O(NlogN) O(NlogN) O(N) 稳定
基数排序 O(P(N+B)) O(P(N+B)) O(N+B) 稳定