快速排序是Hoare 于 1962 年提出的一种二叉树结构的交换排序方法,其基本思想为: 任取待排序元素序列中 的某元素作为基准值,按照该排序码将待排序集合分割成两子序列,左子序列中所有元素均小于基准值,右 子序列中所有元素均大于基准值,然后最左右子序列重复该过程,直到所有元素都排列在相应位置上为止 。
拿这张图进行举例,
第一步:R 先向左走,找比key指向的值(即为6) 小的值,找到5,R停下。
第二步:L 再 向右走,找比6 大 的值,找到7,L停下。
第三步: 交换 L与R所指向的值。
之后重复上面三步,直到L与R相遇。
最后,交换L指向的值与key指向的值。
最后交换的结果就如上图所示,此时我们发现,在数字6左边的数字全都比6小,右边的数字全部比6大,这就表明数字6已经排好了位置。在这之后,我们分别对6的左区间和6的右区间再次进行快排,到最后数组整体就会有序。
一、hoare版本快排
了解了基础思路之后,我们尝试完成hoare版本的快排。
首先我们先进行单趟排序程序的撰写。
void QuickSort(int* a, int left, int right)
{
int begin = left, end = right; //(left和right为数组区间的两头)
int keyi = left;
while(begin < end)//循环继续的条件
{
while(begin < end && a[end] > a[keyi)//从右边找小的值
end--;
while(begin < end && a[begin] < a[keyi])
begin++;
swap(&a[end], &a[begin]);//交换函数交换两值
}
swap(&a[keyi],&a[begin]);
keyi = begin;
}
以上就是单趟排序程序,解决完第一趟之后,我们就以begin指向的位置为界,分为左区间和右区间再次进行排序。需要明确的是,递归结束的条件是,区间内只含有一个值或者区间不存在(即left >= right)
void QuickSort(int* a, int left, int right)
{
if(left >= right)
return;
//将单趟排序的程序放入其中
//数组被分成了[left,keyi-1] keyi [keyi+1,right]的区间
QuickSort(a, left, keyi - 1);
QuickSort(a, keyi + 1, right);
}
以上就是hoare的快排版本。
那么,这段程序有没有可以优化的空间呢?
当数组本身就是有序数组时,我们会发现R会一直走下去与直到与L相遇再交换,之后进行右区间的依次递归,耗费的时间极大,因此在keyi指向的数据选择上,我们采用三数取中的办法( 即数组的两个区间值与中间值相比较,返回既不是最大,也不是最小的数,同时交换该数与keyi指向的数)
int GetMid(int* a, int left, int right)
{
int mid = (left + right) / 2;
if (a[left] < a[mid])
{
if (a[mid] < a[right])
return mid;
else if (a[right] < a[left])
return left;
else
return right;
}
else//a[left] > a[mid]
{
if (a[mid] > a[right])
return mid;
else if (a[right] > a[left])
return left;
else
return right;
}
}
采用这个办法,可以解决数组有序时的问题,还有另一个优化的方向就是小区间优化。我们知道,当递归的深度太深时,会导致栈溢出的问题。为了在一定程度上解决这个问题,我们引入小区间优化,即当递归后的数组元素小于10时,我们直接使用插入排序解决
if ((right - left + 1) < 10)//小区间优化,当数组长度小于10时不需要继续递归,直接用插入排序
{
InsertSort(a + left, right - left + 1);
}
完整代码如下:
int GetMid(int* a, int left, int right)
{
int mid = (left + right) / 2;
if (a[left] < a[mid])
{
if (a[mid] < a[right])
return mid;
else if (a[right] < a[left])
return left;
else
return right;
}
else//a[left] > a[mid]
{
if (a[mid] > a[right])
return mid;
else if (a[right] > a[left])
return left;
else
return right;
}
}
int PartSort1(int* a, int left, int right)
{
int mid = GetMid(a, left, right);//进行三数取中,防止数组有序时快排仍然消耗大量时间的问题
swap(&a[left], &a[mid]);
int keyi = left;
int begin = left, end = right;
while (begin < end)
{
while (begin < end && a[end] >= a[keyi])
{
end--;
}
while (begin < end && a[begin] <= a[keyi])
{
begin++;
}
swap(&a[end], &a[begin]);
}
swap(&a[keyi], &a[begin]);
return begin;
}
void QuickSort(int* a, int left, int right)//时间复杂度为(O(N*LogN))
{
if (left >= right)
return;
if ((right - left + 1) < 10)//小区间优化,当数组长度小于10时不需要继续递归,直接用插入排序
{
InsertSort(a + left, right - left + 1);
}
else
{
int keyi = PartSort1(a, left, right);
QuickSort(a, left, keyi - 1);
QuickSort(a, keyi + 1, right);
}
}
二、前后指针版
接下来介绍快排的另外一种方法,前后指针法
这个方法怎么实现呢?
第一步:cur指针往前走,如果遇到的数比key指向的值小,prev跟着一起往前走,直到cur走到7,prev走到2.
第二步:cur指针指向的值比6大,cur继续往前走,prev停着不动。
第三步:cur指针走到3,比6小,prev往前走,指向7,交换3和7
之后重复上述步骤,直到cur走出数组,此时根据prev指向的位置将数组分成两半进行递归
主体程序与hoare版本快排差别不大,主要是单趟排序的程序有所差别。
int PartSort2(int* a, int left, int right)
{
int mid = Getmid(a, left, right);
swap(&a[mid], &a[left]);
int keyi = left;
int prev = left;
int cur = left + 1;
while(cur > right)
{
if(a[cur] < a[keyi] && prev++ != cur)//避免自身交换
swap(&a[cur], &a[prev]);
cur++;
}
swap(&a[keyi], &a[prev]);
return prev;
}
整体代码如下:
int PartSort2(int* a, int left, int right)
{
int mid = GetMid(a, left, right);
swap(&a[mid], &a[left]);
int keyi = left;
int prev = left;
int cur = prev + 1;
while (cur <= right)
{
if (a[cur] < a[keyi] && prev++ != cur)//避免自己与自己发生交换
swap(&a[cur], &a[prev]);
cur++;
}
swap(&a[keyi], &a[prev]);
return prev;
}
void QuickSort(int* a, int left, int right)//时间复杂度为(O(N*LogN))
{
if (left >= right)
return;
if ((right - left + 1) < 10)//小区间优化,当数组长度小于10时不需要继续递归,直接用插入排序
{
InsertSort(a + left, right - left + 1);
}
else
{
int keyi = PartSort2(a, left, right);
QuickSort(a, left, keyi - 1);
QuickSort(a, keyi + 1, right);
}
}
三、非递归版快排
尽管在上面的程序中我们利用了小区间优化优化掉了绝大多数的递归,但是程序仍然有栈溢出的风险。因此,我们尝试完成非递归版本的快排。
在快排的每一次递归中,变化的值是什么呢?答案是待排序数组的区间,即left和right,那我们的思路就是将每一次排序的区间提前存入某个地方,并利用循环不断读取该区间进行排序。利用数据结构—栈,可以解决这个问题。(关于栈详见其他博客)
void QuickSortNonR(int* a, int left, int right)
{
ST st;
STInit(&st);
STPush(&st,right);
STPush(&st,left);
while(!STEmpty(&st))
{
int begin = STTop(&st);
STPop(&st);
int end = STTop(&st);
STPop(&st);
int keyi = PartSort2(a, begin, end);
if(keyi + 1 < end)
{
STPush(&st, end);
STPush(&st, keyi + 1);
}
if(keyi - 1 > begin)
{
STPush(&st, keyi - 1);
STPush(&st, begin);
}
}
}