数据结构_排序总结
文件从逻辑上可分为排序顺序文件、一般(即非排序)顺序文件;从物理储上可分为连续文件、链接文件。(参考 文件及查找-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。(这里从后往前找该元素在前面已排序序列的插入位置,若从前往后找则结论相反即递减时最少)
代码(顺序插入与折半插入):(由于当前待插入元素前的序列已经有序,因此可以通过复制来实现元素后移从而避免交换,减少操作步骤)
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
题外话:网上有篇文章提到一个看似错误的升序排序算法实际上却“歪打正着”是对的。其伪代码如下:
for i = 1 to n do for j = 1 to n do if A[i] < A[j] then swap A[i] and A[j]
可以看出其含义是扫n躺(外循环),且第 i 躺时的作用是从前往后依次将比 A[i] 大的元素交换到 i 位置(内循环)。可见,每趟扫描后整个序列中的最大元素都会在 i 位置,因此内循环中只有 1 ~ (i-1) 的几个元素可能发生交换。
故该算法的本质含义是:依次将未排序序列的第一个元素“放到”到前面的已排序序列中的应在位置,可见其实质上就是上面的顺序插入排序,只不过这里的“放入”是通过内循环中的若干次元素交换实现的,其与上面的插入排序的区别是是否借助变量。本质简化版:
//极简的顺序插入排序算法 for i = 2 to n do for j = 1 to i − 1 do if A[i] < A[j] then swap A[i] and A[j]
2、冒泡排序
思路:通过可能的相邻元素交换来将未排序序列的最大值交换到该序列的末尾,重复此过程直到没有发生元素交换。是稳定排序。移动元素频繁,效率低。
复杂度:时间平均O(n2)、最好O(n)、最坏O(n2);空间O(1)。
趟数:不定,[1,n-1]。
比较次数:初始序列递增时最少,为n-1,一趟完事;最小元素在末尾时最多,为n(n-1)/2,走n-1趟。
代码:
//冒泡排序,通过可能的相邻元素交换 将 未排序序列中的最大值交换到该未排序序列的末尾,直到没有发生元素交换。 序列升序时1趟、最小值在末尾时n-1趟。 void bubbleSort(int k[],int n) { int i,j,tmp; int flag=1;//可以简单暴力地进行n-1趟冒泡完成排序,但实际上若一趟冒泡下来没有相邻元素交换,则说明已有序,没必要继续后续趟的冒泡了。flag即用来达到此目的,用于标记上趟是否有相邻元素交换。 for(j=n-1; j>0; j--){//j为未排序序列的末元素位置 if(!flag) { break; } flag=0; for(i=0;i<j;i++) { if(k[i]>k[i+1])//若为≥则非稳定 { flag=1; tmp=k[i]; k[i]=k[i+1]; k[i+1]=tmp; } } } }
也可有更简洁的实现,下面希尔排序的实现就用到了该实现,见后文。
3、选择排序
思路:依次从未排序序列选出最大元素放到该序列末尾。非稳定排序。
复杂度:时间平均O(n2)、最好(n2)、最坏O(n2),即与序列初始状态无关;空间O(1)。
趟数:n-1。
比较次数:n(n-1)/2。
代码:
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)趟。
代码(这里子序列的排序分别用泡排序、插入排序,推荐用后者):
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,每次值可不一样),则最坏时间复杂度为T(n)= T(αn)+T((1-a)n)+O(n) ≤ T(n/2)+T(n/2)+O(n) =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;} )
//快排,对冒泡排序的改进 。选择一个枢纽元,然后将小于枢纽元的元素整理到其左边、大于的整理到右边。 void quickSort1(int k[],int s,int t)//s、t分别为起点、终点元素的下标 {//这里以首元素作枢纽元 if(s>=t)return ; int i,j; i=s-1;//以扫描所有元素。这里也可以初始为s,但为了与可指定pivot的对应,这里为s-1 j=t+1;//初始指向末元素的后一个位置 while(1) { do{i++;}while(!(i==t || k[i]>=k[s])); // while( k[++i]<k[s] && i<t);//与上句等价。由于到这里s肯定小于t所以while里的两个表达式谁前谁后没影响,下同。 do{j--;}while(!(j==s || k[j]<=k[s])); // while(k[--j]>k[s] && j>s);//与上句等价 if(i<j) { swapForQuickSort(&k[j],&k[i]); } else break; } swapForQuickSort(&k[j],&k[s]); quickSort1(k,s,j-1); quickSort1(k,j+1,t); } void quickSort2(int k[],int s,int t) {//这里以首元素作为枢纽元 if(s>=t) return; int i=s,j=s; while(++i <=t)//小于枢纽元的放到枢纽元的后面 { if(k[i]<k[s]) { swapForQuickSort(&k[i],&k[++j]); } } swapForQuickSort(&k[j],&k[s]); quickSort2(k,s,j-1); quickSort2(k,j+1,t); } quickSort quickSort
须认识到的是,第一种实现方案中,while循环里枢纽元位置的值也有可能被交换到其他位置,比如枢纽元选首元素且给定的数组首元素为最大值时第一遍循环就会把枢纽元值交换到末位置了。不管i从s还是s-1起均是,可照代码理解下过程。
可以指定枢纽元选取原则的实现方法(与上面的两种对应):
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实现:
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个最大的元素】或【第k大元素】的问题(两者本质是一个问题,都可用下面方法解决),按此方法其平均复杂度为O(n),关键也在于Partition方法——只有保证每次划分出的两个子序列中长者规模减少为原来的α倍(0<α<1)才能保证最坏也为T(n)=T(αn)+O(n)=O(n)。选第k大元素的Java代码实现如下:
class Solution { public int findKthLargest(int[] nums, int k) { return findKthLargest3(nums,k); } //通过小顶堆选出最大的k个元素,则最后堆顶元素即为所求。时间复杂度O(nlgk) public int findKthLargest1(int[] nums, int k) { if(null==nums || nums.length==0) return -1; if(k<0 || k>nums.length) return -1; PriorityQueue<Integer> pq = new PriorityQueue<>(); for(int num:nums){ pq.offer(num); if(pq.size()>k){ pq.poll(); } } return pq.peek(); } //快排思想的快速选择,时间复杂度O(n) public int findKthLargest2(int[] nums, int k) { if(null==nums || nums.length==0) return -1; if(k<0 || k>nums.length) return -1; int left=0, right=nums.length-1; k = nums.length-k;//升序排序后第k大元素应在的位置 while(left<=right){ int p = partition(nums,left,right); if(p==k) return nums[p]; else if(p<k) left=p+1; else right=p-1; } return -1; } //找出 arr 中 第 n/2 大的元素 public static int media_number(int[] nums, int k){ int left = 0, right = nums.length - 1; int pivot_index = parition(nums, left, right);//枢轴元素的数组下标 while(pivot_index != k){ if(pivot_index > k){ right = pivot_index - 1; pivot_index = parition(nums, left, right); } else{ left = pivot_index + 1; pivot_index = parition(nums, left, right); } } return nums[k]; } //把一个元素放到它应在的位置,即其左边均不比其大、右边均不比其小 private int partition(int[] nums, int s, int e){ if(s>=e) return s; int pivot=s; int i=s,j=e+1; while(true){ do{i++;}while(!(i==e || nums[i]>=nums[pivot])); do{j--;}while(!(j==s || nums[j]<=nums[pivot])); if(i<j){ int tmp=nums[i]; nums[i]=nums[j]; nums[j]=tmp; }else{ break; } } int tmp=nums[j]; nums[j]=nums[pivot]; nums[pivot]=tmp; return j; } //快排思想的快速选择,时间复杂度O(n)。另一种实现 public int findKthLargest3(int[] nums, int k) { return sort(nums,0,nums.length-1,k); } private int sort(int[] nums, int s, int e, int k){ if(null==nums || nums.length==0) return -1; if(k<0 || k>nums.length) return -1; if(s<0 || e>=nums.length) return -1; if(s>=e) return nums[s]; int pivot=s; int i=s,j=e+1; while(true){ do{i++;}while(!(i==e || nums[i]>=nums[pivot])); do{j--;}while(!(j==s || nums[j]<=nums[pivot])); if(i<j){ int tmp=nums[i]; nums[i]=nums[j]; nums[j]=tmp; }else{ break; } } int tmp=nums[j]; nums[j]=nums[pivot]; nums[pivot]=tmp; int tarIndex = nums.length-k;//升序排序后第k大元素应在的位置 if(j==tarIndex) return nums[j]; else if(j<tarIndex) return sort(nums,j+1,e,k); else return sort(nums,s,j-1,k); } }
当然,如果k是已知的且值比较小(比较靠近边界,如k<=3)时可以有更简单的O(n)方法:用k个变量记录前k大元素,扫一遍维护各变量值。
求中位数问题可转为求第k大问题来解决。
选第k大也可用堆来解决,但时间复杂度为O(nlgk),没有此划分思想的效率高,其Java实现见下面堆排序部分。
最直观的解法是排序后取k个即可,时间复杂度O(nlgn),很慢。
海量数据找中位数问题,数据无法直接装入内存,但也可递归通过“依值域二分点将数据分到两个文件并确定数在哪个文件”直到可直接载入内存排序来实现。详见这篇文章。
快排的非递归遍历
尾递归算法可通过【栈/队列+循环】来改造成非递归算法。思路很简单,将递归理解为执行一个任务,故可通过栈或队列等来存放这些任务,循环取任务执行直到无任务。
用队列或栈都行,只要能确保所有该有的子任务都被能执行到且不会重复执行即可,但更通常的是栈,因为可借助它使得任务执行顺序和递归的一样,实际上就是模拟递归调用时在OS栈上的执行过程。
非递归的任务执行顺序和递归的通常会有区别(可从下面例子理解),但最终结果一样。
典型的场景是树、图等的前序遍历,快排本质也是前序遍历。
快排递归算法是尾递归,改造成非递归算法如下(与递归的非常像):
class Solution { public int[] sortArray(int[] nums) { if(null==nums || nums.length==0) return nums; LinkedList<Integer> pairs = new LinkedList<>();//用队列或栈都行,只要能确保所有该有的子任务都被执行到即可 pairs.offer(0); pairs.offer(nums.length-1); while(!pairs.isEmpty()){ //get range int s=pairs.pollFirst(); int e=pairs.pollFirst(); //System.out.println(s+" "+e); if(s>=e) continue; //partition int p=partition(nums,s,e); //sub problem #若要使得各任务执行顺序与递归的一样,则要用栈且入栈时后子问题先入、前子问题后入栈。想象下 pairs.offer(s); pairs.offer(p-1); pairs.offer(p+1); pairs.offer(e); } return nums; } int partition(int[] nums, int s, int e){ //法1 int i=s, j=e+1; while(true){ do{i++;}while(!(i==e||nums[i]>=nums[s])); do{j--;}while(!(j==s||nums[j]<=nums[s])); if(i<j){ int tmp=nums[i]; nums[i]=nums[j]; nums[j]=tmp; }else{ break; } } int tmp=nums[j]; nums[j]=nums[s]; nums[s]=tmp; return j; // //法2 // int p=s; // for(int i=s+1;i<=e;i++){ // if(nums[i]<nums[s]){ // p++; // int tmp=nums[p]; // nums[p]=nums[i]; // nums[i]=tmp; // } // } // int tmp=nums[p]; // nums[p]=nums[s]; // nums[s]=tmp; // return p; } }
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
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)。
其他:上面提到了堆的建立和维护,其实质是堆顶元素的“下沉”,除之外,堆的操作还有删除、插入操作:删除(即删除堆顶元素)通常是直接将堆末元素放到堆顶然后对之维护,也是“下沉”,前面堆排序的过程其实就是堆顶元素不断删除到末尾的过程;插入通常是将元素插入到序列末尾然后将该元素“上浮”。
删除后进行下沉、插入后进行上浮的示例(https://www.cnblogs.com/CarpenterLee/p/5488070.html):
//java code //siftDown() private void siftDown(int k, E x) { int half = size >>> 1; while (k < half) { //首先找到左右孩子中较小的那个,记录到c里,并用child记录其下标 int child = (k << 1) + 1;//leftNo = parentNo*2+1 Object c = queue[child]; int right = child + 1; if (right < size && comparator.compare((E) c, (E) queue[right]) > 0) c = queue[child = right]; if (comparator.compare(x, (E) c) <= 0) break; queue[k] = c;//然后用c取代原来的值 k = child; } queue[k] = x; } //siftUp() private void siftUp(int k, E x) { while (k > 0) { int parent = (k - 1) >>> 1;//parentNo = (nodeNo-1)/2 Object e = queue[parent]; if (comparator.compare(x, (E) e) >= 0)//调用比较器的比较方法 break; queue[k] = e; k = parent; } queue[k] = x; }
其他:堆的应用
优先队列。例如Java里的PriorityQueue内部就是用堆实现的。Java里Timer内部就是用优先队列存task的,按下次执行时间排序。
排序:升序大顶堆、降序小顶堆。
从n个元素里选m个最大的元素:建m个元素的小顶堆,对于后面到来的元素,若【小于堆顶元素则丢弃】否则【替换堆顶元素并维护堆性质】(若选m个最小,则建大顶堆)。Java代码示例(借助PriorityQueue):
public List<Integer> maxK(int[] nums, int k) { if(null==nums || nums.length==0) return null; if(k<0 || k>nums.length) return null; PriorityQueue<Integer> pq = new PriorityQueue<>(); for(int num:nums){ pq.offer(num); if(pq.size()>k){ pq.poll(); } } return pq }
选第k大元素:用小顶堆选出最大的k个元素后的堆顶元素,时间复杂度为O(nlgk)。同理,选第k小元素,则用大顶堆选最小的k个元素后的堆顶元素即为所求。Java代码示例:
int findKthLargest(int[] nums, int k) { // 小顶堆,堆顶是最小元素 PriorityQueue<Integer> pq = new PriorityQueue<>(); for (int e : nums) { // 每个元素都要过一遍二叉堆 pq.offer(e); // 堆中元素多于 k 个时,删除堆顶元素 if (pq.size() > k) { pq.poll(); } } // pq 中剩下的是 nums 中 k 个最大元素, // 堆顶是最小的那个,即第 k 个最大元素 return pq.peek(); }
7、二路归并排序
也叫合并排序。
思路:每趟依次将相邻的一对子序列归并为一个更大的子序列,子序列的长度从1开始每趟逐渐加倍,进行ceil(lgn)趟。稳定排序。
将序列分为两半,分别对两部分排好序,然后合并两个排好序后的序列,故时间复杂度为 T(n)=2T(n/2)+O(n)=nlg(n),本质上属于树的后续遍历。注意与前面快排对比,快排是前序遍历。
复杂度:时间平均O(nlgn),最好、最坏均为此;空间O(n)。空间复杂度最高,所以主要用于外部排序而很难用于内排序。
从递归树角度分析时间复杂度:每层上做的操作是合并子节点,而每层上有n个节点、共lgn层,故总的是nlgn。
趟数:ceil(lgn)
代码:
递归版:(本质上就是树的后序DFS遍历)
框架:
//框架 // 定义:排序 nums[lo..hi] void sort(int[] nums, int lo, int hi) { if (lo == hi) { return; } int mid = (lo + hi) / 2; // 利用定义,排序 nums[lo..mid] sort(nums, lo, mid); // 利用定义,排序 nums[mid+1..hi] sort(nums, mid + 1, hi); /****** 后序位置 ******/ // 此时两部分子数组已经被排好序 // 合并两个有序数组,使 nums[lo..hi] 有序 merge(nums, lo, mid, hi); /*********************/ } // 将有序数组 nums[lo..mid] 和有序数组 nums[mid+1..hi] // 合并为有序数组 nums[lo..hi] void merge(int[] nums, int lo, int mid, int hi); //实现 class Merge { // 用于辅助合并有序数组 private static int[] temp; public static void sort(int[] nums) { // 先给辅助数组开辟内存空间 temp = new int[nums.length]; // 排序整个数组(原地修改) sort(nums, 0, nums.length - 1); } // 定义:将子数组 nums[lo..hi] 进行排序 private static void sort(int[] nums, int lo, int hi) { if (lo == hi) { // 单个元素不用排序 return; } // 这样写是为了防止溢出,效果等同于 (hi + lo) / 2 int mid = lo + (hi - lo) / 2; // 先对左半部分数组 nums[lo..mid] 排序 sort(nums, lo, mid); // 再对右半部分数组 nums[mid+1..hi] 排序 sort(nums, mid + 1, hi); // 将两部分有序数组合并成一个有序数组 merge(nums, lo, mid, hi); } // 将 nums[lo..mid] 和 nums[mid+1..hi] 这两个有序数组合并成一个有序数组 private static void merge(int[] nums, int lo, int mid, int hi) { // 先把 nums[lo..hi] 复制到辅助数组中 // 以便合并后的结果能够直接存入 nums for (int i = lo; i <= hi; i++) { temp[i] = nums[i]; } // 数组双指针技巧,合并两个有序数组 int i = lo, j = mid + 1; for (int p = lo; p <= hi; p++) { if (i == mid + 1) { // 左半边数组已全部被合并 nums[p] = temp[j++]; } else if (j == hi + 1) { // 右半边数组已全部被合并 nums[p] = temp[i++]; } else if (temp[i] > temp[j]) { nums[p] = temp[j++]; } else { nums[p] = temp[i++]; } } } }
C实现:
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 }
非递归版:
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)。
采用链式存储时的二路归并排序:与顺序存储类似,难点在于找链表中间节点——可以用快慢指针:每次分别移动2、1步,则快者为null时慢者即为中间节点。
具体分析可参阅 labuladong-归并排序的理解及应用:
8、基数排序(类似桶排序)
思路:一个数的序列,每个数为d位r进制数,从末位到首位每次以该位数为关键字进行排序,进行d趟。稳定排序。
复杂度:时间平均O(d(r+n)),最好、最坏均如是; 空间O(r+n)。实际上复杂度与实现有关,这里对P349的实现而言。
趟数:d
是桶排序的扩展,桶排序:设m个桶(m≥n),采用相同原则将每个元素对应到某个桶(如对应到元素值与桶下标值相等的桶中),然后顺序遍历桶即得到排序序列。时间复杂度为O(m+n)、空间复杂度为O(m)。
以上的所有代码:
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
10、耐心排序(Patience Sorting)
参阅:MarchOn-最长递增子序列-Patience Game、Princeton-Course-LIS
适合于数据个数无限或无法事先一次性拿到全部数据的数据排序,“即来即排”。从这点上看,与水塘抽样很像。
总结
(主要是前六种)
————————————————————————————————————————————————————————————————————————————
| 排序方法 平均时间复杂度 最坏时间复杂度 最好时间复杂度 空间复杂度 是否稳定 趟数 说明
|————————————————————————————————————————————————————————————————————————————
| 插入 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):
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组织到中间、负数组织到右边(与上类似),非稳定算法:
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 }
随机置乱算法(洗牌算法)
与排序相反,其将一个序列打“乱”(打乱的结果可能有n!种的才算“乱”),具体可参阅:洗牌算法-MarchOn
参考资料
《数据结构与教程 第二版》(北航出版社)