快速排序原理
快速排序是另一个时间复杂度在nlog(n)级别的排序算法,它的基本思想是这样的:每次找出一个元素,把这个元素放到它有序时应该在的位置,操作完成后,再把该元素左边的元素和右边的元素进行同样的操作,直至整个数组排序完成。因此,在快速排序当中也适用了递归的方法。下面给出它的基础实现:
1 | // |
partition操作解释
其整个过程与归并排序操作类似,最核心的地方在于__partition
方法,它完成了上面所说的,选择一个元素并把它放到应该在的位置上,同时返回了该位置,以便递归的进行。在整个过程中,设置了3个变量,v
表示被选中的元素,它将被放到正确的位置,j
指向v
应该在的位置,i
则进行整个数组的遍历。在这个过程中,v
默认选择了第一个元素,在后面的遍历中,依次考察了v
后面的每一个元素,j
初始位置指向v
,在整个过程中,要保证arr[l+1,j]<v,arr[j+1,r]>v
。在考察arr[i]
与v
的关系时,如果arr[i]<v
,表明该元素应该在v
的左边,把这个元素交换过来,相当于扩充了arr[l+1,j]
当中的元素,因此j++
,如果arr[i]>v
则它就应该在右侧,此时只需要继续考察下一个元素即可。
直到上述过程完成,j
指向了元素v
的位置,因此将两个位置的元素交换,就把v
放到了正确的位置上。__quickSort
方法则对划分出来的两部分继续递归,直至完成。这便是快速排序的基础实现。但该实现有一些缺陷存在,并会导致在某些情况下复杂度的退化,因此有一些优化的方法,后面来补充。
优化1
- 第一点优化与归并排序一样,在数组划分到一定大小的时候,可以对这一部分进行快速排序,不再继续递归。
- 第二点,由于上述快速排序对于元素的选择默认使用了第一个元素,在每次确定位置之后,将数组分为两部分再分别进行排序。与归并排序不同的是,归并排序将数组一分为二,因此可以保证在不断的划分过程中,这棵树的高度是确定的。但快速排序由于选择的元素最后确定的位置并非确定,如果一个近乎有序的数组,将导致数组被分为差异很大的两部分进行排序。在最坏的情况下,一个数组已经有序,此时该结构缺少左子树,退化为一个链表结构,树的高度变为n,此时的复杂度退化为O(n^2)级别。这会导致快速排序的性能急剧下降。
基于上述分析,对于元素的选择,我们可以换一种方式,采用随机的选择
,选择数组中的一个元素,这使得数组划分不平衡的概率大大降低,提高速度,下面是其简单的改进。
1 | //对arr [l,r]区间进行partition操作,找出一个元素应该在的位置 |
优化2,双路快速排序
随机化的选择解决了近乎有序的数组的效率问题,下面考虑一个拥有大量重复值的数组。通过上述的排序过程可知,在选定一个元素后,比该元素小的都放到了左边,则意味着右边所有元素都大于等于该元素。如果出现了一个数组中有大量重复元素,则右边就拥有大量元素,即再次产生了数组不平衡的问题。解决思路是,由原来的单向循环改为前后双向循环。设选定元素为v
,则大于v
的放到右边,小于v
的放到左边。设定由前往后的下标为i
,由后往前下标为j
,若满足前面的条件,则i
、j
继续往前/后考察,在不满足时,交换元素,再继续向前考察。
由前面设定的条件可以看出,当arr[i]=v或arr[j]=v
时,也会发生交换,这样就把相同的元素都分散到了前后两个部分,以此解决前面出现的不平衡问题。这里变化的是partition过程,下面给出具体实现:
1 | //双路快速排序 |
优化3,三路快速排序
经过前面的优化处理,基本上对所出现的特殊情况进行了优化。三路快速排序则对双路进行优化。从双路排序的思想中可以看到,为了平衡数组两部分,对于等于选定点v
的值都进行了一次交换,但实际上,对于相同元素较多的时候,等于v
这一区间也是非常大的,因此可以将数组划分为3个部分,大于v
,等于v
,小于v
。
在这里设定三个值,lt, gt, i
,i为当前考察的元素,lt,gt
分别是数组满足arr[l,lt]<v, arr[lt+1,i)=v, arr[gt,r]>v
。这样,就避免了对多个重复元素的交换操作,下面看一下实现:
1 | /**三路快速排序**/ |
由于数组被分为3部分,在递归时将会有两个下标,因此没有将partition操作独立出来。上面需要注意
lt, i, gt
的初始值,由于arr[l,lt]<v
,在每次找到小于v
的元素的时候都放到这一部分,也就是lt+1
的位置,因此初始值为l
保证了第一个小于v
的元素会放到arr[l]
后面一位;i
是需要考察的元素,因此初始从l+1
开始;gt
指向第一个大于v
的元素,由于它是从后往前考察,因此满足条件时,arr[gt,r]
这部分会扩充,也就是gt--
,所以gt=r+1
满足了第一个大于v
的元素放到了最后一个位置。