目录
一、快排介绍及其思想
快速排序是C.R.A.Hoare于1962年提出的一种划分交换排序。它采用了一种分治的策略,通常称其为分治法。
思想:
1.先从数列中取出一个数作为基准数。
2.分区过程,将比这个数大的数全放到它的右边,小于或等于它的数全放到它的左边。
3.再对左右区间重复第二步,直到各区间只有一个数。
二、hoare版本
hoare版本是最原始版本,其实现思想如下:
从上面动图我们不难分析出单趟有以下特点:
1. 以首元素为k值
2. 先在右边找比k小的数
3. 然后在左边找比k大的数
4. 最后,k与左边进行交换
我们很容易写出以下代码:
int k = left;
int begin = left;
int end = right;
while (a[k] < a[end])
{
end--;
}
while (a[k] > a[begin])
{
begin++;
}
swap(&a[begin], &a[end]);
我们很容易发现这代码有问题,啥问题?是不是很容易出现越界访问?当没有数比a[end]大时,很容易出现失控,下面也是同理。那么?我们该如何进行处理?很简单,加上控制条件即可,如下:
int k = left;
int begin = left;
int end = right;
while (begin < end && a[k] < a[end])
{
end--;
}
while (begin < end && a[k] > a[begin])
{
begin++;
}
swap(&a[begin], &a[end]);
这样便可进行控制,既然单趟已完成,那么多趟自然不在话下,这里我们用递归方式进行实现。
int PartSort1(int* a, int left,int right)
{
int k = left;
int begin = left;
int end = right;
while (begin < end)
{
while (begin < end && a[k] < a[end]) //这里等号可加可不加
{
end--;
}
while (begin < end && a[k] > a[begin])
{
begin++;
}
swap(&a[begin], &a[end]);
}
swap(&a[begin], &a[k]);
return begin;
}
void QuickSort(int* a, int left,int right)
{
if (left >= right) //递归结束条件判断
{ //当左边与右边相等时,不用进行任何处理
return;
}
int k = PartSort1(a, left, right);
QuickSort(a, left, k - 1);
QuickSort(a, k + 1, right); //区间为:左闭右开
}
那我们这时有个问题:为什么分要从右边开始,为何不能从左边开始?
我们要明白一件事:咱们要确保每一趟的k值的左边一定要比k值小,右边一定要比k值大才行,我们来看下面这组例子:
如果我们从左开始,左边找比k值大的,右边找比k值小的,其结果无外乎为:把6与5换一个位置,仅此而已,咱们的目的肯定会达不到。
要是,我们先找大呢?会发现左边的1会不参与右边的排序,只需将剩下的进行排序,我们对其进行分析,符合我们的目的,所以,我们一定从右边先找小!!!
三、前后指针版
前后指针方法,相比于hoare版,算法思路和实现过程有了较大的提升,是目前较为主流的写法。
通过上面动图我们可得到以下结论:
1. 以首元素为k值
2. 设立两个指针:prev,cur
3. cur位于begin+1的位置,prev位于begin位置,k先存放begin处的值。
4. cur不断往前+1,直到cur >= end时停止循环。
5. 如果cur处的值小于key处的值,并且prev+1 != cur,则与prev处的值进行交换。
6. 当循环结束时,将prev处的值与k的值相交换,并将其置为新的keyi位置。
代码实现:
int PartSort2(int* a, int left, int right)
{
int k = left;
int prev = left;
int cur = prev + 1;
while (cur <= right)
{
if (a[cur] < a[k] && ++prev != cur) //这里要确保当cur遇到比k小的数时,prev要++
{
swap(&a[cur], &a[prev]);
}
cur++;
}
swap(&a[k], &a[prev]);
return prev;
}
这里简单说明一下:
当prev == cur时,因为相等无交换必要,但无论如何只要cur遇到比k小的数时,prev都要++。
四、挖坑法
我们可以看到挖坑法其实和hoare版类似,那为什么还要出该版本呢? 主要是有人搞不清到底先走左还是先走右,所以推出了该版本,该版本更容易理解。
特点如下:
1. 将begin处的值放到k中,将其置为坑位(piti),然后right开始行动找值补坑。
2. right找到比k小的值后将值放入坑位,然后将此处置为新的坑。
3. left也行动开始找值补坑,找到比k大的值将其放入坑位,置为新的坑。
4. 当left与right相遇的时候,将k放入到坑位中。
5. 然后进行[begin,piti-1], piti, [piti+1,end] 的递归分治。
因为有以上基础,所以,我们不在进行赘述。代码实现如下:
int PartSort3(int* a, int left, int right)
{
int k = a[left]; //此处要放入值
int piti = left;
int begin = left;
int end = right;
while (begin < end)
{
while (begin < end && a[end] > k)
{
end--;
}
a[piti] = a[right];
piti = right;
while (begin < end && a[begin] < k)
{
begin++;
}
a[piti] = a[begin];
piti = begin;
}
a[piti] = k;
return piti;
}
五、优化版本
5.1 三数取中
通过以上的版本使我们意识到了一个问题:制约快排效率主要的一个因素为:选k。这个k选得好与不好直接关系到快排的效率问题,所以,有人就提出了三数取中这个方法。
方法为:即知道这组无序数列的首和尾后,我们只需要在首,中,尾这三个数据中,选择一个排在中间的数据作为基准值(keyi),进行快速排序,即可进一步提高快速排序的效率。
int Getmid(int* a, int left, int right)
{
int mid = (left + right) / 2;
if (a[left] > a[right])
{
if (a[right] > a[mid])
{
return right;
}
else if (a[mid] > a[left])
{
return left;
}
else
{
return mid;
}
}
else
{
if (a[right] < a[mid])
{
return right;
}
else if (a[right] < a[left])
{
return left;
}
else
{
return mid;
}
}
}
这时,我们把我们的k值替换为GetMid的返回值即可。
5.2 小区间优化
当我们在进行排序时,如果剩下一个小区间我们仍用快排进行排序时,会降低其效率,那么,这时我们就可以考虑用其他排序来进行排序,从而提高效率。
如:当我们数据量小于10时,我们就可以用插入排序来进行排序。
void InsertSort(int* a, int n)
{
for (int i = 0; i < n - 1; i++)
{
int end = i;
int tmp = a[end + 1];
while (end >= 0)
{
if (tmp < a[end])
{
a[end + 1] = a[end];
end--;
}
else
{
break;
}
}
a[end + 1] = tmp;
}
}
void QuickSort(int* a, int left,int right)
{
if (left >= right)
{
return;
}
if ((right - left + 1) <= 10) //小区间优化
{
InsertSort(a, (right - left + 1));
}
int k = PartSort3(a, left, right);
QuickSort(a, left, k - 1);
QuickSort(a, k + 1, right);
}
六 、非递归实现快排
在用非递归实现快排时,我们要借助栈这个数据结构来辅助实现。
实现思路:
- 入栈一定要保证先入左再入右。
- 取两次栈顶的元素,然后进行单趟排序。
- 划分为[left , k - 1] k [ k + 1 , right ] 进行右、左入栈。
- 循环2、3步骤直到栈为空。
代码实现:
void QuickSortNonR(int* a, int left, int right)
{
ST sk;
STInit(&sk);
STPush(&sk, right);
STPush(&sk, left);
while (!STempty(&sk))
{
int begin = STTop(&sk);
STPop(&sk);
int end = STTop(&sk);
STPop(&sk);
int k = PartSort1(a, begin, end);
if (k + 1 < end)
{
STPush(&sk, end);
STPush(&sk, k + 1);
}
if (begin < k - 1)
{
STPush(&sk, k - 1);
STPush(&sk, begin);
}
}
STDestory(&sk);
}
七、三路划分
为了提高快排效率,有人提出了三路划分,叫我们一起来了解一下吧!
相信大家在快排中会遇到这种情况:一个数组中有多个数据连续情况,这时,我们采用以上版本的话,就会有效率问题。
如若各位不信,可试试用快排做一下这道题目:. - 力扣(LeetCode)
另外这道题目大家可发现这样一种现象:LeetCode官方的C++题解跑不过去。
好了,话不多说,开始寻求解决办法吧!
三路划分实现的大思路其实和前后指针大差不差,不过会有改动地方:把相同的数据放到中间。
这里,我们说一下三路划分思路:
1. k默认取左边位置。
2. left指向最左边,right指向最右边,cur取left下一个位置。
3. cur遇到比left小的就和left交换位置,left++,cur++。
4. cur遇到比left大的就无脑和right交换位置,right--。
5. 遇到相同的值就cur++,直到结束。
代码实现:
void PartSort3Way(int* a, int left, int right)
{
if (left >= right)
{
return;
}
int k = a[left]; //这里直接赋值,不然不好控制
int cur = left + 1;
int begin = left;
int end = right;
while (cur <= right)
{
if (a[cur] < k)
{
swap(&a[cur], &a[left]);
cur++;
left++;
}
else if (a[cur] > k)
{
swap(&a[cur], &a[right]);
right--;
}
else
{
cur++;
}
}
PartSort3Way(a, begin, left - 1);
PartSort3Way(a, right + 1, end);
}
八、introsort
这里,我们再简单介绍一下introsort版本的,它目前是官方版的快排。
introsort是introspective sort采⽤了缩写,他的名字其实表达了他的实现思路,他的思路就是进⾏⾃ 我侦测和反省,快排递归深度太深(sgi stl中使⽤的是深度为2倍排序元素数量的对数值)那就说明在 这种数据序列下,选key出现了问题,性能在快速退化,那么就不要再进⾏快排分割递归了,改换为堆 排序进⾏排序。
它就是可以理解为将堆排,插入排序,快排揉在一起的缝合怪。其实现思想简单,这里就说明一下,主要体现在以下三方面,大家感兴趣可以自行去实现一下。
实现思想:
1. 小区间优化思想:数组⻓度⼩于16的⼩数组,换为插⼊排序,简单递归次数。
2. 定义变量logN检查递归深度:当深度超过2*logN时改⽤堆排序。
3. 选k方面:借助rand函数采用了随机选k的方法。
小结
本文对于快排的实现做了较为深入的讲解,内容有较大难度,大家看完之后,看完后难以理解,可借助画图帮助理解。写排序时,先控制单趟在控制多趟较为容易。好了,本文的内容到这里就结束了,如果觉得有帮助,还请一键三连多多支持一下吧!
完!