快速排序的思想是分治法,当每次划分时,算法若都能分成两个等长的子序列时,那么分治算法效率会达到最大。所以基准枢纽元的选择是很重要,选择基准的方式决定了分割后两个子序列的长度,进而对整个算法的效率产生决定性影响。
通常实现的快速排序没有经过充分考虑的选择那个枢纽元,只是将第一个或最后一个元素用作枢纽元。选择第一个元素作为枢纽元的程序例子见快速排序的两种实现思路二,选择最后一个元素作为枢纽元的程序例子见快速排序的两种实现思路一
快速排序枢纽元选择的优化方法
方法一:随机选取枢纽元
如果输入序列是随机的,那么固定枢纽元为第一个或者最后一个这是可以接受的,但是如果输入是预排序的或者是反序的,那么固定枢纽元就产生一个劣质的分割,因为所有的元素不是被划入S1就是被划入S2。更有甚者,这种情况发生在所有的递归调用中。
实际上,如果第一个元素用作枢纽元而且输入是预先排序的,那么快速排序的时间复杂度为Θ(n^2)。然而,预排序的输入(或者有一大段预排序数据的输入)也是相当常见的,因此,使用第一个元素作为枢纽元的算法效率不是很高的。
随机选取枢纽元是相对安全的策略。由于枢轴的位置是随机的,那么产生的分割也不会总是会出现劣质的分割。在整个数组数字全相等时,仍然是最坏情况,时间复杂度是O(n^2)。实际上,随机化快速排序得到理论最坏情况的可能性仅为1/(2^n)。所以随机化快速排序可以对于绝大多数输入数据达到O(nlogn)的期望时间复杂度。
随机选取枢纽元算法:
1 | //随机选取枢纽元 |
方法二:三数取中分割法选取枢纽元
虽然随机选取枢轴时,减少出现不好分割的几率,但是最坏情况下还是O(n^2),要缓解这种情况,就引入了三数取中选取枢纽,一组N个数的中值是第[N/2]个最大的数。枢纽元的最好的选择是数组的中值。可是,这很难算出来,并且会明显减慢快速排序的速度。这样的中值的估计可以通过随机选取三个元素并用它们的中值作为枢纽元而得到。事实上,随机性并没有多大的帮助,因此一般的做法是使用左端、右端和中心位置上的三个元素的中值作为枢纽元。显然使用三数中值分割法消除了预排序输入的不好情形。
三数取中分割法枢纽元算法:
1 | //三数取中分割法:通过确定nums[low], nums[mid], nums[height]三者之中的那个第二大的元素为枢纽元时,便能尽最大限度保证快速排序算法不会出现O(N ^ 2)的最坏情况。。 |
枢纽元确定后递归快排函数优化方法
方法一:当待排序序列的长度分割到一定大小后,使用插入排序
对于很小和部分有序的数组,快排不如插排好。当待排序序列的长度分割到一定大小后,继续分割的效率比插入排序要差,此时可以使用插排而不是快排.截止范围:待排序序列长度N < 10,进行插入排序,而不是继续划分快排。
1 | //利用插入排序优化 |
方法二:在一次分割结束后,可以把与枢纽元相等的元素聚在一起,继续下次分割时,不用再对与枢纽元相等元素分割
举例:
1 | 举例:待排序序列 1 4 6 7 6 6 7 6 8 6 |
具体实现过程分为两步:
1 | 第一步,在划分过程中,把与枢纽元key相等元素放入数组的两端 |
具体代码实现:
枢纽元通过三数取中分割法后放在第一个元素,此法是从左右两边分别向中间靠拢,所以和枢纽元相等的值会出现在枢纽元左右两边,因此需要左右两边都进行相等枢纽元的聚集。
1 | vector<int> getPartitionGatherKey(vector<int> &nums, int low, int height) |
枢纽元通过三数取中分割法后放在最后一个元素,此法枢纽元左边都是小于等于枢纽元的元素,右边都是大于枢纽元的元素,所以和枢纽元相等的值只会出现在枢纽元左两边,因此只需要左边进行相等枢纽元的聚集。
1 | vector<int> getPartitionGatherKey(vector<int> &nums, int low, int height) |