快速排序是对冒泡排序的一种改进,由 C.A.R.Hoare(Charles Antony Richard Hoare,东尼·霍尔)在1962年提出。快速排序是相同数量级的排序算法中,平均性能最好的算法,是内部排序实现的优选算法。
基本思想
快速排序的基本思想是,通过一趟排序将要排序的数据分割成独立的两部分,其中一部分的所有数据比另一部分的所有数据要小,再按这种方法对这两部分数据分别进行快速排序,整个排序过程可以递归进行,使整个数据变成有序序列。
从每次划分的结果来看,其思想还是冒泡:将小于中枢值的元素冒泡到一侧,将大于中枢值的元素冒泡到另一侧。与冒泡排序每次只能交换相邻的两个元素不同的是,快速排序是跳跃式的交换,交换的距离很大,因此总的比较和交换次数少了很多,速度也快了不少。
和归并排序一样,快速排序也是基于分治技术实现的算法。与归并排序按照元素在列表中的位置进行划分不同,快速排序是按照元素的值进行划分。
伪代码实现
快速排序的核心操作是选择一个元素,对列表中的元素进行划分。因为一次划分,就有一个元素落到最终位置,所以快速排序无需进行合并操作,只需递推的划分列表,直到待划分列表的长度变为1。假设n个元素使用数组存储,快速排序的伪代码实现如下:
假设数组A[0...n-1]表示待排序数组
QuickSort(A[0...n-1])
QuickSort(A[0...n-1], 0, n-1)
QuickSort(A[0...n-1], l, r)
// 回归条件:待排序区间小于等于0(只有一个元素)
if(l >= r)
return
pivot = Partition(A[0...n-1], l, r)
// 递推
QuickSort(A[0...n-1], l, pivot)
QuickSort(A[0...n-1], pivot, r)
Partition(A[0...n-1], low, high)
// 注意:这里使用待划分数组的第一个元素作为中轴元素
pivot = A[low]
while(low < high) then
while(A[high]>=pivot) then
high--;
Swap(A[0...n-1], high, low)
while(A[low]<=pivot) then
low++;
Swap(A[0...n-1], low, high)
return low
Swap(A[0...n-1], src, dst)
tmp = A[src]
A[src] = A[dst]
A[dst] = tmp
接下来分析该算法的执行效率。根据算法效率分析一文,算法的执行效率受增长次数的影响。显然,在不同的场景下,Partition和QuickSort增长次数可能不同。考虑以下场景,如果每次Partition都位于子数组的中点,那么Partition的执行效率降为
O
(
1
)
O(1)
O(1),该算法的执行效率将主要受递归式(
O
(
l
o
g
2
n
)
O(log_2^n)
O(log2n))影响。根据分治法一文,此时可以采用主定理(快速排序是基于分治技术实现的算法)分析其执行效率。基于比较次数
C
(
n
)
C(n)
C(n)的递推关系式如下:
当
n
>
1
时
,
C
(
n
)
=
2
C
(
n
/
2
)
+
n
当n>1时,C(n) = 2C(n/2) + n
当n>1时,C(n)=2C(n/2)+n
C
(
1
)
=
0
C(1) = 0
C(1)=0
根据代入法,代入主方法公式(
T
(
n
)
=
a
T
(
n
/
b
)
+
f
(
n
)
T(n) = aT(n/b) + f(n)
T(n)=aT(n/b)+f(n)),可知a=2,b=2,
f
(
n
)
=
n
f(n)=n
f(n)=n,经计算
n
l
o
g
b
a
=
n
n{log_b^a} = n
nlogba=n。根据主定理
T
(
n
)
=
θ
(
n
l
o
g
b
a
l
o
g
k
+
1
n
)
=
θ
(
n
l
o
g
n
)
T(n)= θ(n{log_b^alog^{k+1}n})=θ(nlog n)
T(n)=θ(nlogbalogk+1n)=θ(nlogn)。
如果每次Partition都处于极端,两个数组有一个子数组为空,而另一个子数组仅比划分的数组少一个元素。当数组已经有序时,会出现这种情况。该场景下,递归式的执行效率降为
O
(
1
)
O(1)
O(1),该算法的执行效率将主要受Partition(
O
(
n
)
O(n)
O(n))的影响。如果A[0…n-1]是严格递增数组,如果将A[0]作为中枢元素,则从左到右扫描,比较次数是1,从右到左扫描,比较次数是n。依次类推,直到只剩两个待排序元素。此时比较次数是3次。根据求和公式,可得到下面的公式:
C
(
n
)
=
(
n
+
1
)
+
n
+
.
.
.
+
3
=
(
n
+
1
)
+
n
+
.
.
.
+
3
+
2
+
1
−
2
−
1
C(n) = (n+1) + n + ... + 3 = (n+1) + n + ... + 3 + 2 + 1 - 2 -1
C(n)=(n+1)+n+...+3=(n+1)+n+...+3+2+1−2−1
C
(
n
)
=
(
n
+
1
)
∗
(
n
+
2
)
/
2
−
3
=
θ
(
n
2
)
C(n) = (n+1)*(n+2)/2 - 3=θ(n^2)
C(n)=(n+1)∗(n+2)/2−3=θ(n2)
以上两种情况,分别是快速排序的最优执行效率(
O
(
n
l
o
g
n
)
O(nlog n)
O(nlogn))和最差执行效率(
O
(
n
2
)
O(n^2)
O(n2)),为了了解快速的排序的实用性,还需计算其平均情况下的效率。快速排序的平均执行效率的计算不再本文体现,有兴趣的同学可以阅读《算法设计与分析基础》一书的快速排序一节。从结论来看,快速排序的平均执行效率约为
1.39
n
l
o
g
n
1.39nlog n
1.39nlogn,接近最优执行效率。此外,针对快速排序,还有很多的优化策略,这里不再展开。这里给出java版本实现:
public class QuickSort {
public void sort(int[] array) {
if (array == null || array.length <= 1) {
return;
}
sort(array, 0, array.length - 1);
}
private void sort(int[] array, int left, int right) {
if (left >= right) {
return;
}
int pivot = partition(array, left, right);
sort(array, left, pivot - 1);
sort(array, pivot + 1, right);
}
private int partition(int[] array, int low, int high) {
int pivot = array[low];
while (low < high) {
while(low < high && array[high] >= pivot) {
high--;
}
swap(array, high, low);
while (low < high && array[low] <= pivot) {
low++;
}
swap(array, low, high);
}
return low;
}
private void swap(int[] array, int srcSubscript, int dstSubscript) {
if (srcSubscript == dstSubscript) {
return;
}
int temp = array[srcSubscript];
array[srcSubscript] = array[dstSubscript];
array[dstSubscript] = temp;
}
}
总结
快速排序本质是使用分治法实现的冒泡排序。在最坏情况下,快速排序的时间复杂度和冒泡排序一样( n 2 n^2 n2)。而在最优情况下,快速排序的时间复杂度和归并排序的时间复杂度一样( n l o g n nlog n nlogn)。因为快速排序的平均时间复杂度与最优情况下的时间复杂度接近,所以使其成为优选的内部排序算法。
参考
《算法设计与分析基础》 第三版 Anany Levitin 著 潘彦 译
《数据结构 严蔚敏 吴伟民 著
https://leetcode-cn.com/problems/sort-an-array/ 排序数组
http://data.biancheng.net/view/117.html 快速排序算法详解(原理、实现和时间复杂度)
https://blog.csdn.net/wangxufa/article/details/121732018 分治法( Divide and Conquer)
https://blog.csdn.net/wangxufa/article/details/121198034 算法效率分析
https://leetcode-cn.com/problems/sort-an-array/ 排序数组