快速排序是比较经典的算法,所以我把在编程珠玑上的讲解整理了一下,也当给自己理清思路。在某些特定情况下,在空间和时间上应该还有很多优化的策略,所以以后会继续学习优化的方法。
一、单向划分的快排实现
该程序不会导致无限递归调用,因为每次都排除了x[m],与二分搜索会终止的原理一样。
当输入数组是不同元素的随机排列时,该快速排序的平均时间复杂度是O(n log n),栈空间为O(log n).任何基于比较的排序至少需要O( n log n)次比较,因此快速排序接近最优算法。
c库函数的快速排序算法的通用接口开销很大,所以本程序可能适合与一些表现良好的应用程序,但在一些常见的输出下,本程序可能会退化为平方时间算法。
1 #include<iostream> 2 using namespace std; 3 int x[100]; 4 void qsort1(int l , int u) 5 { 6 if(l >= u) 7 return ; 8 int m = l; 9 for(int i = l + 1; i <= u; i++) 10 { 11 if(x[i] < x[l]) 12 swap(x[++m] , x[i]); 13 } 14 swap(x[l] , x[m]); 15 qsort1(l , m - 1); 16 qsort1(m + 1 , u); 17 } 18 int main() 19 { 20 int n; 21 cin>>n; 22 for(int i = 0; i < n;i++) 23 { 24 cin>>x[i]; 25 } 26 qsort1(0 , n - 1); 27 for(int i = 0 ; i < n; i++) 28 { 29 cout<<x[i]<<" "; 30 } 31 cout<<endl; 32 return 0; 33 }
二、单向划分加哨兵优化
对qsort1的一点优化。将划分方案从右向左运行,由于循环结束时x[m] = t;所以可直接使用参数(l , m-1)和(m+1 , u)进行递归,不再需要swap操作,用x[l]作为哨兵还省去了内循环中的一次比较。
1 #include<iostream> 2 using namespace std; 3 int x[100]; 4 void qsort2(int l , int u) 5 { 6 int i , m , t; 7 if(l >= u) 8 return ; 9 i = m = u + 1; 10 t = x[l]; 11 do{ 12 while(x[--i] < t) 13 ; 14 swap(x[--m] , x[i]); 15 }while(i != l); 16 qsort2(l , m - 1); 17 qsort2(m + 1 , u); 18 } 19 int main() 20 { 21 int n; 22 cin>>n; 23 for(int i = 0; i < n; i++) 24 cin>>x[i]; 25 qsort2(0 , n - 1); 26 for(int i = 0 ; i < n; i++) 27 cout<<x[i]<<" "; 28 cout<<endl; 29 return 0; 30 }
三、双向划分的快速排序
n个相同元素组成的数组,对于这种输入,插入排序的性能比较好,每个与素需要移动的距离都是0,所以总的运行时间是O(n),但是qsort1算法的性能比较糟糕,n-1次的划分中每次划分都需要O(n)时间来去掉一个元素,所以总的运行时间是O(n^2)。使用双向划分可以避免这问题。采用双向划分,当输入元素相时,如果采用向右跳过避免多与操作的方式,还是会得到平方时间的算法(注意向右的do循环会执行到底),正确的做法是当遇到相同元素是停止扫描,交换i和j 的元素值。这样做交换的次数增加了,但是将所有元素都同的最坏情况变成了差不多需要nlog2n次比较的最好情况。
1 #include<iostream> 2 using namespace std; 3 int x[100]; 4 void qsort3(int l , int u) 5 { 6 if(l >= u) 7 return ; 8 int t = x[l]; 9 int i = l; 10 int j = u + 1; 11 while(1){ 12 do{ 13 i++; 14 }while(i <= u && x[i] < t); 15 do{ 16 j--; 17 }while(x[j] > t); 18 if(i > j) 19 break; 20 swap(x[i] , x[j]); 21 } 22 swap(x[l] , x[j]); 23 qsort3(l , j - 1); 24 qsort3(j + 1 , u); 25 } 26 int main() 27 { 28 int n; 29 cin>>n; 30 for(int i = 0; i < n; i++) 31 cin>>x[i]; 32 qsort3(0 , n - 1); 33 for(int i = 0 ; i < n; i++) 34 cout<<x[i]<<" "; 35 cout<<endl; 36 return 0; 37 }
四、采用随机化优化的快速排序
对于随机输入,以上算法没问题,但是对于某些输入,这种算法的时间和空间都偏多。例如,如果数组已经按升序排列好了,或者已经按降序排列好了,就会围绕最小或者最大,然后次小或次大划分下去,总共需要O(n^2)的时间。随机选择划分元素就可以得到好得多的性能,我们通过把x[l]和x[l...u]中的一个随机项想交换来实现这一点swap(l,randint(l , u));结合了随机划分元素和双向划分以后,对于任意输入,快排的期望运行时间都正比于nlog n,随机情况下的性能边界是通过调用随机数生成器得到的,而不是通过对输入的分布进行假设得到的。
用插入排序来排序很小的子数组,可以继续提高性能。
cutoff用以停止继续划分,一般选取cutoff为50。
1 #include<iostream> 2 using namespace std; 3 int x[100]; 4 void isort3(int l , int u) 5 { 6 int j; 7 for(int i = l ; i <= u ;i++){ 8 int t = x[i]; 9 for(j = i ; j > 0 && x[j-1] > t ;j--) 10 x[j] = x[j-1]; 11 x[j] = t; 12 } 13 } 14 int randint(int l , int u) 15 { 16 return l + rand()%(u - l); 17 } 18 void qsort4(int l , int u) 19 { 20 if(u - l < 50){ 21 isort3(l , u); 22 return; 23 } 24 swap(x[l] , x[randint(l , u)]); 25 int t = x[l]; 26 int i = l; 27 int j = u + 1; 28 while(1){ 29 do{ 30 i++; 31 }while(i <= u && x[i] < t); 32 do{ 33 j--; 34 }while(x[j] > t); 35 if(i > j) 36 break; 37 swap(x[i] , x[j]); 38 } 39 swap(x[l] , x[j]); 40 qsort4(l , j - 1); 41 qsort4(j + 1 , u); 42 } 43 int main() 44 { 45 int n; 46 cin>>n; 47 for(int i = 0; i < n; i++) 48 cin>>x[i]; 49 qsort4(0 , n - 1); 50 for(int i = 0 ; i < n; i++) 51 cout<<x[i]<<" "; 52 cout<<endl; 53 return 0; 54 }
五、快速排序总结
C库函数qsort非常简单并且相对比较快,比我们自己写的快排慢,仅仅因为其通用灵活的接口对每次比较都是用函数调用。c++库函数sort具有最简单的接口:我们通过调用sort(x,x+n)对数组排序,其实现也非常高效,若系统的排序能满足我们的要求,就不用考虑自己编写代码了。