作者:小树苗渴望变成参天大树
作者宣言:认真写好每一篇博客
作者gitee:gitee
如 果 你 喜 欢 作 者 的 文 章 ,就 给 作 者 点 点 关 注 吧!
今天讲一种不一样的排序,听名字就知道这个排序不拐弯抹角的,我们来看看它又多快速,并且快速排序的前三种方法都是递归思想,最后一种是非递归,我们就来重点介绍着集中方法来实现快排(以升序为例)
快速排序是Hoare于1962年提出的一种二叉树结构的交换排序方法,其基本思想为:任取待排序元素序列中的某元素作为基准值,按照该排序码将待排序集合分割成两子序列,左子序列中所有元素均小于基准值,右子序列中所有元素均大于基准值,然后最左右子序列重复该过程,直到所有元素都排列在相应位置上为止。
我们来看看动图,再来画图详细给大家介绍一下:
我们选择一个keyi,保持不动,a[keyi]作为关键字,一趟排序下来这个关键字左边的值都比它小,右边的值都比它大,说明这个关键字来到了它最终来到的地方
通过左右指针将小的数跳到关键字的左边,大的数跳到关键字的右边,这个图表示的一趟排序接下来看看画图分析
通过这个图,我们看到要注意的两个细节,一个是找大找小的左右判断,不能错过了,而是判断条件要不要加等于号,把这两个细节掌握号,单趟就掌握好了,我们来看看单趟排序的代码吧:
int partsort1(int* a, int left, int right)
{
int keyi = left;
while (left < right)
{
while (left<right && a[right]>=a[keyi])//找小
{
right--;
}
while (left < right && a[left] <= a[keyi])//找大
{
left++;
}
swap(&a[left], &a[right]);//交换
}
swap(&a[keyi], &a[left]);//结束后,把keyi位置的值和左边的值交换
keyi = left;//改变keyi的位置
}
看单趟排序结果:
相信大家看到这里对于函数体里面的内容已经了解了,但是为什么我的函数设计要带返回值呢??
原因是我再前言就讲过,快排的前三种都是递归的方法,思想都是一样的,就是单趟排序的思想不一样,因为一趟排序之后,关键字会跳到它最终的位置,下一趟排序,它就不需要参加排序,只需要把它左边区间和右边你区间进行排序,返回这个关键字的下标,目的是方便递归时找到左右区间的范围
void quicksort(int* a, int left, int right)
{
if (left >= right)
{
return;
}
int keyi = partsort1(a, left, right);
quicksort(a, left, keyi - 1);//对关键字左区间排序
quicksort(a, keyi+1, right);//对关键字右区间排序
}
相比较而言,这里的递归还没有二叉树那些递归难理解,主要理解单趟排序的思想,就行了,接下来的两种方法只需要修改partsort函数体里面的内容就行了,我们开始介绍下面两种思想
挖坑法是后面的牛人觉得hoare的方法有点难理解,又想出来的一种新的方法,思想是,把数组的最左边和最右边先当成一个坑,把这个坑的数据先保存起来,这样有数过来就直接放到这个坑位里面,不怕原数据被覆盖了,这个数就变成新的坑位,采取的还是左边找大,右边找小,找到就放到坑位里面,同时找到的位置变成新的坑位
我们先来看看动图演示:
我们再来看看图解:
通过这个图,我们要注意结束条件和找大找小条件都是和第一种一样的那我们来看看这个单趟排序的代码时怎样的:
int partsort2(int* a, int left, int right)
{
int key = a[left];//将坑位的关键字保存起来
int pivot = left;//定义一个坑位下标
while (left < right)
{
while (left < right && a[right] >= key)//找小
{
right--;
}
a[pivot] = a[right];//把找到小的数放到左坑
pivot = right;
while (left < right && a[left] <= key)//找大
{
left++;
}
a[pivot] = a[left];//把找到大的数放到右坑
pivot = left;
}
a[pivot] = key;//把保存的关键字放到最后的坑位
pivot = left;
return pivot;
}
和我们画的图解结果一样,这两种单趟的大思想都是把大的数快速跳到右边,比较小的跳到左边,关键字跳到它最终要出现的位置,最后都是通过递归再对左右区间进行排序
我们来看最终的快速排序:
void quicksort(int* a, int left, int right)
{
if (left >= right)
{
return;
}
int keyi = partsort2(a, left, right);//就改了一下这个函数体,其余和之前一样
quicksort(a, left, keyi - 1);
quicksort(a, keyi+1, right);
}
相信大家看到这里对于挖坑法应该理解,希望大家下来可以取尝试理解一下,接下来我将讲解另一种方法,前后指针法
这个方法相比较而言比前面两种更好,而且不容易出错,那我就来介绍一下这种方法,思想是:通过一个cur指针来找小,找到把prev指针+1把值进行交换,大的数跳到前面,小的数跳到后面,+1找到的是大的数,因为是cur跳过的数,所以是大的数,然后重复上面的步骤,知道cur结束,最后把keyi的值和prev的值进行交换
我们来看看动图演示:
接下来看看图解:
这个方法理解了就很好理解,一定要多画图,接下来我们看一下单趟排序的代码:
int partsort3(int* a, int left, int right)
{
int prev = left;
int cur = left + 1;
int keyi = left;
while (cur <= right)//结束条件
{
if (a[cur] < a[keyi])//找到小
{
++prev;//就++prev
swap(&a[prev], &a[cur]);//两者交换
}
cur++;//cur一直都需要++
}
swap(&a[prev], &a[keyi]);//循环结束
keyi = prev;
return keyi;
}
这个方法的好处就在于些的时候不容易出错,就一趟遍历即可,前两种方法在条件判断的时候容易出错,我们来看看这个方法的完整快排代码:
void quicksort(int* a, int left, int right)
{
if (left >= right)
{
return;
}
int keyi = partsort3(a, left, right);
quicksort(a, left, keyi - 1);
quicksort(a, keyi+1, right);
}
希望这三种方法你可以学会,接下来我要将这三种方法还可以进行优化。
我们递归的思想大致是这样的,每增加一行的递归就会选出两倍的关键字到最终的位置,但是在有序的情况,我们看看会怎么样
这样看来快排在有序的时间效率最低, 那我们就把它弄成不有序,有两种方法,一个是随机数法,一个是三数取中
一、我们来看看随机数,随便取一个a[keyi]然后和左边交换,在进行排序,来看代码:
void quicksort(int* a, int left, int right)
{
if (left >= right)
{
return;
}
int mid = left+rand() % (right - left + 1);
if (mid != left)
{
swap(&a[left], &a[mid]);
}
int keyi = partsort3(a, left, right);
quicksort(a, left, keyi - 1);
quicksort(a, keyi+1, right);
}
为什么要加一个left,防止是递归右区间的时候。
我们再来看看三数取中,意思是在三个数中选出中间那个数,我们来看看代码:
int GetMid(int* a, int left, int right)
{
int mid = (right - left) / 2;
if (a[left] < a[mid])
{
if (a[mid] < a[right])//left mid right
{
return mid;
}
else if (a[right] < a[left])//right left mid
{
return left;
}
else
{
return right;//left right mid
}
}
else//a[left] >= a[mid]
{
if (a[right] > a[left])//mid left right
{
return left;
}
else if (a[mid] > a[right]) //right mid left
{
return mid;
}
else
{
return right;//mid right left
}
}
}
也是选出来和最左边的数交换一下,但整体而言三叔取中用到比较多,因为随机数选出来的可能刚好是最小的keyi,这样和没选没啥区别,而三数取中保证选出来的keyi不是最小的
这里我就不在给大家测试性能了,如果大家感兴趣可以取我的这篇测排序性能博客调用我的函数去测试,对于快排尽量数据搞多些,效果明显,也可以和其他排序进行对比
对于优化其实还有一种方法就是小区间优化,对于数据量不是那么大的时候不需要使用递归来做,我们可以使用直接插入排序来做,具体为什么我就不做介绍了,大家了解一下就好了,反正越到最后递归的次数越多,浪费的时间就多,用直接插入排序节省一点时间,像希尔排序还要分组没必要和选择排序最好最坏情况一样的,直接插入排序在有序的时候比较好,所以选择了直接插入排序:
我们来看看怎么写的:
void quicksort(int* a, int left, int right)
{
if (left >= right)
{
return;
}
int mid = GetMid(a, left, right);
int keyi = partsort3(a, left, right);
if (keyi - 1 - left > 10)
{
quicksort(a, left, keyi - 1);
quicksort(a, keyi + 1, right);
}
else
{
insertsort(a + left, keyi - 1 - left + 1);//a+left是左区间起始位置,keyi - 1 - left + 1元素个数
}
if ((right - keyi - 1) > 10)
{
quicksort(a, left, keyi - 1);
quicksort(a, keyi + 1, right);
}
else
{
insertsort(a + keyi + 1, right - keyi - 1 + 1);// a + keyi + 1是右区间起始位置,right-keyi-1+1元素个数
}
}
这差不多是最好效率的快排排序了,大家可以把优化的和不优化的都测测看看性能是不是提高了,接下来我们来讲非递归的快排了。
我们递归的本质是,开辟先的函数空间,主要保存的左右区间的下标,我们可以通过栈来实现,分别把左右区间入栈,然后在进行单趟排序,调用之前的方法,然后再左右区间入栈
来看看图解:
大家跟着我的步骤走,我·采用的单趟排序是hoare法,我们来看代码:
void QuickSortNonR(int* a, int left, int right)
{
stack ST;
STackInit(&ST);
if (left >= right)
return;
StackPush(&ST, right);//先入右
StackPush(&ST,left);//再入左
while (!StackEmpty(&ST))
{
int left = StackTop(&ST);//出左
StackPop(&ST);
int right = StackTop(&ST);出右
StackPop(&ST);
int keyi = partsort1(a, left, right);//前三种方法都可以
if (keyi - 1 - left >=1)
{
StackPush(&ST, keyi - 1);//左区间右边入
StackPush(&ST, left);//左区间左边入
}
if (right - keyi - 1 >= 1)
{
StackPush(&ST, right);//右区间右边入
StackPush(&ST, keyi+1);//右区间左边入
}
}
}
这个方法我们要先导入一个之前写过的栈,再之前的博客有介绍过,然后就利用栈的特性来实现快排的非递归,大家一定要多画画图。希望我这个图解能给大家解答困惑
今天加的内容非常的多,大家一定要多消化消化,其实快速排序是选择排序家族,冒泡也是选择家族的一种,因为冒泡比较简单,我就没有和快排一起介绍了,希望大家可以知道,今天的,关于时间复杂度我可能再后期的博客会更新,把前面集中博客进行对比的介绍,到时候欢迎大家过来支持一下,今天的知识就先分享到这里了,我们下篇再见