写在前面的话
最近系统地学习了快速排序算法,在此作一笔记。主要包括快排的各种版本:普通版本,改进的普通版本,随机化版本,三数值取中分割版本和Stooge版本。对各版本进行了简要分析,并给出了具体实现。
《算法导论》对快排的描述
快速排序是基于分治模式的,下面是对一个典型子数组A[p..r]排序的分治过程的三个步骤:
分解:数组A[p..r]被划分成两个(可能空)子数组A[p..q-1]和A[q+1..r],使得A[p..q-1]中的每个元素都小于等于A(q),而且,小于等于A[q+1..r]中的元素。下标q也在这个划分过程中计算。
解决:通过递归调用快速排序,对子数组A[p..q-1]和A[q+1..r]排序。
合并:因为两个子数组是就地排序的,它们的合并不需要操作:整个数组A[p..r]已排序
快速排序的关键是PARTITION(分解)过程,它对子数组A[p..r]进行就地重排。PARTITION结果的好坏直接影响快排的效率。
下面用具体代码来分析各种版本的快排。首先是《算法导论》7.1节描述的,普通版的快排。
#include"stdio.h"
/*普通版快速排序*/
//函数声明
void QuickSort(int A[],int n);
void QSort(int A[],int l, int r);
int Partition(int A[],int l, int r);
int Partition(int A[],int p, int r) //分割过程
{
int t;
int x = A[r];
int i = p - 1;
for(int j = p; j < r; j++)
{
if(A[j] <= x)
{
i++;
t = A[j]; A[j] = A[i]; A[i] = t;
}
}
t = A[i+1]; A[i+1] = A[r]; A[r] = t;
return i+1;
}
void QSort(int A[],int l, int r) //三个参数的快排子模块
{
int p;
if(l < r)
{
p = Partition(A, l, r);
QSort(A, l, p-1);
QSort(A, p+1, r);
}
}
void QuickSort(int A[],int n) //快排
{
QSort(A, 0, n-1);
}
void main() //验证排序
{
int a[5]={6,2,4,8,5};
QuickSort(a, 5);
for(int i=0;i<5;i++)
{
printf("%d ",a[i]);
}
printf("\n");
}
这个版本的PARTITION方法简单易懂,但有时会有一些不必要的交换发生,如排序 2 8 1 3 5 6 4
2 8 1 3 5 6 4
第一次交换8和1
2 1 8 3 5 6 4
第二次交换8和3
2 1 3 8 5 6 4
最后一次交换8和4
2 1 3 4 5 6 8
一次分割完成
对PARTITION过程稍加修改,得到优化的分解过程:
int Partition(int A[], int l, int r) //稍加优化的分割过程
{
int t;
int x = A[r];
int i = l-1, j = r;
while(1)
{
while(A[++i] < x)
;
while(A[--j] > x)
;
if(i < j)
{
t = A[i]; A[i] = A[j]; A[j] = t;
}
else
break;
}
t = A[i]; A[i] = A[r]; A[r] = t;
return i;
}
这一PARTITION过程通过比较,减少了交换次数。
上面两种方法都把输入数组的最后一个元素作为主元,即哨兵元素。如果输入是随机的,那么这是可以接受的,但是如果输入是预排序的或是反序的,那么这样的主元就产生一个劣质的分割,因为所有元素不是都被划入A[p..q-1]就是都被划入A[q+1..r]。下面给出两种可以避免产生劣质分割的PARTITION过程。
/*随机分割法*/
int Partition(int A[], int p, int r)
{
int t;
int rd = rand() % (r-p+1) + p;
assert(rd >= p && rd <= r);
t = A[r]; A[r] = A[rd]; A[rd] = t;/*将主元放在位置r */
int x = A[r];
int i = p-1, j = r;
while(1)
{
while(A[++i] < x)
;
while(A[--j] > x)
;
if(i < j)
{
t = A[i]; A[i] = A[j]; A[j] = t;
}
else
break;
}
t = A[i]; A[i] = A[r]; A[r] = t;
return i;
}
/*三数值取中分割法*/
int Partition(int A[], int p, int r)
{
int t;
int c = (p+r)/2;
if(A[p] > A[c])
{
t = A[p]; A[p] = A[c]; A[c] = t;
}
if(A[p] > A[r])
{
t = A[p]; A[p] = A[r]; A[r] = t;
}
if(A[c] > A[r])
{
t = A[c]; A[c] = A[r]; A[r] = t;
}
/*使得A[l] <= A[c] <= A[r] */
int k = r - 1;
t = A[c]; A[c] = A[k]; A[k] = t; /*将主元放在r-1的位置*/
int x = A[k];
int i = p-1, j = k;
while(1)
{
while(A[++i] < x)
;
while(A[--j] > x)
;
if(i < j)
{
t = A[i]; A[i] = A[j]; A[j] = t;
}
else
break;
}
t = A[i]; A[i] = A[k]; A[k] = t;
return i;
}
另外,Howard、Fine教授提出了一种“漂亮的”排序算法,称为Stooge排序。它将待排序数组分为3段,递归地分别对输入数组的前2/3、后2/3和前2/3进行排序。这是一个号称很厉害的排序算法。说它厉害并不是因为它有多么的快,事实上它比插入排序还要慢。它的厉害之处在于,用一般的掰手指头的方法绝对无法证明它的正确性。关于其证明,详见http://apps.hi.baidu.com/share/detail/19522201。具体算法如下:
<
/*Stooge_sort Stooge排序 */
void stooge(int A[], int i, int j)
{
if(A[i] > A[j])
{// exchange
int temp = A[i];
A[i] = A[j];
A[j] = temp;
}
if(i+1 >= j)
return;
int k = (j-i+1)/3;// first 2/3
stooge(A, i, j-k);// last 2/3
stooge(A, i+k, j);// first 2/3
stooge(A, i, j-k);
}
int partition(int A[], int p, int r)
{
int x = A[r];
int i = p-1;
for(int j=p; j<r; j++)
{
if(A[j] <= x)
{
i++;
int temp = A[i];
A[i] = A[j];
A[j] = temp;
}
}
int temp = A[i+1];
A[i+1] = A[r];
A[r] = temp;
return i+1;
}
int main()
{
int A[] = {3,5,2,7,4,10,1,9};
for(int i=0;i<8;i++)
printf("%d ",A[i]);
printf("\n");
int x = partition(A, 0, 3);
int len =sizeof(A)/sizeof(int);
stooge(A, 0, len-1);
for(int i=0;i<8;i++)
printf("%d ",A[i]);
printf("\n");
return 0;
}
/p>
Stooge排序简单易懂,但遗憾的是其效率很低,因为递归和比较的次数太多。