快速排序是一种分治的排序算法。它将一个数组分成两个子数组,之后分别对这两部分记录继续进行排序,以达到整个序列有序的目的。
快排的基本思想:由左子数组(有序且没有任何元素大于切分元素)、切分元素和右子数组(有序且没有任何元素小于切分元素)组成的结果数组一定是有序的
快速排序可以与归并排序对比理解:
1、归并排序:递归调用发生在处理整个数组之前;快速排序:递归调用发生在处理整个数组之后。
2、归并排序中,一个数组被等分为两半;在快速排序中,切分(partition)的位置取决于数组的内容。
性能评估:
将长度为 N 的无重复数组排序,快速排序平均需要 次比较(以及 1/6 的交换),最多需要约 次比较
快速排序的三个步骤:
(1)选择基准:在待排序列中,按照某种方式挑出一个元素,作为 “基准”(pivot)
(2)分割操作(partition):以该基准在序列中的实际位置,把序列分成两个子序列。此时,在基准左边的元素都比该基准小,在基准右边的元素都比基准大
(3)递归地对两个序列进行快速排序,直到序列为空或者只有一个元素。
该方法的关键在于切分,切分结果要求数组满足下面三个条件:
- 对于某个 j,a[j] 已经排定;
- a[lo] 到 a[j-1] 中的所有元素都不大于 a[j];
- a[j+1] 到 a[hi] 中的所有元素都不小于 a[j]
快速排序就是递归的调用切分,来进行排序。 由左子数组(有序且没有任何元素大于切分元素)、切分元素和右子数组(有序且没有任何元素小于切分元素)组成的结果数组也一定是有序的。
一般策略:
选择基准的方式
对于分治算法,当每次划分时,算法若都能分成两个等长的子序列时,那么分治算法效率会达到最大。 也就是说,基准的选择是很重要的。选择基准的方式决定了两个分割后两个子序列的长度,进而对整个算法的效率产生决定性影响**。最理想的方法是,选择的基准恰好能把待排序序列分成两个等长的子序列**
三种选择基准的方法:
方法(1):固定位置
思想:取序列的第一个或最后一个元素作为基准
方法(2):随机选取基准
引入的原因:在待排序列是部分有序时,固定选取枢轴使快排效率底下,要缓解这种情况,就引入了随机选取枢轴
思想:取待排序列中任意一个元素作为基准
方法(3):三数取中(median-of-three)
引入的原因: 虽然随机选取枢轴时,减少出现不好分割的几率,但是还是最坏情况下还是O(n^2),要缓解这种情况,就引入了三数取中选取枢轴
分析: 最佳的划分是将待排序的序列分成等长的子序列,最佳的状态我们可以使用序列的中间的值,也就是第N/2个数。可是,这很难算出来,并且会明显减慢快速排序的速度。这样的中值的估计可以通过随机选取三个元素并用它们的中值作为枢纽元而得到。事实上,随机性并没有多大的帮助,因此一般的做法是使用左端、右端和中心位置上的三个元素的中值作为枢纽元。显然使用三数中值分割法消除了预排序输入的不好情形,并且减少快排大约14%的比较次数。
1. 切换插入排序
当待排序序列的长度分割到一定大小后(5~15),使用插入排序
原因:对于很小和部分有序的数组,快排不如插排好。当待排序序列的长度分割到一定大小后,继续分割的效率比插入排序要差,此时可以使用插排而不是快排
和大多数递归排序算法一样,改进快速排序性能的一个简单办法基于以下两点:
- 对于小数组,快速排序比插入排序慢;
- 因为递归,快速排序的 sort() 方法在小数组中也会调用自己。
因此,在排序小数组时应该切换到插入排序。
2. 三向切分
在一次分割结束后,可以把与Key相等的元素聚在一起,继续下次分割时,不用再对与key相等元素分割
三向切分的快速排序: 它从左到右遍历数组一次,维护一个指针 使得 a[lo…lt-1] 中的元素都小于 v,一个指针 使得 a[gt+1…hi] 中的元素都大于 v,一个指针 使得 a[lt…i-1] 中的元素都等于 v,a[i…gt] 中的元素都还未确定,如图 2.3.4 所示。一开始 i 和 lo 相等,我们使用 Comparable 接口(而非 less())对 a[i] 进行三向比较来直接处理以下情况:
- a[i] 小于 v,将 a[lt] 和 a[i] 交换,将 lt 和 i 加一;
- a[i] 大于 v,将 a[gt] 和 a[i] 交换,将 gt 减一;
- a[i] 等于 v,将 i 加一。
这些操作都会保证数组元素不变且缩小 gt-i 的值(这样循环才会结束)。另外,除非和切分元素相等,其他元素都会被交换。
性能:
为对于包含大量重复元素的数组,它将排序时间从线性对数级降低到了线性级别。
但是在数组中重复元素不多的普通情况下它比标准的二分法多使用了很多次交换。有一种更优的三向切分方法,使得三向切分的快速排序比归并排序和其他排序方法在包括重复元素很多的实际应用中更快。
3. 优化递归操作
快排函数在函数尾部有两次递归操作,我们可以对其使用尾递归优化
优点:如果待排序的序列划分极端不平衡,递归的深度将趋近于n,而栈的大小是很有限的,每次递归调用都会耗费一定的栈空间,函数的参数越多,每次递归耗费的空间也越多。优化后,可以缩减堆栈深度,由原来的O(n)缩减为O(logn),将会提高性能。