快速排序
基本思路
说到快速排序,我们得先聊一聊分治法。分治法共三个步骤:
- 分解 - 分解原问题为若干子问题,这些子问题相互独立并且是原问题的较小规模的实例。
- 解决 - 递归「分解」子问题,直到子问题达到临界条件无法再进行更小的分解时,求解该子问题。
- 合并 - 合并子问题的解,即得到原问题的解。
分治法是一种强有力的编程思想,分而治之,将复杂的问题分解为规模小的简单的子问题,再逐个解决这些简单的子问题,最后将子问题的结果合并起来,得到复杂问题的答案。
快速排序就是用到了分治法的思想,我们以数组arr[p...r]为例,来说明快排的思路:
- 分解: 数组arr[p...r]分解为两个子数组arr[p...q-1]和arr[q+1...r],使得arr[q]大于arr[p...q-1]中的所有元素,并且arr[q]小于arr[q+1...r]中的所有元素。
- 解决:递归地对两个子数组进行快速排序。
- 合并:原址排序,不需要合并操作。
除了分治法的思想,快速排序的另一个关键点在于分解这一步,称之为切分。我们可以取arr[p]为切分元素,然后从数组的最左边开始向右遍历直到找到一个大于它的元素A,再从数组的最右边向左遍历直到找到一个小于它的元素B,此时分两种情况:
- 当元素A的索引小于元素B的索引,想象一下,此时上述的两次遍历的指针还没有相遇过。我们交换元素A和B,继续上述的两次遍历,直到遇到第二种情况。
- 当元素A的索引大于等于元素B的索引,此时两次遍历的指针已经相遇交错了,即保证了数组arr内的所有元素都被遍历了最少一次,我们交换切分元素arr[p]和元素B,就完成了分解操作,而分解操作中描述的arr[q]就是切分元素arr[p]新的位置。
php代码实现快速排序
将数组的第一个元素作为切分元素实现切分函数
function partition(&$arr, $lo, $hi) {
$i = $lo;
$j = $hi + 1;
while (1) {
// 从$lo+1 开始找一个比lo大的数{$i}
while ($arr[$lo] > $arr[++$i]) {
if ($i == $hi) break;
}
// 从最右边往左找一个比lo小的数{$j}
while ($arr[$lo] < $arr[--$j]) {
if ($j == $lo) break;
}
// 假使$i >= $j, 跳出循环
if ($i >= $j) break;
// 假使$i < $j, $i的值 与 $j的值交换, 继续2,3步骤
$tmp = $arr[$j];
$arr[$j] = $arr[$i];
$arr[$i] = $tmp;
}
// 假使$i >= $j, $j 的值与$lo的值交换, 返回$j
$tmp = $arr[$j];
$arr[$j] = $arr[$lo];
$arr[$lo] = $tmp;
return $j;
}
调用切分函数实现快速排序
function quick_sort(&$arr) {
quick($arr, 0, count($arr) - 1);
}
function quick(&$arr, $lo, $hi) {
if ($lo >= $hi) return;
$j = partition($arr, $lo, $hi);
quick($arr, $lo, $j - 1);
quick($arr, $j + 1, $hi);
}
测试代码
$arr = [];
for ($i = 0; $i < 100; $i++) {
$arr[$i] = rand(0, 10000);
}
print_r($arr);
quick_sort($arr);
print_r($arr);
性能
空间复杂度
虽然快速排序是原地排序,但是在递归调用的过程中也使用了栈空间,平均空间复杂度为: \(O(\lg n)\), 最坏情况下空间复杂度为\(O(n)\)。
时间复杂度
切分函数进行n=r-p+1次比较和小于n次交换,可知每一次切分的时间复杂度是\(O(n)\)。
快速排序的时间复杂度≈切分函数的时间复杂度*递归分解的次数。
在最坏的情况下,即数组已经有序,需要递归分解n次,此时的时间复杂度会达到\(O(n ^ 2)\)。在我们完全可以在代码层面上避免这种最坏的情况,在此提供两种思路:
- 在排序函数入口处,将数组的元素随机打乱。
- 在切分函数入口处,随机获取数组的一个元素A,把A与数组的第一个元素,即原切分元素交换位置,目的是让切分元素随机化。
在数组元素次序随机的情况下,递归的次数趋向于二分法,即分解\(\lg n\)次,可得快排的平均时间复杂度为\(O(N * \lg n)\)。我们总可以做到快速排序的随机化,可以忽略最坏的情况,故此快速排序是一个高效的算法。