快速排序
一、算法性能
- 快速排序平均性能良好,平均运行时间为O(nlgn);
- 最坏情况是每次划分选取的基准都是当前无序区中关键字最小(或最大)的记录,划分的结果是基准左边的子区间为空(或右边的子区间为空),而划分所得的另一个非空的子区间中记录数目,仅仅比划分前的无序区中记录个数减少一个。时间复杂度为O(n*n)。当待排序的序列是完全有序的时候,快速排序递归就会达到最高代价。
二、基本原理:选择一个基准记录;从左到右扫描记录表,找到关键字大于等于基准的记录,同时从右到左扫描记录表,找到小于等于基准的记录,交换两个记录。
三、关于基准记录的选择
1、取首;
2、选择当前记录表的第一、中间和最后一个记录中关键字为中值的记录。提高选择均等划分记录表的可能性。k=3
四、代码
1 #include<iostream> 2 3 using namespace std; 4 5 int Partition(int* A, int p, int r) 6 { 7 int x = A[r]; 8 int i = p-1; 9 int j,temp; 10 for (j=p;j<r;j++) 11 { 12 if (A[j]<=x) 13 { 14 i++; 15 temp = A[j]; 16 A[j] = A[i]; 17 A[i] = temp; 18 } 19 } 20 temp = A[i+1]; //exchange 21 A[i+1] = A[r]; 22 A[r] = temp; 23 return (i+1); 24 } 25 26 27 void QuickSort(int *A, int p, int r) 28 { 29 int q; 30 if (p<r) 31 { 32 q = Partition(A, p, r); 33 QuickSort(A, p, q-1); 34 QuickSort(A, q+1, r); 35 } 36 } 37 38 void ImpQuickSort(int* A, int p, int r) 39 { 40 int q; 41 while (p<r) 42 { 43 q = Partition(A, p, r); 44 if ((q-p)<(r-q)) 45 { 46 ImpQuickSort(A, p, q-1); 47 p = q+1; 48 } 49 else 50 { 51 ImpQuickSort(A, q+1, r); 52 r = q-1; 53 } 54 } 55 } 56 57 int RandomizedSelect(int* A, int p, int r, int i) 58 { 59 int q, k; 60 if (p==r) 61 return A[p]; 62 q = Partition(A, p, r); 63 k = q-p+1; 64 if (k==i) 65 return A[q]; 66 else if (i<k) 67 RandomizedSelect(A,p,q-1,i); 68 else 69 RandomizedSelect(A,q+1,r,i-k); 70 } 71 72 int main() 73 { 74 int n; 75 int *a; 76 cin>>n; 77 a = new int[n]; 78 for (int i=0;i<n;i++) 79 { 80 cin>>a[i]; 81 } 82 QuickSort(a,0,n-1); 83 for (int i=0;i<n;i++) 84 cout<<a[i]<<' '; 85 cout<<endl; 86 cout<<RandomizedSelect(a,0,n-1,5); 87 return 0; 88 }
五、改进方法
1、改进基准选择方案
- 随机选择基准
- 选择当前记录表的第一、中间和最后一个记录中关键字为中值的记录。提高选择均等划分记录表的可能性。
- 每次取数据集的中位数作为基准
选取中位数平均运行时间是O(n),最坏运行时间O(n*n)。
1 int RandomizedSelect(int* A, int p, int r, int i) 2 { 3 int q, k; 4 if (p==r) 5 return A[p]; 6 q = Partition(A, p, r); 7 k = q-p+1; 8 if (k==i) 9 return A[q]; 10 else if (i<k) 11 RandomizedSelect(A,p,q-1,i); 12 else 13 RandomizedSelect(A,q+1,r,i-k); 14 }
2、用插入排序改进
当数据量较小时,快速排序相对较慢,其递归结构必然导致反复排序小序列。
- 改进方法一:使用排序小序列较快的方法代替快速排序,比如插入排序;具体小到何种规模时采用插入排序,一些文章中说5—25 之间。SGI STL 中的快速排序采用的值是 10。
- 改进方法二:当需要排序的记录表小于一定长度时,什么也不做。从整个记录表来看,此时整个记录表接近于排好序,正是利用插入排序性能最好的情况,对整个表调用一次插入排序。实验表明,子表长度小于9时,可采用此策略。
3、减小递归深度
递归执行需要栈空间,如果记录被均匀划分,则算法的最大递归深度是lgn,需要栈空间O(lgn);如果是最坏情况,记录表被划分成长度为n-1和1的子表,则算法的递归深度是n,需要栈空间O(n)。为了减小递归深度,可以递归排序两个子表中较小的,在用循环代替第二个递归,使得算法递归深度最多为O(lgn)。
利用了尾递归:
摘自:http://www.cnblogs.com/Anker/archive/2013/03/04/2943498.html
顾名思义,尾递归就是从最后开始计算, 每递归一次就算出相应的结果, 也就是说, 函数调用出现在调用者函数的尾部, 因为是尾部, 所以根本没有必要去保存任何局部变量. 直接让被调用的函数返回时越过调用者, 返回到调用者的调用者去。尾递归就是把当前的运算结果(或路径)放在参数里传给下层函数,深层函数所面对的不是越来越简单的问题,而是越来越复杂的问题,因为参数里带有前面若干步的运算路径。
尾递归是极其重要的,不用尾递归,函数的堆栈耗用难以估量,需要保存很多中间函数的堆栈。比如f(n, sum) = f(n-1) + value(n) + sum; 会保存n个函数调用堆栈,而使用尾递归f(n, sum) = f(n-1, sum+value(n)); 这样则只保留后一个函数堆栈即可,之前的可优化删去。
1 void ImpQuickSort(int* A, int p, int r) 2 { 3 int q; 4 while (p<r) 5 { 6 q = Partition(A, p, r); 7 if ((q-p)<(r-q)) 8 { 9 ImpQuickSort(A, p, q-1); 10 p = q+1; 11 } 12 else 13 { 14 ImpQuickSort(A, q+1, r); 15 r = q-1; 16 } 17 } 18 }
以上三点综合利用大概可以提高原本快速排序20%-30%的性能。
4、三分区
对于每个元素完全相同的序列来讲,快速排序也会退化到 O(n^2)。可以将快速排序的二分区变成三分区:在分区的时候,将序列分为 3 堆,一堆小于基准元素,一堆等于基准元素,一堆大于基准元
素,下次递归调用快速排序的时候,只需对小于和大于基准元素的两堆数据进行排序,中间等于基准元素的一堆已经放好。
浙公网安备 33010602011771号