快速排序的基本思想就是:让每个元素都摆正自己的位置。不多说,直接先来个例子!
现在有下列一组数需要排序,我们先选定一个主元,通常都是选第一个或最后一个。这里我们选择第一个数,即20。我们初步的思想是,将20移动到它应该处在的位置上去!什么意思呢,就是把所有小于20的数挪到它的左边,大于20的数挪到它的右边就完事儿了。
我们维护两个指针,small表示小于20的数,big表示大于20的数,从两边向中间扫描。
我们只需要最后处理20的位置,所以从右端开始扫。第一个遇上的是8,小于20,很显然处在了它不该在的位置,这时候我们要将8挪到small指针这里来,big指针相当于遇到了断点,无法继续移动了,所以只能移动small指针。
small指针一直移动下一个大于20的数的地方便停下来,这时候big指针迎来了机会,它只需要将small指针上的数搬过来便可以继续移动了。
如此循环往复,直至small指针和big指针相遇。
到这里,我们就找到了20应该在的位置,将它放上去!
大功告成!
我们完成了快速排序最基本的一轮行为,接下来,只需要对左边的子数组和右边的子数组分别再重复以上步骤即可排序完整个数组。
代码如下:
public void quickSort(int[] arr, int start, int end) {
if (start >= end) {
return;
}
int small = start;
int big = end;
int standard = arr[small];
while (small < big) {
while (small < big && arr[big] >= standard) {
big--;
}
arr[small] = arr[big];
while (small < big && arr[small] <= standard) {
small++;
}
arr[big] = arr[small];
}
arr[small] = standard;
quickSort(arr, start, small - 1);
quickSort(arr, small + 1, end);
}
上述代码其实是尾递归的,可以将最后一个递归改写成迭代:
public static void quickSort(int[] arr, int start, int end) {
while (start < end) {
int small = start;
int big = end;
int standard = arr[small];
while (small < big) {
while (small < big && arr[big] >= standard) {
big--;
}
arr[small] = arr[big];
while (small < big && arr[small] <= standard) {
small++;
}
arr[big] = arr[small];
}
arr[small] = standard;
quickSort(arr, start, small - 1);
start = small + 1;
}
}
描述完快速排序的基本行为之后,我们再来看看快组排序的性能。
可以看出来,在每一轮的行为中,快排会将整个数组都遍历一遍,即每一轮的时间复杂度都是Θ(n),所以要考察快排的性能,关键点是,到底会分多少轮?
我们来看一种最好的情况,就是每一轮划分都会将数组一分为二
很直观的可以看到,是会划分为lgN轮,每一轮复杂度都是Θ(n),所以总的时间复杂度为Θ(nlgn)。
当然,也可以直接用主定理来算:
T
(
n
)
=
2
T
(
n
/
2
)
+
Θ
(
n
)
T(n) = 2T(n/2) + Θ(n)
T(n)=2T(n/2)+Θ(n)
结果也是一样的。
当然,这是最好的情况,那么最坏的情况是什么呢?
可以想象一下,每一次选定主元的时候,都十分不巧的选到了相应子数组的最大值或者最小值,所以每次划分相当于只是在上一轮的数组基础上减1:
n
+
(
n
−
1
)
+
(
n
−
1
)
+
.
.
.
+
1
=
n
∗
(
n
+
1
)
/
2
n + (n - 1) + (n - 1) + ... + 1 = n * (n + 1) / 2
n+(n−1)+(n−1)+...+1=n∗(n+1)/2
即 Θ(n²) 的时间复杂度。
当然,这其实是很极端的情况,你要相信自己点子没有这么背,每一轮都碰到最坏情况!
所以快速排序是一个最坏情况下时间复杂度达到平方级别的排序算法,那为什么它的应用还这么广泛呢?
因为快排的平均性能还是挺好的,我们前面也讨论过,只有在运气极度不好时才会碰到最坏的情况。
可以这么来考虑,只要划分数组时,子数组长度不是线性减少时,就能达到线性对数阶的性能。因为只要是成比例的划分,哪怕每一轮都是1%和99%,那也是指数级别的减少,指数爆炸可不是说说而已。所以快速排序的平均时间复杂度为Θ(nlgn)。
既然快速排序的性能这么依赖于子数组的划分,那么可不可以想办法能够更公平的划分,让极端情况出现的概率更小呢?
当然是可以了,比如说在选定主元时加入随机,每轮运行前从数组中随机选一个数交换到端点作为主元,然后再进行后续操作。
快速排序是分治算法的一个典型应用,平均性能很好,而且是原址的,不需要额外空间。
好啦,关于快速排序我们就说到这里了!