快速排序因其排序效率高, 成为 二十世纪最伟大的10大算法之一。 本文根据Introduction of algorithm (IOA)和 数据结构 李春葆版,研究快速排序算法。
快速排序算法最重要在Partition 过程,即将数组A[p, ..., r]分治排序,使得 A[p, ..., q-1]<=A[q]<=A[q+1, ..., r]。实现上述功能后,递归的排序A[q]之前及之后的数组。
首先给出IOA上的 算法流程
QUICKSORT(A, p, r)
1 if p < r
2 then q ← PARTITION(A, p, r) //关键
3 QUICKSORT(A, p, q - 1)
4 QUICKSORT(A, q + 1, r)
PARTITION(A, p, r)
1 x ← A[r]
2 i ← p - 1
3 for j ← p to r - 1
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
这个Partition算法设计的十分美, 其中有两个指针i,j,分别使得
IF p<= k <= i, then A[k] <= x
IF i+1 <= k <= j -1, then A[k] > x
IF k = r, then A[k] = x
x 称作是pivot (主元)。 交换过程如下:
当A[j] < x 时候(即找到一个小于pivot的元素时候),交换A[i], A[j].
最后在交换A[i+1] 和A[r](主元)。 返回值是pivot的位置。 这个算法十分的帅!!
递归过程比较好理解。代码如下。
void QuickSort_LastPivot(int* A, int left, int right)
{
if(left < right)
{
int PartitionPos = QuickSort_Partion_LastPivot(A, left, right);
QuickSort_LastPivot(A, left, PartitionPos);
QuickSort_LastPivot(A, PartitionPos + 2, right);
}
}
int QuickSort_Partion_LastPivot(
int* A, int left, int right)
{
int i = left - 1, j;
int pivot = A[right];
for(j = left; j < right; j++)
{
if(A[j] <= pivot)
{
i++;
Swap(&A[i], &A[j]);
}
}
Swap(&A[i+1], &A[right]);
return i;
}
快速算法是一种不稳定算法,平均时间复杂度是 O(nlogn). 当最坏的情况下如果数组已经是排序好的,每次partition的大小都是一样的时候这样算法的复杂会reduce 到O(n^2). 影响算法性能其实在partition的 pivot的选择上, 上述选取最后一个节点为pivot。为了增加随机性,可以随机的选择一个pivot。 代码如下
int QuickSort_Random_Partion_LastPivot(
int* A, int left, int right)
{
srand(time(NULL));
Swap(&A[right], &A[(rand()%(right - left + 1)) + left]);
return QuickSort_Partion_LastPivot(A, left, right);
}
在李春葆的数据结构Partition是另外一种算法,这个partition过程其实是HOARE-PARTITION 过程
void Partition_HOARE(int* list, int left, int right)
{
int pivot = list[right];
int i = left, j = right;
while(left < right)
{
while(i < j && list[i] <= pivot)
{
i++;
}
list[j] = list[i];
while(j > i && list[j] >= pivot)
{
j--;
}
list[i] = list[j];
}
list[i] = pivot;
return i;
}
李春葆第二章练习题2.4的第二个解法。(第一个解法有误,当存在相等的两个元素时候第一个解法死循环)
效率上来说还是IOA上的partition方法更加高效一些。
之前面试被XJ问道如何根据快速排序找到中位数的算法,当时好久不看这些东西 真是没有想出来。其实在快排上可以很快找到。
当partition的位置大于中间的位置,只在左面去Partition;
当partition的位置小于中间的位置,只在右面去Partition;
等于时候直接返回。
其实该方法可以快速的找到任意一组数中任意rank位置的数,保证左面的都小于他,右面的都大于他。
程序如下
void FindNumberAtRank(int* A, int left, int right, int rank)
{
if (rank < left && rank > right)
return;
if(left < right)
{
int PartitionPos = QuickSort_Partion_LastPivot(A, left, right);
if ((PartitionPos + 1) > rank)
FindNumberAtRank(A, left, PartitionPos, rank);
else if ((PartitionPos + 1) < rank)
FindNumberAtRank(A, PartitionPos, right, rank);
else
return ;
}
}
// 测试
int main(int argc, _TCHAR* argv[])
{
clock_t begin = clock();
int A[11] = {43,3,2,4,1,5,6,4,9,78,8};
int rank = 4;
qs.FindNumberAtRank(A, 0, num-1, rank);
printf("Then ranked %d is %d\n", rank, A[rank]);
for(int i = 0; i < num; i++)
{
printf("%d ", A[i]);
}
printf("\n");
clock_t end = clock();
printf("Running time: %f s\n", float(end-begin)/1000l );
system("pause");
return 0;
}
程序保证在Rank 左面的数都小于 A[rank], 在 rank右面的数都大于 A[rank]。
这个算法的复杂度是O(n*lgn) - n, 每次平均少排一半的元素,总共下来(1/2 + 1/4 + 1/8 + ....)*n = n 次 排序。所以复杂度是 O(n*lgn) - n。
因为面的不好,心中一直抑郁,这回终于解开我心中的疙瘩。
其实这个Partition过程还有好多应用,比如将 元素划分section,使得每个section在一个范围内。即保留多个指针。