这是我参与11月更文挑战的第12天,活动详情查看:2021最后一次更文挑战
算法原理
快排的思想是这样的:如果要排序数组中下标从p到r之间的一组数据,我们选择p到r之间的任意一个数据作为pivot(分区点)。然后遍历p到r之间的数据,将小于pivot的放到左边,将大于pivot的放到右边,将povit放到中间。经过这一步之后,数组p到r之间的数据就分成了3部分,前面p到q-1之间都是小于povit的,中间是povit,后面的q+1到r之间是大于povit的。根据分治、递归的处理思想,我们可以用递归排序下标从p到q-1之间的数据和下标从q+1到r之间的数据,直到区间缩小为1,就说明所有的数据都有序了。
递推公式:quick_sort(p…r) = quick_sort(p…q-1) + quick_sort(q+1, r)
终止条件:p >= r
性能分析
-
算法稳定性:
因为分区过程中涉及交换操作,如果数组中有两个8,其中一个是pivot,经过分区处理后,后面的8就有可能放到了另一个8的前面,先后顺序就颠倒了,所以快速排序是不稳定的排序算法。比如数组[1,2,3,9,8,11,8],取后面的8作为pivot,那么分区后就会将后面的8与9进行交换。 -
时间复杂度:最好、最坏、平均情况
快排也是用递归实现的,所以时间复杂度也可以用递推公式表示。
如果每次分区操作都能正好把数组分成大小接近相等的两个小区间,那快排的时间复杂度递推求解公式跟归并的相同。T(1) = C;n=1时,只需要常量级的执行时间,所以表示为C。
T(n) = 2*T(n/2) + n;n>1所以,快排的时间复杂度也是
O(nlogn)。如果数组中的元素原来已经有序了,比如1,3,5,6,8,若每次选择最后一个元素作为pivot,那每次分区得到的两个区间都是不均等的,需要进行大约n次的分区,才能完成整个快排过程,而每次分区我们平均要扫描大约
n/2个元素,这种情况下,快排的时间复杂度就是O(n^2)。
前面两种情况,一个是分区及其均衡,一个是分区极不均衡,它们分别对应了快排的最好情况时间复杂度和最坏情况时间复杂度。那快排的平均时间复杂度是多少呢?T(n)大部分情况下是O(nlogn),只有在极端情况下才是退化到O(n^2),而且我们也有很多方法将这个概率降低。 -
空间复杂度:快排是一种原地排序算法,空间复杂度是
O(1)
原地交换的实现图解
php实现
/**
* 快排
* @param array $array
* @return array
*/
public function quickSort(array $array)
{
$count = count($array);
$this->quickSortRecursive($array, 0, $count - 1);
return $array;
}
private function quickSortRecursive(array &$array, int $left, int $right)
{
if ($left >= $right) {
return;
}
$q = $this->partition($array, $left, $right);
$this->quickSortRecursive($array, $left, $q - 1);
$this->quickSortRecursive($array, $q + 1, $right);
}
private function partition(array &$array, int $left, int $right)
{
$pivot = $array[$right];
$index = $left;
for ($j = $left; $j < $right; ++$j) {
if ($array[$j] < $pivot) {
$tmp = $array[$j];
$array[$j] = $array[$index];
$array[$index] = $tmp;
$index ++;
}
}
$tmp = $array[$index];
$array[$index] = $array[$right];
$array[$right] = $tmp;
return $index;
}
归并排序与快速排序的区别
归并和快排用的都是分治思想,递推公式和递归代码也非常相似,那它们的区别在哪里呢?
- 归并排序,是先递归调用,再进行合并,合并的时候进行数据的交换。所以它是自下而上的排序方式。何为自下而上?就是先解决子问题,再解决父问题。
- 快速排序,是先分区,在递归调用,分区的时候进行数据的交换。所以它是自上而下的排序方式。何为自上而下?就是先解决父问题,再解决子问题。