文件从逻辑上可分为排序顺序文件、一般(即非排序)顺序文件;从物理储上可分为连续文件、链接文件。(参考 文件及查找-MarchOn)
定义
将文件的记录按记录关键字值递增或递减顺序重新组织,得到有序的文件记录。通常指的是连续顺序文件的排序,当然链接顺序文件也可;当记录只包含关键字时即为元素的排序。
分类
分类法1:内排序、外排序:排序是否完全在内存进行。(外排序用于数据量大而无法一次全装入内存的数据文件的排序,通常用多路归并法)。
分类法2:连续顺序文件排序、链接顺序文件排序
分类法3:稳定排序、不稳定排序:关键字值一样的文件记录在排序前后相对位置保持不变的排序是稳定排序。
时间效率
排序过程的基本动作包括元素比较和元素移动
衡量:排序算法的时间效率主要用排序过程中元素间的比较次数来衡量。
几个下界:
1、基于元素交换进行排序的算法的平均时间复杂度下限为Ω(nlgn),可用决策树证明(n个元素有n!种排列,每确定两个数的大小关系进入一棵子树,故复杂度为 O( ln(n!) ) =O(nlgn))。显然,桶排序、基数排序不是基于元素交换的排序,故不适用此结论;
2、基于相邻元素交换进行排序的算法平均时间复杂度下限为Ω(n2)(此下界比上述的下界更紧),且是稳定的。基于相邻元素交换或顺序移动的排序算法是稳定的,如冒泡、插入(插入排序插入时本质上也是相邻元素交换)。
该下界的证明:基于交换的排序实际上就是消除序列中的逆序偶的过程,每次相邻交换最多消除一个逆序使总逆序数减1(相邻交换不会使逆序数增加,若不是相邻交换则可能会增加),而一个序列的平均逆序数为n(n-1)/4(因为一个序列L和其反序列Lr的逆序数和为n(n-1)/2),故此时为消除逆序最少要进行O(n2)次交换,即为一个下界。当然此下界对有些算法可能不够紧。
内排序
以下n为元素个数。
1、插入排序
思路:依次将元素插入到前面的已排序序列中,也称简单插入排序或直接插入排序。是稳定排序。分为顺序插入、折半插入,后者与前者相比减少了比较次数(寻找插入位置)但移动次数不变。
复杂度:时间平均O(n2)、最好O(n)、最坏O(n2);空间O(1)。
趟数:n-1。
比较次数(对顺序插入而言):序列递增时最少,为n-1;递减时最多,为n(n-1)/2。
代码(顺序插入与折半插入):(由于当前待插入元素前的序列已经有序,因此可以通过复制来实现元素后移从而避免交换,减少操作步骤)
![](https://i-blog.csdnimg.cn/blog_migrate/8f900a89c6347c561fdf2122f13be562.gif)
![](https://i-blog.csdnimg.cn/blog_migrate/961ddebeb323a10fe0623af514929fc1.gif)
1 //插入排序,依次将元素插入到元素前面的已排序序列中。 n-1趟 2 void insertSort1(int k[],int n) 3 {//顺序插入 4 int i,j,tmp; 5 for(i=1;i<n;i++) 6 { 7 tmp=k[i]; 8 for(j=i-1;j>=0 && k[j]>tmp;j--)//若为≥则非稳定 9 { 10 k[j+1]=k[j]; 11 } 12 k[j+1]=tmp; 13 } 14 } 15 16 void insertSort2(int k[],int n) 17 {//折半插入,采用折半查找应插入的位置。移动元素的次数不变,但比较次数(找元素应在的位置)少了 18 int i,j,tmp; 19 int low,mid,high; 20 for(i=1;i<n;i++) 21 { 22 tmp=k[i]; 23 24 low=0,high=i-1; 25 while(low<=high) 26 { 27 mid=low+(high-low)/2; 28 if(k[mid]>tmp) high=mid-1;//若为≥则非稳定 29 else low=mid+1; 30 } 31 32 for(j=i-1;j>=low;j--) 33 { 34 k[j+1]=k[j]; 35 } 36 k[j+1]=tmp; 37 } 38 } 39 40 insertSort
2、冒泡排序
思路:通过可能的相邻元素交换来将未排序序列的最大值交换到该序列的末尾,重复此过程直到没有发生元素交换。是稳定排序。移动元素频繁,效率低。
复杂度:时间平均O(n2)、最好O(n)、最坏O(n2);空间O(1)。
趟数:不定,[1,n-1]。
比较次数:初始序列递增时最少,为n-1,一趟完事;最小元素在末尾时最多,为n(n-1)/2,走n-1趟。
代码:
![](https://i-blog.csdnimg.cn/blog_migrate/8f900a89c6347c561fdf2122f13be562.gif)
![](https://i-blog.csdnimg.cn/blog_migrate/961ddebeb323a10fe0623af514929fc1.gif)
1 //冒泡排序,通过可能的相邻元素交换 将 未排序序列中的最大值交换到该未排序序列的末尾,直到没有发生元素交换。 序列升序时1趟、最小值在末尾时n-1趟。 2 void bubbleSort(int k[],int n) 3 { 4 int i,j,tmp; 5 int flag;//可以简单暴力地进行n-1趟冒泡完成排序,但实际上若一趟冒泡下来没有相邻元素交换,则说明已有序,没必要继续后续趟的冒泡了。flag即用来达到此目的,用于标记上趟是否有相邻元素交换。 6 7 j=n-1;//j为未排序序列的末元素位置 8 do 9 { 10 flag=0; 11 for(i=0;i<j;i++) 12 { 13 if(k[i]>k[i+1])//若为≥则非稳定 14 { 15 flag=1; 16 17 tmp=k[i]; 18 k[i]=k[i+1]; 19 k[i+1]=tmp; 20 } 21 } 22 j--; 23 }while(flag==1); 24 } 25 26 bubbleSort
3、选择排序
思路:依次从未排序序列选出最大元素放到该序列末尾。非稳定排序。
复杂度:时间平均O(n2)、最好(n2)、最坏O(n2),即与序列初始状态无关;空间O(1)。
趟数:n-1。
比较次数:n(n-1)/2。
代码:
![](https://i-blog.csdnimg.cn/blog_migrate/8f900a89c6347c561fdf2122f13be562.gif)
![](https://i-blog.csdnimg.cn/blog_migrate/961ddebeb323a10fe0623af514929fc1.gif)
1 //选择排序,依次从未排序序列中选择最大元素放到该未排序序列的末尾。n-1趟 2 void selectSort(int k[],int n) 3 { 4 int i,j,tmp; 5 int d;//标记未排序序列中最大值元素的位置 6 for(j=n-1;j>0;j--)//j为未排序序列末元素位置 7 { 8 d=0; 9 for(i=1;i<=j;i++) 10 { 11 if(k[i]>k[d]) d=i; 12 } 13 if(d!=j) 14 { 15 tmp=k[d]; 16 k[d]=k[j]; 17 k[j]=tmp; 18 } 19 } 20 } 21 22 selectSort
4、希尔排序
思路:对直接插入排序的改进。也叫缩小增量排序法。是非稳定排序。希尔排序算法实现简单且性能又比上述一般算法好,因此很适用于适当大量输入数据的排序。
gap为一个递减的增量序列:ht、ht-1、...、h2、h1、1。每趟以gap值hk为间距将序列分为hk个独立子序列(相距hk的元素属同一个子序列),该趟的排序效果为使得每个子序列内有序即对于每个i有k[i]≤k[i+hk],子序列的排序可以选用 插入排序、泡排序等。在很多情况下,gap为1时序列几乎已经有序,使得不需要进行较多元素的移动就能达到排序目的。
复杂度:时间平均O(nlgn)、最好O(nlgn)、最坏O(n2);空间O(1)。
选择不同增量序列时,最坏时间复杂度可能有所改善:为折半gap序列时最坏时间复杂度为O(n2);为奇数gap序列时最坏时间复杂度为O(n1.5);目前最好的Sedgewick序列为O(n7/6)。
趟数:增量序列的长度,具体取决于增量序列的选择。因此是不定的。若采用折半gap序列则趟数为ceil(lg2n)趟。
代码(这里子序列的排序分别用泡排序、插入排序,推荐用后者):
![](https://i-blog.csdnimg.cn/blog_migrate/8f900a89c6347c561fdf2122f13be562.gif)
![](https://i-blog.csdnimg.cn/blog_migrate/961ddebeb323a10fe0623af514929fc1.gif)
1 //希尔排序,对直接插入排序的改进。每趟以gap为间距将序列分为gap个独立子序列(相距gap的元素属同一个子序列),该趟的排序效果为使得每个子序列内有序。gap从大到小,最后为1;子序列的排序可以选用 泡排序、选择排序等 2 void shellSort(int k[],int n) 3 { 4 int i,j,tmp,flag; 5 int gap; 6 for(gap=n/2;gap>0;gap/=2) 7 { 8 do//这里子序列内的排序采用泡排序 9 { 10 flag=0; 11 for(i=0;i<n-gap;i++) 12 { 13 if(k[i]>k[i+gap]) 14 { 15 flag=1; 16 17 tmp=k[i]; 18 k[i]=k[i+gap]; 19 k[i+gap]=tmp; 20 } 21 } 22 }while(flag==1); 23 } 24 } 25 26 void shellSort(int k[],int n) 27 { 28 int i,j,tmp; 29 int gap; 30 for(gap=n/2;gap>0;gap/=2) 31 {//这里子序列内的排序采用插入排序 32 for(i=gap;i<n;i++) 33 { 34 tmp=k[i]; 35 for(j=i-gap;j>=0 && k[j]>tmp;j-=gap) 36 { 37 k[j+gap]=k[j]; 38 } 39 k[j+gap]=tmp; 40 } 41 } 42 }
5、快速排序
思路:对冒泡排序的改进。选个枢纽元,将小于该元素的元素移到左边、大于的移到右边,此过程为一次划分;然后以枢纽元为界对两个子序列递归快排。非稳定排序。
复杂度:
平均时间复杂度:O(nlgn),只要每次枢纽元的选取原则一样(如都取首元素或者都取中间元素),平均时间复杂度都为此。
最好时间复杂度:O(nlgn)。
最坏时间复杂度:与划分的结果有关,而划分的结果与枢纽元的选取原则有关。
若每次划分后长子序列的规模都缩小为原来的α倍(0<α<1,每次值可不一样),则最坏时间复杂度为O(nlgn)。如每次都取序列的中位数作为枢纽元,此时子序列均为原序列的一半规模。
最坏情形下每次划分都使得两个子序列规模分别为1、n-1,则最坏时间复杂度为O(n2)。如序列初始有序时此时退化为冒泡排序,时间复杂度为O(n2)。
空间复杂度:O(lgn),一次划分时空间复杂度为O(1),递归使得增长到O(lgn)。
枢纽元的选择:(只要每次划分时枢纽元的选取原则一样(如都取首元素或都取中间元素),平均复杂度都为O(nlgn),但最坏复杂度则与枢纽元的选取有关,如选首元素最坏O(n2)而选中位数最坏O(nlgn);除非题特别说明,否则一般指的是以首元素为枢纽元)。
首元素或随机选择方案(随机选一个元素并交换到首元素,然后按首元素为枢纽元的情况处理)均不好,前者在序列初始有序时恶化成O(n2)、后者随机法耗时且不能保证总是划分成对称大小的两个子序列因此最坏也为O(n2);
选中位数总能将元素划分成两半,可以在O(n)时间内完成(相当于选第n/2大),因此整个排序复杂度最坏也为O(nlgn),但选中位数较麻烦;
实际应用中一般用 首、末、中心 三元素的中值。
趟数:不定。
代码(两种实现方法,如果要求用链表来实现快排,则第二种可以改造成链表版本):(可用宏定义实现交换两元素,避免函数调用实现方法的开销: #define swap(T, a, b) { T c=a; a=b; b=c;} )
![](https://i-blog.csdnimg.cn/blog_migrate/8f900a89c6347c561fdf2122f13be562.gif)
![](https://i-blog.csdnimg.cn/blog_migrate/961ddebeb323a10fe0623af514929fc1.gif)
1 //快排,对冒泡排序的改进 。选择一个枢纽元,然后将小于枢纽元的元素整理到其左边、大于的整理到右边。 2 void quickSort1(int k[],int s,int t)//s、t分别为起点、终点元素的下标 3 {//这里以首元素作枢纽元 4 if(s>=t)return ; 5 int i,j; 6 i=s;//初始指向第一个元素 7 j=t+1;//初始指向末元素的后一个位置 8 while(1) 9 { 10 do{i++;}while(!(i==t || k[i]>=k[s])); 11 // while( k[++i]<k[s] && i<t);//与上句等价。由于到这里s肯定小于t所以while里的两个表达式谁前谁后没影响,下同。 12 13 do{j--;}while(!(j==s || k[j]<=k[s])); 14 // while(k[--j]>k[s] && j>s);//与上句等价 15 16 if(i<j) 17 { 18 swapForQuickSort(&k[j],&k[i]); 19 } 20 else break; 21 } 22 swapForQuickSort(&k[j],&k[s]); 23 quickSort1(k,s,j-1); 24 quickSort1(k,j+1,t); 25 } 26 27 void quickSort2(int k[],int s,int t) 28 {//这里以首元素作为枢纽元 29 if(s>=t) return; 30 int i=s,j=s; 31 while(++i <=t)//小于枢纽元的放到枢纽元的后面 32 { 33 if(k[i]<k[s]) 34 { 35 swapForQuickSort(&k[i],&k[++j]); 36 } 37 } 38 swapForQuickSort(&k[j],&k[s]); 39 quickSort2(k,s,j-1); 40 quickSort2(k,j+1,t); 41 } 42 43 quickSort
可以指定枢纽元选取原则的实现方法(与上面的两种对应):
![](https://i-blog.csdnimg.cn/blog_migrate/8f900a89c6347c561fdf2122f13be562.gif)
![](https://i-blog.csdnimg.cn/blog_migrate/961ddebeb323a10fe0623af514929fc1.gif)
1 void quickSort1_pivot(int k[],int s,int t) 2 {//可以自定义枢纽元 3 if(s>=t) return; 4 5 int pivot=s+(t-s+1)/3;//指定枢纽元 6 int i=s-1;//初始化使得遍历所有元素 7 int j=t+1; 8 while(1) 9 { 10 do {i++;} while(!(i==t || k[i]>=k[pivot])); 11 // while(k[++i]<k[pivot] && i<t);//与上句等价 12 13 do {j--;} while(!(j==s || k[j]<=k[pivot])); 14 // while(k[--j]>k[pivot] && j>s);//与上句等价 15 16 if(i<j) 17 { 18 swapForQuickSort(&k[j],&k[i]); 19 if(i==pivot) pivot=j; 20 else if(j==pivot) pivot=i; 21 } 22 else break; 23 } 24 //循环结束后i=j+1,pivot所在元素应该与i、j离pivot近者交换 25 //因为若pivot≥i,由于k[i]是从前往后k[s,...,i]中第一个≥k[pivot]的,则k[s,...,i-1]<k[pivot],故交换k[i]、k[pivot]; 26 //若pivot<i即pivot≤j ,由于k[j]是从后往前k[t,...,j]中第一个≤k[pivot]的,则k[j+1,...,t]>k[pivot],故交换k[j]、k[pivot] 27 if(pivot>=i) 28 { 29 j=i; 30 } 31 32 swapForQuickSort(&k[pivot],&k[j]); 33 quickSort1_pivot(k,s,j-1); 34 quickSort1_pivot(k,j+1,t); 35 } 36 37 void quickSort2_pivot(int k[],int s,int t) 38 {//可以自定义枢纽元 39 if(s>=t) return; 40 41 int pivot=s+(t-s+1)/3;//指定枢纽元 42 int i=s-1;//初始化使得遍历所有元素 43 int j=s-1; 44 while(++i <=t)//小于枢纽元的放到数组前部分 45 { 46 if(k[i]<k[pivot]) 47 { 48 swapForQuickSort(&k[i],&k[++j]); 49 if(pivot==j)pivot=i;//由于比k[pivot]小的才会交换,故被交换的k[i]不会是k[pivot] 50 } 51 } 52 swapForQuickSort(&k[pivot],&k[++j]); 53 54 quickSort2_pivot(k,s,j-1); 55 quickSort2_pivot(k,j+1,t); 56 }
模板:关键在于划分函数Partition的实现。
1 void quickSort(int *data,int p,int r) 2 { 3 //调用者应该确保p<r 4 if(p>=r) return; 5 int q=Partition(data,p,r);//Partition选一个基准元将data划分为左右两部分。不同的Partition实现可以实现不同的快排,如随机快排等 6 quickSort(data,p,q-1);//对左部分排序 7 quickSort(data,q+1,r);//对右部分排序 8 }
以下是几种Partition实现:
![](https://i-blog.csdnimg.cn/blog_migrate/8f900a89c6347c561fdf2122f13be562.gif)
![](https://i-blog.csdnimg.cn/blog_migrate/961ddebeb323a10fe0623af514929fc1.gif)
1 //Partition的一种实现。以首元素为枢纽元 2 int Partition(int *data,int p,int r) 3 { 4 int i=p,j=r+1; 5 while(1) 6 { 7 while(data[++i]<data[p] && i<r); 8 while(data[--j]>data[p] && j>p); 9 if(i<j) swapForQuickSort(&data[i],&data[j]); 10 else break; 11 } 12 swapForQuickSort(&data[p],&data[j]); 13 return j; 14 } 15 16 //Partition的一种实现。随机划分算法 17 int RandomPartition(int *data,int p,int r) 18 { 19 int i=Random(p,r);//随机选一个元素作为枢纽元 20 swapForQuickSort(&data[p],&data[i]);//交换到首位 21 return Partition(data,p,r); 22 }
拓展:基于此划分思想可以解决很多问题,如从序列选第k小元素,按此方法其平均复杂度为O(n),关键也在于Partition方法——只有保证每次划分出的两个子序列的规模至少为原来的α倍(0<α<1)才能保证最坏也为T(n)=T(αn)+O(n)=O(n)。
6、堆排序
思路:对选择排序的改进。元素用完全二叉树的数组形式存储,走n-1趟。升序排用大顶堆、降序排用小顶堆,以大顶堆为例第i趟排序就是把前n+1-i个元素组成大顶堆并把堆的首末元素交换。非稳定排序。
复杂度:时间平均O(nlgn),最好、最坏均为此;空间O(1)。
趟数:n-1。
代码:建初始堆(复杂度O(n)); 然后将堆顶元素与堆尾元素交换并维护堆的性质(维护的复杂度O(lgn)),如此往复n-1次即可( 复杂度为 n*O(lgn)=O(nlgn) )。P344
![](https://i-blog.csdnimg.cn/blog_migrate/8f900a89c6347c561fdf2122f13be562.gif)
![](https://i-blog.csdnimg.cn/blog_migrate/961ddebeb323a10fe0623af514929fc1.gif)
1 //堆排序,对选择排序的改进。n-1趟,以大顶堆为例,第i趟将前n+1-i个元素组成大顶堆并把堆顶元素与末元素交换。 2 void adjust(int k[],int n,int i)//元素采用完全二叉树形式存储在k[]。调整以i为堆顶的大顶堆。时间复杂度O(lgn) 3 { 4 int tmp=k[i]; 5 int j;//j指向左右孩子较大者 6 7 j=2*i+1; 8 while(j<n) 9 { 10 if(j<n-1 && k[j]<k[j+1]) j++; 11 if(tmp<k[j]) 12 { 13 k[(j-1)/2]=k[j]; 14 j=2*j+1; 15 } 16 else 17 { 18 break; 19 } 20 } 21 k[(j-1)/2]=tmp;//上面的循环有可能因为j>=n而退出而非因else而退出,所以此句不能放else里 22 } 23 void heapSort(int k[],int n) 24 { 25 int i,tmp; 26 for(i=(n-1)/2;i>=0;i--)//建立初始堆:从完全二叉树的最后一个分支节点开始,从下往上从右往左调整以该分支节点为根节点的堆。 时间复杂度O(n) 27 { 28 adjust(k,n,i); 29 } 30 for(i=n-1;i>=1;i--)//进行n-1趟,每趟将首元素交换到未排序序列末尾i,然后序列长度减1并调整堆。 数据复杂度O(nlgn) 31 { 32 tmp=k[0]; 33 k[0]=k[i]; 34 k[i]=tmp; 35 adjust(k,i,0); 36 } 37 } 38 39 heapSort
时间复杂度分析:堆维护一次的复杂度为元素最大可能移动次数,即树高,所以为O(lgn)。建初堆复杂度为 调整以每个分支节点为根节点的子树时的最大可能移动次数 的和,可以假定完全二叉树是满的,则易求出和为O(n)。
其他:上面提到了堆的建立和维护,其实质是堆顶元素的“下沉”,除之外,堆的操作还有删除、插入操作:删除(即删除堆顶元素)通常是直接将堆末元素放到堆顶然后对之维护,也是“下沉”;插入通常是将元素插入到序列末尾然后将该元素“上浮”。
其他:堆的应用
优先队列
从n个元素里选m个最小的元素:建m个元素的大顶堆,对于后面到来的元素,若小于堆顶元素则替换堆顶元素然后维护大顶堆性质、否则忽略该元素。(若选m个最大,则建小顶堆)。
排序:升序大顶堆、降序小顶堆。
7、二路归并排序
也叫合并排序。
思路:每趟依次将相邻的一对子序列归并为一个更大的子序列,子序列的长度从1开始每趟逐渐加倍,进行ceil(lgn)趟。稳定排序。
复杂度:时间平均O(nlgn),最好、最坏均为此;空间O(n)。空间复杂度最高,所以主要用于外部排序而很难用于内排序。
趟数:ceil(lgn)
代码:
递归版:
![](https://i-blog.csdnimg.cn/blog_migrate/8f900a89c6347c561fdf2122f13be562.gif)
![](https://i-blog.csdnimg.cn/blog_migrate/961ddebeb323a10fe0623af514929fc1.gif)
1 //合并排序的子函数,用于将连个排好序的序列合并成一个有序序列 2 void merge(int from[],int to[],int s,int m,int e) 3 { 4 int i=s,j=m+1,k=s; 5 while(i<=m && j<=e) 6 { 7 if(from[i]<=from[j]) to[k++]=from[i++]; 8 else to[k++]=from[j++]; 9 } 10 while(i<=m) to[k++]=from[i++]; 11 while(j<=e) to[k++]=from[j++]; 12 } 13 14 15 //合并排序递归版 16 void mergeSort_recursive(int *data,int *tmp,int s,int e)//tmp为合并时临时使用的辅助空间 17 { 18 if(s>=e) return; 19 int m=s+(e-s)/2; 20 mergeSort_recursive(data,tmp,s,m); 21 mergeSort_recursive(data,tmp,m+1,e); 22 23 merge(data,tmp,s,m,e);//合并到数组tmp 24 int i=s; 25 while(i<=e) {data[i]=tmp[i];i++;} //复制回数组a 26 }
非递归版:
![](https://i-blog.csdnimg.cn/blog_migrate/8f900a89c6347c561fdf2122f13be562.gif)
![](https://i-blog.csdnimg.cn/blog_migrate/961ddebeb323a10fe0623af514929fc1.gif)
1 //合并排序的子函数,用于将连个排好序的序列合并成一个有序序列 2 void merge(int from[],int to[],int s,int m,int e) 3 { 4 int i=s,j=m+1,k=s; 5 while(i<=m && j<=e) 6 { 7 if(from[i]<=from[j]) to[k++]=from[i++]; 8 else to[k++]=from[j++]; 9 } 10 while(i<=m) to[k++]=from[i++]; 11 while(j<=e) to[k++]=from[j++]; 12 } 13 14 15 //data中有一系列已排好序的大小为size的相邻子数组。将每相邻的两个合并为一个,存入tmp 16 void mergePass(int *data,int *tmp,int size,int n) 17 { 18 int i=0; 19 while(i <= n-2*size) 20 { 21 merge(data,tmp,i,i+size-1,i+2*size-1);//合并大小为size的相邻2个有序子数组到tmp 22 i+= 2*size; 23 } 24 //剩下元素个数少于2*size 25 if(i<n-size) merge(data,tmp,i,i+size-1,n-1); 26 else for(;i<n;i++) tmp[i]=data[i];//merge(data,tmp,i,n-1,n-1); 27 } 28 //合并排序非递归版 29 void mergeSort(int *data,int n) 30 { 31 int tmp[n]; 32 int size=1;//子数组大小 33 while(size<n) 34 { 35 mergePass(data,tmp,size,n); 36 size+=size; 37 mergePass(tmp,data,size,n); 38 size+=size; 39 } 40 }
其他:自然合并排序:扫描一趟得到初始有序的各子序列,然后对之进行若干次合并,与上述归并排序相比能够减少合并次数。在初始有序时时间复杂度为O(n),而上述方法仍需要O(nlgn)。
8、基数排序
思路:一个数的序列,每个数为d位r进制数,从末位到首位每次以该位数为关键字进行排序,进行d趟。稳定排序。
复杂度:时间平均O(d(r+n)),最好、最坏均如是; 空间O(r+n)。实际上复杂度与实现有关,这里对P349的实现而言。
趟数:d
是桶排序的扩展,桶排序:设m个桶(m≥n),采用相同原则将每个元素对应到某个桶(如对应到元素值与桶下标值相等的桶中),然后顺序遍历桶即得到排序序列。时间复杂度为O(m+n)、空间复杂度为O(m)。
以上的所有代码:
![](https://i-blog.csdnimg.cn/blog_migrate/8f900a89c6347c561fdf2122f13be562.gif)
![](https://i-blog.csdnimg.cn/blog_migrate/961ddebeb323a10fe0623af514929fc1.gif)
1 #include<stdio.h> 2 3 4 5 //插入排序,依次将元素插入到元素前面的已排序序列中。通过复制实现元素后移以避免明显地使用交换。 n-1趟 6 void insertSort1(int k[],int n) 7 {//顺序插入 8 int i,j,tmp; 9 for(i=1;i<n;i++) 10 { 11 tmp=k[i]; 12 for(j=i-1;j>=0 && k[j]>tmp;j--) 13 { 14 k[j+1]=k[j]; 15 } 16 k[j+1]=tmp; 17 } 18 } 19 void insertSort2(int k[],int n) 20 {//折半插入,采用折半查找应插入的位置。移动元素的次数不变,但比较次数(找元素应在的位置)少了 21 int i,j,tmp; 22 int low,mid,high; 23 for(i=1;i<n;i++) 24 { 25 tmp=k[i]; 26 27 low=0,high=i-1; 28 while(low<=high) 29 { 30 mid=low+(high-low)/2; 31 if(k[mid]>tmp) high=mid-1; 32 else low=mid+1; 33 } 34 35 for(j=i-1;j>=low;j--) 36 { 37 k[j+1]=k[j]; 38 } 39 k[j+1]=tmp; 40 } 41 } 42 43 44 //冒泡排序,通过可能的相邻元素交换 将 未排序序列中的最大值交换到该未排序序列的末尾,直到没有发生元素交换。 序列升序时1趟、最小值在末尾时n-1趟。 45 void bubbleSort(int k[],int n) 46 { 47 int i,j,tmp; 48 int flag;//可以简单暴力地进行n-1趟冒泡完成排序,但实际上若一趟冒泡下来没有相邻元素交换,则说明已有序,没必要继续后续趟的冒泡了。flag即用来达到此目的,用于标记上趟是否有相邻元素交换。 49 50 j=n-1;//j为未排序序列的末元素位置 51 do 52 { 53 flag=0; 54 for(i=0;i<j;i++) 55 { 56 if(k[i]>k[i+1]) 57 { 58 flag=1; 59 60 tmp=k[i]; 61 k[i]=k[i+1]; 62 k[i+1]=tmp; 63 } 64 } 65 j--; 66 }while(flag==1); 67 } 68 69 //选择排序,依次从未排序序列中选择最大元素放到该未排序序列的末尾。n-1趟 70 void selectSort(int k[],int n) 71 { 72 int i,j,tmp; 73 int d;//标记未排序序列中最大值元素的位置 74 for(j=n-1;j>=0;j--)//j为未排序序列末元素位置 75 { 76 d=0; 77 for(i=1;i<=j;i++) 78 { 79 if(k[i]>k[d]) d=i; 80 } 81 if(d!=j) 82 { 83 tmp=k[d]; 84 k[d]=k[j]; 85 k[j]=tmp; 86 } 87 } 88 } 89 90 91 void swapForQuickSort(int *a,int *b) 92 { 93 int tmp=*a; 94 *a=*b; 95 *b=tmp; 96 } 97 //希尔排序,对直接插入排序的改进。每趟以gap为间距将序列分为gap个独立子序列(相距gap的元素属同一个子序列),该趟的排序效果为使得每个子序列内有序。gap从大到小,最后为1;子序列的排序可以选用 泡排序、选择排序等 98 void shellSort(int k[],int n) 99 { 100 int i,j,tmp,flag; 101 int gap; 102 for(gap=n/2;gap>0;gap/=2) 103 { 104 do//这里子序列内的排序采用泡排序 105 { 106 flag=0; 107 for(i=0;i<n-gap;i++) 108 { 109 if(k[i]>k[i+gap]) 110 { 111 flag=1; 112 113 tmp=k[i]; 114 k[i]=k[i+gap]; 115 k[i+gap]=tmp; 116 } 117 } 118 }while(flag==1); 119 } 120 } 121 122 void shellSort2(int k[],int n) 123 { 124 int i,j,tmp; 125 int gap; 126 for(gap=n/2;gap>0;gap/=2) 127 {//这里子序列内的排序采用插入排序 128 for(i=gap;i<n;i++) 129 { 130 tmp=k[i]; 131 for(j=i-gap;j>=0 && k[j]>tmp;j-=gap) 132 { 133 k[j+gap]=k[j]; 134 } 135 k[j+gap]=tmp; 136 } 137 } 138 } 139 140 141 void quickSort(int *data,int p,int r) 142 { 143 if(p>=r) return; 144 int q=Partition(data,p,r);//Partition选一个基准元将data划分为左右两部分。不同的Partition实现可以实现不同的快排,如随机快排等 145 quickSort(data,p,q-1);//对左部分排序 146 quickSort(data,q+1,r);//对右部分排序 147 } 148 149 //Partition的一种实现。不同的Partition实现可以实现不同的快排,如随机快排等 150 int Partition(int *data,int p,int r) 151 { 152 //调用者应该确保p<r 153 int i=p,j=r+1; 154 while(1) 155 { 156 while(data[++i]<data[p] && i<r); 157 while(data[--j]>data[p] && j>p); 158 if(i<j) swapForQuickSort(&data[i],&data[j]); 159 else break; 160 } 161 swapForQuickSort(&data[p],&data[j]); 162 return j; 163 } 164 165 166 //快排,对冒泡排序的改进 。选择一个枢纽元,然后将小于枢纽元的元素整理到其左边、大于的整理到右边。 167 void quickSort1(int k[],int s,int t)//s、t分别为起点、终点元素的下标 168 {//这里以首元素作枢纽元 169 if(s>=t)return ; 170 int i,j; 171 i=s;//初始指向第一个元素 172 j=t+1;//初始指向末元素的后一个位置 173 while(1) 174 { 175 do{i++;}while(!(i==t || k[i]>=k[s])); 176 // while( k[++i]<k[s] && i<t);//与上句等价.由于到这里s肯定小于t,所以两个表达式谁前谁后没影响。但最好先检查边界以免越界: while(++i<t && k[i]<k[s]) 177 178 do{j--;}while(!(j==s || k[j]<=k[s])); 179 // while(k[--j]>k[s] && j>s);//与上句等价 180 181 if(i<j) 182 { 183 swapForQuickSort(&k[j],&k[i]); 184 } 185 else break; 186 } 187 swapForQuickSort(&k[j],&k[s]); 188 quickSort1(k,s,j-1); 189 quickSort1(k,j+1,t); 190 } 191 void quickSort1_pivot(int k[],int s,int t) 192 {//可以自定义枢纽元 193 if(s>=t) return; 194 195 int pivot=s+(t-s+1)/3;//指定枢纽元 196 int i=s-1;//初始化使得遍历所有元素 197 int j=t+1; 198 while(1) 199 { 200 do {i++;} while(!(i==t || k[i]>=k[pivot])); 201 // while(k[++i]<k[pivot] && i<t);//与上句等价 202 203 do {j--;} while(!(j==s || k[j]<=k[pivot])); 204 // while(k[--j]>k[pivot] && j>s);//与上句等价 205 206 if(i<j) 207 { 208 swapForQuickSort(&k[j],&k[i]); 209 if(i==pivot) pivot=j; 210 else if(j==pivot) pivot=i; 211 } 212 else break; 213 } 214 //循环结束后i=j+1,pivot所在元素应该与i、j离pivot近者交换 215 //因为若pivot≥i,由于k[i]是从前往后k[s,...,i]中第一个≥k[pivot]的,则k[s,...,i-1]<k[pivot],故交换k[i]、k[pivot]; 216 //若pivot<i即pivot≤j ,由于k[j]是从后往前k[t,...,j]中第一个≤k[pivot]的,则k[j+1,...,t]>k[pivot],故交换k[j]、k[pivot] 217 if(pivot>=i) 218 { 219 j=i; 220 } 221 222 swapForQuickSort(&k[pivot],&k[j]); 223 quickSort1_pivot(k,s,j-1); 224 quickSort1_pivot(k,j+1,t); 225 } 226 227 228 void quickSort2(int k[],int s,int t) 229 {//这里以首元素作为枢纽元 230 if(s>=t) return; 231 int i=s,j=s; 232 while(++i <=t)//小于枢纽元的放到枢纽元的后面 233 { 234 if(k[i]<k[s]) 235 { 236 swapForQuickSort(&k[i],&k[++j]); 237 } 238 } 239 swapForQuickSort(&k[j],&k[s]); 240 quickSort2(k,s,j-1); 241 quickSort2(k,j+1,t); 242 } 243 void quickSort2_pivot(int k[],int s,int t) 244 {//可以自定义枢纽元 245 if(s>=t) return; 246 247 int pivot=s+(t-s+1)/3;//指定枢纽元 248 int i=s-1;//初始化使得遍历所有元素 249 int j=s-1; 250 while(++i <=t)//小于枢纽元的放到数组前部分 251 { 252 if(k[i]<k[pivot]) 253 { 254 swapForQuickSort(&k[i],&k[++j]); 255 if(pivot==j)pivot=i;//由于比k[pivot]小的才会交换,故被交换的k[i]不会是k[pivot] 256 } 257 } 258 swapForQuickSort(&k[pivot],&k[++j]); 259 260 quickSort2_pivot(k,s,j-1); 261 quickSort2_pivot(k,j+1,t); 262 } 263 264 265 266 //堆排序,对选择排序的改进。n-1趟,以大顶堆为例,第i趟将前n+1-i个元素组成大顶堆并把堆顶元素与末元素交换。 267 void adjust(int k[],int n,int i)//元素采用完全二叉树存储在k[]。调整以i为堆顶的大顶堆。时间复杂度O(lgn) 268 { 269 int tmp=k[i]; 270 int j;//j指向左右孩子较大者 271 272 j=2*i+1; 273 while(j<n) 274 { 275 if(j<n-1 && k[j]<k[j+1]) j++; 276 if(tmp<k[j]) 277 { 278 k[(j-1)/2]=k[j]; 279 j=2*j+1; 280 } 281 else 282 { 283 break; 284 } 285 } 286 k[(j-1)/2]=tmp;//上面的循环有可能因为j>=n而退出而非因else而退出,所以此句不能放else里 287 } 288 void heapSort(int k[],int n) 289 { 290 int i,tmp; 291 for(i=(n-2)/2;i>=0;i--)//建立初始堆:从完全二叉树的最后一个分支节点开始,从下往上从右往左调整以该分支节点为根节点的堆。 时间复杂度O(n) 292 { 293 adjust(k,n,i); 294 } 295 for(i=n-1;i>=1;i--)//进行n-1趟,每趟将首元素交换到未排序序列末尾i,然后序列长度减1并调整堆。 数据复杂度O(nlgn) 296 { 297 tmp=k[0]; 298 k[0]=k[i]; 299 k[i]=tmp; 300 adjust(k,i,0); 301 } 302 } 303 304 305 //合并排序的子函数,用于将连个排好序的序列合并成一个有序序列 306 void merge(int from[],int to[],int s,int m,int e) 307 { 308 int i=s,j=m+1,k=s; 309 while(i<=m && j<=e) 310 { 311 if(from[i]<=from[j]) to[k++]=from[i++]; 312 else to[k++]=from[j++]; 313 } 314 while(i<=m) to[k++]=from[i++]; 315 while(j<=e) to[k++]=from[j++]; 316 } 317 318 319 //合并排序递归版 320 void mergeSort_recursive(int *data,int *tmp,int s,int e)//tmp为合并时临时使用的辅助空间 321 { 322 if(s>=e) return; 323 int m=s+(e-s)/2; 324 mergeSort_recursive(data,tmp,s,m); 325 mergeSort_recursive(data,tmp,m+1,e); 326 327 merge(data,tmp,s,m,e);//合并到数组tmp 328 int i=s; 329 while(i<=e) {data[i]=tmp[i];i++;} //复制回数组a 330 } 331 332 //data中有一系列已排好序的大小为size的相邻子数组。将每相邻的两个合并为一个,存入tmp 333 void mergePass(int *data,int *tmp,int size,int n) 334 { 335 int i=0; 336 while(i <= n-2*size) 337 { 338 merge(data,tmp,i,i+size-1,i+2*size-1);//合并大小为size的相邻2个有序子数组到tmp 339 i+= 2*size; 340 } 341 //剩下元素个数少于2*size 342 if(i<n-size) merge(data,tmp,i,i+size-1,n-1); 343 else 344 { 345 // for(;i<n;i++) tmp[i]=data[i]; 346 merge(data,tmp,i,n-1,n-1); 347 } 348 } 349 //合并排序非递归版 350 void mergeSort(int *data,int n) 351 { 352 int tmp[n]; 353 int size=1;//子数组大小 354 while(size<n) 355 { 356 mergePass(data,tmp,size,n); 357 size+=size; 358 mergePass(tmp,data,size,n); 359 size+=size; 360 } 361 } 362 363 364 365 //将奇数移到左边,偶数移到右边 366 void arrangeOf2Sets(int k[],int n,int isStable) 367 {//约定i为当前元素位置,s之前的元素为奇数(不包括s) 368 int i,s,tmp; 369 s=0; 370 if(isStable) 371 {//稳定的排序 372 int t; 373 for(i=0;i<n;i++) 374 { 375 if(k[i] & 1==1) 376 {//后移为k[i] 腾出位置 377 for(tmp=k[i],t=i;t>s;t--) 378 { 379 k[t]=k[t-1]; 380 } 381 k[s]=tmp; 382 ++s; 383 } 384 } 385 } 386 else 387 {//非稳定排序 388 for(i=0;i<n;i++) 389 { 390 if(k[i] & 1==1) 391 {//直接交换 392 tmp=k[s]; 393 k[s]=k[i]; 394 k[i]=tmp; 395 ++s; 396 } 397 } 398 } 399 } 400 401 void arrangeOf3Sets(int k[],int n) 402 {//约定s之前的元素均为正数,e之后的元素均为负数,i为当前元素位置 403 int s,e,i,tmp; 404 s=0; 405 e=n-1; 406 407 i=s; 408 while(i<=e) 409 { 410 if(k[i]>0) 411 { 412 tmp=k[i]; 413 k[i]=k[s]; 414 k[s]=tmp; 415 s++; 416 } 417 else if(k[i]==0) 418 { 419 i++; 420 } 421 else 422 { 423 tmp=k[i]; 424 k[i]=k[e]; 425 k[e]=tmp; 426 e--; 427 } 428 } 429 } 430 431 void printAry(int k[],int n) 432 { 433 int i; 434 for(i=0;i<n;i++) 435 { 436 printf("%d ",k[i]); 437 } 438 printf("\n"); 439 } 440 441 442 443 444 int main() 445 { 446 int k1[]={1,2,3,4,5}; 447 int k2[]={5,4,3,2,1}; 448 int k3[]={5,1,4,3,2}; 449 int k4[]={1}; 450 int n=sizeof(k1)/sizeof(int); 451 452 453 //插入排序 454 // insertSort1(k1,n); 455 // insertSort1(k2,n); 456 // insertSort1(k3,n); 457 // insertSort1(k4,1); 458 459 // insertSort2(k1,n); 460 // insertSort2(k2,n); 461 // insertSort2(k3,n); 462 // insertSort2(k4,1); 463 464 465 466 //冒泡排序 467 // bubbleSort(k1,n); 468 // bubbleSort(k2,n); 469 // bubbleSort(k3,n); 470 // bubbleSort(k4,1); 471 472 473 474 //选择排序 475 // selectSort(k1,n); 476 // selectSort(k2,n); 477 // selectSort(k3,n); 478 // selectSort(k4,1); 479 480 481 482 //希尔排序 483 // shellSort(k1,n); 484 // shellSort(k2,n); 485 // shellSort(k3,n); 486 // shellSort(k4,1); 487 488 // shellSort2(k1,n); 489 // shellSort2(k2,n); 490 // shellSort2(k3,n); 491 // shellSort2(k4,1); 492 493 494 495 //快排 496 497 // quickSort1(k1,0,n-1); 498 // quickSort1(k2,0,n-1); 499 // quickSort1(k3,0,n-1); 500 // quickSort1(k4,0,0); 501 502 // quickSort2(k1,0,n-1); 503 // quickSort2(k2,0,n-1); 504 // quickSort2(k3,0,n-1); 505 // quickSort2(k4,0,0); 506 507 // quickSort1_pivot(k1,0,n-1); 508 // quickSort1_pivot(k2,0,n-1); 509 // quickSort1_pivot(k3,0,n-1); 510 // quickSort1_pivot(k4,0,0); 511 512 // quickSort2_pivot(k1,0,n-1); 513 // quickSort2_pivot(k2,0,n-1); 514 // quickSort2_pivot(k3,0,n-1); 515 // quickSort2_pivot(k4,0,0); 516 517 518 519 //堆排序 520 // heapSort(k1,n); 521 // heapSort(k2,n); 522 // heapSort(k3,n); 523 // heapSort(k4,1); 524 525 526 //归并排序 527 // int tmp[n]; 528 // mergeSort_recursive(k1,tmp,0,n-1); 529 // mergeSort_recursive(k2,tmp,0,n-1); 530 // mergeSort_recursive(k3,tmp,0,n-1); 531 // mergeSort_recursive(k4,tmp,0,0); 532 533 mergeSort(k1,n); 534 mergeSort(k2,n); 535 mergeSort(k3,n); 536 mergeSort(k4,1); 537 538 539 //奇数左边偶数右边 540 // arrangeOf2Sets(k3,n,0); 541 542 //正数左边、0中间、负数右边 543 // int d[]={-1,0,2,-2,4,-5,9,0}; 544 // arrangeOf3Sets(d,8); 545 // printAry(d,8); 546 547 548 printAry(k1,n); 549 printAry(k2,n); 550 printAry(k3,n); 551 printAry(k4,1); 552 return 0; 553 }
9、计数排序(枚举排序、秩排序)
思路:对每一个要排序的元素,统计小于它的所有元素的个数,从而得到该元素在整个序列中的位置。n趟。稳定排序(跟实现有关)。
复杂度:时间平均O(n2)、最好均如是;空间O(n)。
趟数:n
总结
(主要是前六种)
————————————————————————————————————————————————————————————————————————————
| 排序方法 平均时间复杂度 最坏时间复杂度 最好时间复杂度 空间复杂度 是否稳定 趟数 说明
|————————————————————————————————————————————————————————————————————————————
| 插入 O(n2) O(n2) O(n) O(1) √(顺序移动 ) n-1
| 冒泡 O(n2) O(n2) O(n) O(1) √(相邻元素交换) [1,n-1]
| 选择 O(n2) O(n2) O(n2) O(1) × n-1
|————————————————————————————————————————————————————————————————————————————
| 希尔 O(nlgn) O(n2) O(nlgn) O(1) × 不定 对插入排序的改进
| 快速 O(nlgn) O(n2) O(nlgn) O(lgn),递归导致 × 不定 对冒泡排序的改进
| 堆排序 O(nlgn) O(nlgn) O(nlgn) O(1) × n-1 对选择排序的改进
|————————————————————————————————————————————————————————————————————————————
| 二路归并 O(nlgn) O(nlgn) O(nlgn) O(n),创建辅助数组导致 √ ceil(lgn) 主要用于外部排序
| 基数排序 O(d(n+r)) O(d(n+r)) O(d(n+r)) O(n+r) √ d (d位r进制)桶排序的扩展
|————————————————————————————————————————————————————————————————————————————
| 桶排序 O(m+n) O(m+n) O(m+n) O(m) × 1 m为桶大小
| 计数排序 O(n2) O(n2) O(n2) O(n) √ n
|————————————————————————————————————————————————————————————————————————————
note:
基于相邻元素交换或顺序移动的排序算法是稳定的。
前7中是基于元素比较的排序,后两种不是。
希尔排序是对插入排序的改进、快排是对冒泡排序的改进、堆排序是对选择排序的改进。
归类
插入类:直接插入排序、希尔排序
交换类:冒泡排序、快速排序
选择类:选择排序、堆排序
归并类:二路归并
分配排序:桶排序、基数排序、计数排序等。
外排序
通常用多路归并排序。
其他
arrangeOf2Sets
将元素奇数组织到左边、偶数组织到右边,稳定O(n2) 或 非稳定算法O(n):
![](https://i-blog.csdnimg.cn/blog_migrate/8f900a89c6347c561fdf2122f13be562.gif)
![](https://i-blog.csdnimg.cn/blog_migrate/961ddebeb323a10fe0623af514929fc1.gif)
1 //将奇数移到左边,偶数移到右边 2 void arrangeOf2Sets(int k[],int n,int isStable) 3 {//约定i为当前元素位置,s之前的元素为奇数(不包括s) 4 int i,s,tmp; 5 s=0; 6 if(isStable) 7 {//稳定的排序 8 int t; 9 for(i=0;i<n;i++) 10 { 11 if(k[i] & 1==1) 12 {//后移为k[i] 腾出位置 13 for(tmp=k[i],t=i;t>s;t--) 14 { 15 k[t]=k[t-1]; 16 } 17 k[s]=tmp; 18 ++s; 19 } 20 } 21 } 22 else 23 {//非稳定排序 24 for(i=0;i<n;i++) 25 { 26 if(k[i] & 1==1) 27 {//直接交换 28 tmp=k[s]; 29 k[s]=k[i]; 30 k[i]=tmp; 31 ++s; 32 } 33 } 34 } 35 }
这其实就是快排划分的一种实现法,从前往后搜索;也可用快排划分的另一种实现法,分别从两边往中间搜索。非稳定时O(n)、稳定时O(n2)。
此外,可用插入排序的思想,从前往后,遇到奇数时就往前找插入到找到的第一个奇数后面。O(n2)
还可空间换时间,新建个数组,然后扫描输入的数组两遍分别把奇数和偶数存入新数组,然后存回原数组覆盖。O(n) (别学排序学傻了-_-!)
arrangeOf3Sets
O(n)复杂度将正数组织到左边,0组织到中间、负数组织到右边(与上类似),非稳定算法:
![](https://i-blog.csdnimg.cn/blog_migrate/8f900a89c6347c561fdf2122f13be562.gif)
![](https://i-blog.csdnimg.cn/blog_migrate/961ddebeb323a10fe0623af514929fc1.gif)
1 void arrangeOf3Sets(int k[],int n) 2 {//约定s之前的元素均为正数,e之后的元素均为负数,i为当前元素位置 3 int s,e,i,tmp; 4 s=0; 5 e=n-1; 6 7 i=s; 8 while(i<=e) 9 { 10 if(k[i]>0) 11 { 12 tmp=k[i]; 13 k[i]=k[s]; 14 k[s]=tmp; 15 s++; 16 } 17 else if(k[i]==0) 18 { 19 i++; 20 } 21 else 22 { 23 tmp=k[i]; 24 k[i]=k[e]; 25 k[e]=tmp; 26 e--; 27 } 28 } 29 }
参考资料
《数据结构与教程 第二版》(北航出版社)