快速排序之所以叫快速排序,肯定要配得上它的名字。我们就来看看它是如何这么快的。
快速排序的原理
快速排序是霍尔创建的,是通过以第一个或者最后一个数值作为比较值key,形成一个它的左边比它都小,右边比它都大的情况,然后把这个再成左右两个部分重复这个操作,一直细化。
霍尔版本
而霍尔的方法就是左右(l,r)两个指针向中间靠近,如果l遇到的值比key的值大就停住,r遇到比key小的值也停下,然后交换l,r的值,一直重复这个操作到两个指针相遇,将相遇的值与key交换就是最终要完成的情形
我们来分析一下这个过程
首先r先走找到5比6小,然后l走找到7
然后交换两者
继续这个操作,r先走,l后走找到4和9
然后交换
然后重复,r先走找到3,l后走与它相遇,相遇就停止
然后交换3和key
key就到了中间,形成我们需要到这个情况
接下来就是将这个操作分成左右两个,key左边重复操作,右边也是一样。那么这个操作会迭代出两个分支,然后一直二分下去,一直要l>=r的时候我们就停止。
我们很明显的发现可以用递归来完成这个操作。
提醒点
我一直在强调现r走再l走肯定有原因,主要是保证任何时候key和重合地方交换的时候,k都是比这个值大的。
那么我们就来证明一下:
循环结束的情况只有两种,一种是l撞到r,一种是r撞到l
第一种:因为是l撞到r,说明r已经走了,那么r停的地方一定是比key要大的,所以最后交点比
key大。
第二种:这种情况是出现在上一次,l和r交换后的,原先r的值交给了现在的l,那么现在的r重合于r就是之前r的值,之前r停止是比key大的,所以还是比key大。
综上任何情况都是比key大的。
如果我们反过来先让l走,那么不会出现上面的情况。
挖坑法
挖坑发就是先把key处的值取出来,然后就只有一个坑需要其他的值填,那么就让r向前走找到比key小的值放到坑出,那么现在r的地方又有一个坑,我们就要l向后走找比key大的值填坑。然后又是r,又是l...一直到两个相遇。最后一个坑就由key填下。
前后指针法
前后指针法相对有点抽象:
cur遇到比key小的就和++prev交换
上面是快排最基础的形式
还存在一定的缺陷,我们往下看
缺陷
上面三种都是有缺陷的
当排的是顺序数据的时候时间复杂度会坍缩成N²,并且会导致递归深度太深栈溢出。
霍尔和挖坑是差不多的,当r往前移动会在第一个相遇,然后分为左右,第二次递归的右边会在第二个下标相遇,然后第三个一直到末尾。就是N²。
前后指针法的cur会一直往后到循环结束,然后递归到下一次。也是n²的时间复杂度。
为了防着这种情况,有一个三数取中的算法让l,mid,r中第二大的数搞到最开始,这样可以尽可能的保证一趟排序完后key左右两边的长度大概相同。
然后我们将之前的三段代码写成单趟的:
那么可以变成这样写
优化
我们看到递归是二分的,那么就像二叉树一样。那么最后一层递归量相当于前面的一半,而且最后一层也没有多大长度。我们就可以用其他的排序来解决:
防栈溢出
用递归还是有栈溢出的风险,所以我们安排非递归:
用栈也是可以的,模仿递归。