最近重新研究了快速排序,整理了一下快速排序的各种版本。
一、单向扫描
ok,先来看单向扫描。
算法导论上版本是这样的:
PARTITION(A, p, r)
1 x ← A[r] //以最后一个元素,A[r]为主元
2 i ← p - 1
3 for j ← p to r - 1 //注,j从p指向的是r-1,不是r。
4 do if A[j] ≤ x
5 then i ← i + 1
6 exchange A[i] <-> A[j]
7 exchange A[i + 1] <-> A[r] //最后,交换主元
8 return i + 1
下面是c++代码:
void my_swap(int *a,int *b) { if(a==b) return; *a=*a^*b; *b=*b^*a; *a=*a^*b; } int partition(int a[],int left,int right)//以最后一个元素为主元 { int key=a[right]; int k=left-1; for(int i=left;i<right;i++) { if(a[i]<=key) //可以不加=号 { k++; if(k!=i) my_swap(&a[i],&a[k]); } } k++; my_swap(&a[k],&a[right]); return k; } void quickSort(int a[],int left,int right) { if(left>=right) return; int k=partition(a,left,right); quickSort(a,left,k-1); quickSort(a,k+1,right); }
算法导论上取最后一个元素为主元,当然可以选第一个为主元,请看下面的代码:
int partition2(int a[],int left,int right)//以第一个元素为主元 { int key=a[left]; int k=left; for(int i=left+1;i<=right;i++) { if(a[i]<=key) //可以不加=号 { k++; if(k!=i) my_swap(&a[i],&a[k]); } } //注意此处没有k++ my_swap(&a[left],&a[k]); return k; }
这两种情况如果出现最坏情况,则算法效率就将为O(n*n),比如说取第一个为主元,刚好第一个是最小的数。我们可以采用三数取中分割法避免最坏情况的发生。
ok,下面看一下什么是三数取中分割法,意思很简单,就是取第一个,中间一个,最后一个,取这三个数中的中间大的那个数作为主元。
好,意思明白,下面直接看代码:
代码测试:采用随机数极限测试,取0-100的随机数进行测试。int partition3(int a[],int left,int right) //三数取中分割法 { int mid=(left+right)/2; if(a[left]<a[mid]) my_swap(&a[left],&a[mid]); if(a[right]<a[mid]) my_swap(&a[right],&a[mid]); //经过以上两步后mid是最小的。 if(a[left]>a[right]) my_swap(&a[left],&a[right]); //以上已经将中间的那个移动到第一个位置 int key=a[left]; int k=left; for(int i=left+1;i<=right;i++) { if(a[i]<=key) //可以不加=号 { k++; if(k!=i) my_swap(&a[i],&a[k]); } } my_swap(&a[left],&a[k]); return k; }
int main() { srand(time(NULL)); const int N = 100; //“极限”测试。为了保证程序的准确无误,你也可以让N=10000。 int *data = new int[N]; for(int i =0; i<N; i++) data[i] = rand()%100; //同样,随机化的版本,采取随机输入。 cout<<"初试值:"<<endl; for(int i=0; i<N; i++) cout<<data[i]<<" "; cout<<endl; quickSort(data,0,N-1); cout<<"after quick sort:"<<endl; for(int i=0; i<N; i++) cout<<data[i]<<" "; cout<<endl; delete []data; return 0; }
二、双向扫描版
双向扫描主要是Hoare版本和其变形。
那么,什么是霍尔提出的快速排序版本列?如下,即是:
HOARE-PARTITION(A, p, r)
c++代码:
1 x ← A[p]
2 i ← p - 1
3 j ← r + 1
4 while TRUE
5 do repeat j ← j - 1
6 until A[j] ≤ x
7 repeat i ← i + 1
8 until A[i] ≥ x
9 if i < j
10 then exchange A[i] <-> A[j]
11 else return j
int partition5(int a[],int left,int right) { int key=a[left]; int i=left-1; int j=right+1; while(true) { do { j--; } while(a[j]>key); do { i++; } while(a[i]<key); if(i<j) { my_swap(&a[i],&a[j]); } else { return j; //此处返回的不是主元,只是满足a[left...j] <=a[j+1...right] } } }
使用上述分割法,需要如下的代码:
void quickSort1(int a[],int left,int right) { if(left>=right) return ; int k=partition5(a,left,right); quickSort1(a,left,k); //因为使用的是partition5的分割方法,所以此处是k而不是k-1 quickSort1(a,k+1,right); }
考虑这样一种情况:
int partition5(int a[],int left,int right) { int key=a[left]; int i=left; int j=right; while(true) { while(a[j]>key) { j--; } while(a[i]<key) { i++; } if(i<j) { my_swap(&a[i],&a[j]); } else { return j; } } }
这种情况会出现死循环,考虑如下数组:2,3,4,5,6,2 数组中a[left]=a[right]=key 交换后还是2,3,4,5,6,2 进入死循环,有人说while循环加个等号,
这种情况的话如果数组全部相等,2,2,2,,,,,2,则会出现数组越界的错误。
一种变种算法:
int partition4(int a[],int left,int right) //双向遍历分割 { int key=a[left]; int i=left; int j=right; while(i<j) //最终一定是i=j结束 { while(a[j]>=key&&i<j) j--; my_swap(&a[j],&a[i]); while(a[i]<=key&&i<j) i++; my_swap(&a[j],&a[i]); } a[i]=key; //可以不用这句,因为a[i]肯定是key return i; }
看下面事例:a:3 8 7 1 2 5 6 4 //以第一个元素为主元
2 8 7 1 5 6 4
b:2 7 1 8 5 6 4
c:2 1 7 8 5 6 4
d:2 1 7 8 5 6 4
e:2 1 3 7 8 5 6 4 //最后补上,关键字3