基本概念
快速排序的最差时间性能为O(n^2),期望时间性能为O(n*logn)
快速排序和归并排序一样,都属于分治法,其三个步骤如下
- 分解(关键步骤):将序列(A[p], ..., A[r])划分成两个(可能空)的子序列(A[p], ..., A[q-1])和(A[q+1], ..., A[r]),使得(A[p], ..., A[q-1])中的任意元素均小于等于A[q],(A[q+1], ..., A[r])中的任意元素均大于等于A[q]。Aq称为“轴”(pivot)
- 解决:递归地对分解得到的两个子序列进行快速排序
- 合并:经过分解步骤后,两个子序列之间已有大小关系,无需再合并
pivot的选取
有三种方法:
- 将第一个元素作为pivot——放弃,遇到预排序或反序的输入序列,快排的效率会变得很差
- 随机选取pivot——放弃,虽然克服了方法1的缺点,但随机数生成本身的开销较大,得不偿失
- 三数中值分割法——选用
三数中值分割法的一般做法是:使用左端(left)、右端(right)和中心位置mid=floor((low+high)/2)三个数中的中间大的那个数作为pivot
分解(partition)策略
策略一(详见严蔚敏《数据结构》):维护三个指针pl, ph, pm,其中pl从序列头部向尾部搜索,ph从尾部向头部搜索,pm始终指向“轴”,步骤如下
- 若*pl > *pm,则swap(pl, pm), pm = pl, pl++
- 若*ph < *pm,则swap(ph, pm), pm = ph, ph--
- 上面两个步骤交替进行
策略二(详见见《数据结构及算法分析——C语言描述》):将pivot放至最右端,维护两个下标i和j(此策略容易理解,但在处理临界情况(如相等元素、2/3小数组)时比较麻烦一点
- i从左向右搜索找到大于等于a[pivot]的元素停止
- j从右向左搜索找到小于等于a[pivot]的元素停止
- 若i和j尚未交错,则swap(&a[i], &a[j]),然后继续步骤1和2;否则swap(&a[i], &a[pivot]),分解结束
策略三(详见《算法导论》)
:以A[r]为“轴",记为x,从序列头部向尾部遍历(下标记为k)。若A[k] ≤ x,则放入左侧浅阴影区域;若A[k] ≥ x,则放入中间深阴影区域。遍历完成则分解步骤结束。此策略理论上需要同时维护两个区间边界,即图中的i和j,而实际代码中只需维护i即可
代码(采用分解策略二):
void swap(int * a, int * b){ //利用位异或进行swap
if(a != b){ //位异或需要加条件判断,注意
*a ^= *b;
*b ^= *a;
*a ^= *b;
}
}
void med3(int a[], int left, int right){ //三数中值分割法获得pivot
int mid = (left+right)/2;
if(a[left] > a[mid])
swap(&a[left], &a[mid]);
if(a[left] > a[right])
swap(&a[left], &a[right]);
if(a[mid] > a[right])
swap(&a[mid], &a[right]);
if(mid != right-1)
swap(&a[mid], &a[right-1]); //min放在a[left],max放在a[right],pivot放在a[right-1]
}
void q_sort(int a[], int left, int right){
if(left < right){
med3(a, left, right);
if(right-left <= 2) //长度小于3的小数组已被med3排序好,直接跳过
return;
int i = left, j = right-1, pivot = right-1; //注意初始条件
while(1){
while(a[++i] < a[pivot]); //遇到等于a[pivot]的元素也要停止
while(a[--j] > a[pivot]); //注意自增减的位置
if(i < j){
swap(&a[i], &a[j]);
}
else //i和j交错时退出循环
break;
}
swap(&a[i], &a[pivot]);
q_sort(a, left, i-1);
q_sort(a, i+1, right);
}
}
代码(采用分解策略三):
void swap(int * a, int * b){ //swap的另一个版本
int tmp = *a;
*a = *b;
*b = tmp;
}
int partition(int * a, int low, int high){
int x = low+rand()%(high-low+1); //随机选择a[low]...a[high]之间的一个作为pivot
swap(&a[x], &a[high]);
int div = low; //实际只需要维护小区间和大区间的分界点div即可
for(int k = low; k < high; k++){ //大区间和未遍历区间的分界点实际由游标k维护
if(a[k] < a[high]){
swap(&a[k], &a[div]);
div++;
}
}
swap(&a[high], &a[div]); //这步别漏
return div;
}
void q_sort(int * a, int low, int high){ //快速排序没有“合并”过程
if(low < high){ //注意这里判断条件只能是low < high,不能是low != high,因为对于只含两个元素的数组,partition后会出现low > high的情况
int div = partition(a, low, high);
q_sort(a, low, div-1);
q_sort(a, div+1, high);
}
}
综上,最终代码(位异或swap+三数中值分割法+策略3)为:
void swap(int * a, int * b){
if(a != b){
*a ^= *b;
*b ^= *a;
*a ^= *b;
}
}
void q_sort(int a[], int left, int right){
if(left < right){
int mid = (left+right)/2;
if(a[left] > a[mid]) swap(&a[left], &a[mid]);
if(a[left] > a[right]) swap(&a[left], &a[right]);
if(a[mid] < a[right]) swap(&a[mid], &a[right]); //把pivot放在最右端right处方能用策略三
int i, div = left;
for(i = left; i < right; i++){
if(a[i] < a[right]){
swap(&a[i], &a[div]);
div++;
}
}
swap(&a[div], &a[right]);
q_sort(a, left, div-1);
q_sort(a, div+1, right);
}
}