快速排序(Quick Sort)
- 基于分治策略。
- 核心思想:递归地将待排数组分隔成独立的两部分,其中一部分记录的关键字均比另一部分的关键字小,则可分别对这两部分记录继续进行排序,以达到整个序列有序。
步骤:
- 分解:以 a [ s t a r t ] a[start] a[start] 为基准元素,将 a [ s t a r t , e n d ] a[start,end] a[start,end] 划分成3段( a [ s t a r t , q − 1 ] , a [ q ] , a [ q + 1 , e n d ] a[start,q-1], a[q],a[q+1,end] a[start,q−1],a[q],a[q+1,end]),使得 a [ s t a r t , q − 1 ] a[start,q-1] a[start,q−1] 中的任何元素都小于等于 a [ q ] a[q] a[q],而 [ q + 1 , e n d ] [q+1,end] [q+1,end] 中的任何元素都大于等于 a [ q ] a[q] a[q]。下标 q q q 是在划分的过程中由 a [ s t a r t ] a[start] a[start] 的值决定的。
- 递归求解:通过递归,分别对 a [ s t a r t , q − 1 ] a[start,q-1] a[start,q−1] 和 a [ q + 1 , e n d ] a[q+1,end] a[q+1,end] 再次进行快速排序的划分。
- 合并:因为对 a [ s t a r t , q − 1 ] a[start,q-1] a[start,q−1] 和 a [ q + 1 , e n d ] a[q+1,end] a[q+1,end] 的排序是就地进行的,因此他们都排好序后不需要特意进行合并计算, a [ s t a r t , e n d ] a[start,end] a[start,end] 则已经排好序。
C++ 代码
#include <iostream>
using namespace std;
/*******************************************************************************
* 快速排序(Quick Sort)
将待排数组分隔成独立的两部分,其中一部分记录的关键字均比另一部分的关键字小,则
可分别对这两部分记录继续进行排序,以达到整个序列有序。
********************************************************************************/
// 快速排序核心算法——Partition:
int Partition(int array[], int start, int end)
{
int i = start, j = end + 1;
// 将start暂存在x里面,顺便作为基准元素
int x = array[start];
// 将小于x的元素交换到左边区域,将大于x的元素交换到右边区域
while (true)
{
while (array[++i] < x && i < end)
; // 找到小于基准元素的第一个数
while (array[--j] > x)
; // 找到大于基准元素的第一个数
if (i >= j)
break; // 小于基准元素的第一个数在大于基准元素的第一个数的右边,说明划分完毕
swap(array[i], array[j]);
}
array[start] = array[j]; // 将中间位置的元素放在第一个
array[j] = x; // 将刚才暂存在x中的start(基准元素放在中间)
return j; // 返回中间位置索引
}
int *QuickSort(int array[], int start, int end)
{
int q = Partition(array, start, end);
if (q > start)
QuickSort(array, start, q - 1);
if (q < end)
QuickSort(array, q + 1, end);
return array;
}
快速排序核心算法——Partition
Partition 对 a [ s t a r t , e n d ] a[start,end] a[start,end] 进行划分的时候,以元素 x = a [ s t a r t ] x=a[start] x=a[start] 作为划分的基准,分别从左、右两端开始扩展两个区域 a [ s t a r t , i ] a[start,i] a[start,i] 和 a [ j , e n d ] a[j,end] a[j,end],使 a [ s t a r t , i ] a[start,i] a[start,i] 中的元素小于等于 x x x,而 a [ j , e n d ] a[j,end] a[j,end] 中元素均大于等于 x x x,初始时, i = s t a r t , j = e n d + 1 i=start, j=end+1 i=start,j=end+1。
在循环体中,下标 j j j 逐渐减小, i i i 逐渐增大,直到 a [ i ] ≥ x ≥ a [ j ] a[i] \geq x \geq a[j] a[i]≥x≥a[j]。如果这两个不等式是严格 > > > 的,此时若 i < j i<j i<j,就应该交换 a [ i ] a[i] a[i] 与 a [ j ] a[j] a[j] 的位置。
w h i l e while while 循环重复直至 i ≥ j i \geq j i≥j 时结束,这时 a [ s t a r t , e n d ] a[start,end] a[start,end] 已经被进行划分成 a [ s t a r t , q − 1 ] , a [ q ] , a [ q + 1 , e n d ] a[start,q-1], a[q],a[q+1,end] a[start,q−1],a[q],a[q+1,end] 三段。在Partition 结束时,返回划分点 q = j q=j q=j。
注意:
- 算法中的下标 i i i 和 j j j 不能超出 a [ s t a r t , e n d ] a[start,end] a[start,end] 的下标界限。
- 在快速排序算法中选取 a [ s t a r t ] a[start] a[start] 作为基准可以保证算法的正常结束。如果选用 a [ e n d ] a[end] a[end] 作为基准则有可能会陷入死循环。(当 a [ e n d ] a[end] a[end] 是 a [ s t a r t , e n d ] a[start,end] a[start,end] 的最大元素时)
时间复杂度分析
Partition 部分的计算复杂度为 O ( e n d − s t a r t − 1 ) O(end-start-1) O(end−start−1),是 O ( n ) O(n) O(n) 复杂度。
【快速排序的运行时间与划分的对称性有关。】
最坏情况——不对称划分
基准元素都是很不巧是两端的极值,则时间复杂度如下:
T
(
n
)
=
{
O
(
1
)
n
⩽
1
T
(
n
−
1
)
+
O
(
n
)
n
>
1
T(n)=\left\{\begin{array}{ll} O(1) & n \leqslant 1 \\ T(n-1)+O(n) & n>1 \end{array}\right.
T(n)={O(1)T(n−1)+O(n)n⩽1n>1
解得:
T
(
n
)
=
O
(
n
2
)
T(n)=O(n^2)
T(n)=O(n2)
最好情况——对称划分
基准元素都是很巧是中值,则时间复杂度如下:
T
(
n
)
=
{
O
(
1
)
n
⩽
1
2
T
(
n
/
2
)
+
O
(
n
)
n
>
1
T(n)=\left\{\begin{array}{ll} O(1) & n \leqslant 1 \\ 2T(n/2)+O(n) & n>1 \end{array}\right.
T(n)={O(1)2T(n/2)+O(n)n⩽1n>1
解得:
T
(
n
)
=
O
(
n
log
n
)
T(n)=O(n\log n)
T(n)=O(nlogn)
算法改进——基准元素随机化
改进的思想很简单,就是为了避免上述的最坏情况,防止遇到每次都选到极值元素的倒霉蛋。在数组还没被划分时,在 a [ s t a r t , e n d ] a[start,end] a[start,end] 中随机选出一个元素作为划分基准。
如果想了解其他排序算法可见:算法分析与设计:7大排序算法大汇总(C++)