既然这是一篇主题思想为优化快排的文章,天然就不讨论关于快排的一些定义和基础性的问题,只说快排应该怎么优化。html
首先快排的平均时间复杂度优于不少排序,可是时间复杂度也有和他同样的,也就是堆排序,但为何实际应用中快排要好于堆排呢?算法
缘由主要有三个:数组
答案是确定的,能够说在多数状况下,基本的快排速度是优于其余排序的,可是凡事都有局限性,快速排序对于数据不平衡的数组,重复数组,小数组等状况速度是比较不理想的,这个时候天然就须要优化,来尽量的减少局限性。缓存
咱们知道基本的快速排序选取第一个或最后一个元素做为枢轴,可是,这一直很很差的处理方法。对于这个问题通常有两种处理方法:随机选取枢轴、三数取中(median-of-three)。网络
选三个数(或更多,通常为左中右)做为样本,取其中位数做为枢轴点,这样划分能够尽量的使枢轴在中间,使两边数据更均衡。ide
/*函数做用:取待排序序列中low、mid、high三个位置上数据,选取他们中间的那个数据做为枢轴*/
int PickMiddle(int arr[],int low,int high) {
int mid = (high+low) / 2;//计算数组中间的元素的下标
//使用三数取中法选择枢轴
if (arr[low] > arr[high])
sawp(arr,low,high); //目标: arr[low] <= arr[high]
if (arr[mid] > arr[high])
sawp(arr,mid,high); //目标: arr[mid] <= arr[high]
//以上两步保证把最大的移到最右端
if (arr[mid] > arr[low])
sawp(arr,mid,low); //目标: arr[low] >= arr[mid]
//此时,arr[mid] <= arr[low] <= arr[high]
return arr[low];
//low的位置上保存这三个位置中间的值
//分割时能够直接使用low位置的元素做为枢轴,而不用改变分割函数了
}
复制代码
对于很小和部分有序的数组,快排不如插排好。当待排序序列分割到必定长度后,继续分割的效率比插入排序要差,此时可使用插排而不是快排。函数
if(high-low < 7)
{
InsertSort(L); //进行插入排序
return;
}//else进行正常的快排
复制代码
快速排序对于元素重复率特别高的数组,效率显得很是低下。举个例子,假如在排序过程当中一个子数组已所有为重复元素,则对于此数组排序就应该中止了,但快排算法依然会将其切分为更小的数组。性能
一个简单的改进想法就是将数组分为三部分:小于当前切分元素的部分,等于当前切分元素的部分,大于当前切分元素的部分。用一张图说明就是这样:学习
int QSort(int arr[],int low,int high) {
if(low < high)
{
int lt = low; //low为枢轴位置
int gt = high;
int i=low+1; //low位置的元素为枢轴元素,因此用于比较的元素从low+1开始
int temp = arr[low]; //将枢轴的元素储存到temp中
while(i <= gt)
{
if(arr[i] < temp) //小于枢轴元素的放在lt左边
sawp(arr,lt++,i++); //即交换lt和i位置的元素,此时枢纽位置(lt)右移一位,i也所以右移
else if(arr[i] > temp) //大于枢轴元素的放在gt右边
sawp(arr,i,gt--); //交换i和gt位置的元素,gt须要左移,i因为变为gt位置元素,因此不须要移动
else //相等时,无需交换,只需把i右移一位
i++;
}
//lt-gt的元素已经排定,只需对it左边和gt右边的元素进行递归求解
QSort(arr,low,lt-1);
QSort(arr,gt+1,high);
}
}
复制代码
咱们知道快排是一个递归算法,而递归的问题是若是递归太深容易栈溢出。因此针对快排的优化,还有一个角度是对递归的优化。网上不少文章中都提到一个叫作尾递归优化的东西。优化
为了更好的理解,得先了解了一下什么叫尾递归。
若是一个函数中全部递归形式的调用都出如今函数的末尾,咱们称这个递归函数是尾递归的。当递归调用是整个函数体中最后执行的语句且它的返回值不属于表达式的一部分时,这个递归调用就是尾递归。尾递归函数的特色是在回归过程当中不用作任何操做,这个特性很重要,由于大多数现代的编译器会利用这种特色自动生成优化的代码。
具体用法看这篇文章吧,我就不复述了。 尾调用优化 - 阮一峰的网络日志
了解完尾递归后,再来看看网上流行的快排尾递归优化方法。
关键代码以下
void QSort(int arr[],int low,int high) {
int pivot; //枢轴
while(low<high)//直到把右半边数组划分红最多只有一个元素为止,就排完了!
{
pivot=Partition(arr,low,high);
QSort(arr,low,pivot-1); //对低子表递归排序
low=pivot+1;
}
}
复制代码
首先上面代码中递归调用并非最后一步,甚至最后一行都不是,和咱们上面看到的尾递归的定义不太同样,我查阅了网上不少资料,其中算法导论中7-4章也提到了这个问题。
算法导论中的题目:
中文版本:
原书中的问题是这样的:
The QUICKSORT algorithm of Section 7.1 contains two recursive calls to itself. After the call to PARTITION, the left subarray is recursively sorted and then the right subarray is recursively sorted. The second recursive call in QUICKSORT is not really necessary; it can be avoided by using an iterative control structure. This technique, called tail recursion, is provided automatically by good compilers. Consider the following version of quicksort, which simulates tail recursion.
这个问题也提到尾递归技术,但在后续的描述中更像是描述尾递归的思想,which simulates tail recursion这是书中的原话,意思也很明显,就是模拟尾递归技术,而非真正的尾递归。
那么用这样的方式到底能不能下降递归深度呢?
答案是能,不妨来画一下这两种方法的递归树:
首先设置模拟数组为(1,2,3,4,5,6,7,8,9,10,11),枢轴选取采用三数取中方式
普通双递归的递归树为:
而若是采用模拟尾递归的方法,因为去掉了高子表的递归,而采用直接调用快排处理函数;不难发现其实本质是至关于把右节点擦掉,把右节点的子节点直接链接到当前节点。这样右边的叶深度,也就是最大栈深度就降低了。这个是不须要依赖编译器优化的。
不过这种优化我以为没什么必要,毕竟这只是把递归算法转化为了迭代算法,而全部的递归均可以转化为迭代,为什么又不把另外一个递归写成迭代呢;更况且通过优化后快排的平均递归深度基本为,虽然可能为n,但这种状况在使用了各类优化后,几乎不可能出现了,因此我以为优化算法,最重要的应该是结合具体状况对算法自己优化,而不是粗暴的把递归改为非递归。
参考文献: