一、概述
快速排序(Quicksort),又称分区交换排序(partition-exchange sort),简称快排,是一种比较排序算法,最早由 东尼·霍尔(Tony Hoare)提出。在平均状况下,排序
n
n
n 个元素要
O
(
n
log
n
)
O(n \log n)
O(nlogn) 次比较。在最坏状况下则需要
O
(
n
2
)
O(n^2)
O(n2) 次比较,但这种状况并不常见。通常情况下快速排序比其他算法更快,因为它的内部循环(inner loop)可以在大部分的架构上有效率地达成。
二、分析
快速排序使用分治策略(Divide and conquer)来把一个序列(list)分为 较小 和 较大 的 2 个子序列,然后对两个子序列递归排序。
排序步骤为:
- 选择基准值:从数列中挑出一个元素,称为“基准”(pivot),
- 分割:重新排序数列,所有比基准值小的元素摆放在基准前面,所有比基准值大的元素摆在基准后面(与基准值相等的数可以到任何一边)。在这个分割结束之后,对基准值的排序就已经完成。
- 递归排序子序列:递归地将小于基准值元素的子序列和大于基准值元素的子序列排序。
- 递归结束条件:递归数组的大小是 零 或 一,排序结束。
注:快排是不稳定排序,如需实现稳定排序需加入额外的辅助数据。
比如将原本的数据 data
变成 { data, index }
三、实现
不同的基准值方法,对排序的时间性能有决定性影响。下面介绍两种常用的分区法。
1、Lomuto 分区方案
最早由 Nico Lomuto 提出,此方案选择最后一个元素作为基数,由于此方案代码更 紧凑 且 易于理解,常被用于教程。但整体效率低于 Hoare 的原始方案。特别是在整个数组已经有序时,效率下降到 O ( n 2 ) O(n^2) O(n2)
- 排序过程示意
亮黄色为基数
绿色为排序完成
橙色为交换数据
// 对数组进行排序
// list 数据
// lo 最小索引
// hi 最大索引
algorithm quicksort(list, lo, hi)
{
// 判断索引是否合法
if lo >= hi || lo < 0 then
return
// 对数组进行分区并获得基数 pivot 的 index
p := partition(list, lo, hi)
// 对分区进行排序
quicksort(list, lo, p - 1) // 左分区
quicksort(list, p + 1, hi) // 右分区
}
// 将数组分成两个区
algorithm partition(list, lo, hi)
{
pivot := list[hi] // 选择最后一个元素作为基数 pivot
i := lo - 1 // 临时枢轴(分区轴)索引
for j := lo to hi - 1 do
{
// 当前元素小于等于 基数 时
if list[j] <= pivot
{
// 更新分区位置
i := i + 1
// 交换当前元素与临时枢轴索引处的元素
swap list[i] with list[j]
}
}
// 将基数 pivot 移动到分区中间(在较小和较大元素之间)
i := i + 1
swap list[i] with list[hi]
return i // 最终基数的索引
}
2、Hoare 分区法
使用数组的中间元素作为基准值,Hoare 的方案比 Lomuto 的分区方案更有效,因为它平均执行的交换次数减少了三倍。
排序过程示意图
- Hoare 分区伪代码
// 对数组进行排序
// list 数据
// lo 最小索引
// hi 最大索引
algorithm quicksort(list, lo, hi)
{
if(lo >= 0 && hi >= 0 && lo < hi)
{
p := partition(list, lo, hi)
}
quicksort(list, lo, p) // Note: the pivot is now included
quicksort(list, p + 1, hi)
}
algorithm partition(list, lo, hi)
{
// 基准值
pivot := list[ floor((hi + lo) / 2) ] // 取数组中间元素作为基准值
// Left index
i := lo - 1
// Right index
j := hi + 1
loop forever
// 从左往右 找到一个比 基准值 大的数
do i := i + 1 while list[i] < pivot
// 从右往左 找到一个比 基准值 小的数
do j := j - 1 while list[j] > pivot
// 如果下标交叉 则返回
if i ≥ j then return j
// 交互查找到两个数位置
swap list[i] with list[j]
}
3、代码实现
using System.Collections.Generic;
namespace MarsUtil.Algorithm.Sort
{
public class Quicksort
{
#region Lomuto 分区法
// Lomuto 分区法,以最后一个元素做基准值
public void QuicksortByLomuto(List<int> list, int low, int hight)
{
// 判断索引是否合法
if (low >= hight || low < 0)
{
return;
}
// 对数组进行分区并获得基准值 pivot 的 index
int pivotIndex = _PartitionByLomuto(list, low, hight);
// 对分区进行排序
QuicksortByLomuto(list, low, pivotIndex - 1); // 左分区
QuicksortByLomuto(list, pivotIndex + 1, hight); // 右分区
}
private int _PartitionByLomuto(List<int> list, int low, int hight)
{
int pivot = list[hight]; // 选择最后一个元素作为基准值 pivot
int i = low - 1; // 临时枢轴(分区轴)索引
for (int j = low; j < hight; j++)
{
// 当前元素小于等于 基准值 时
if (list[j] <= pivot)
{
// 更新分区位置
i += 1;
// 交换当前元素与临时枢轴索引处的元素
_Swap(list, i, j);
}
}
// 将基准值 pivot 移动到分区中间(在较小和较大元素之间)
i += 1;
_Swap(list, i, hight);
// 最终基准值的索引
return i;
}
#endregion
#region Hoare 分区法
// Hoare 分区法 使用数组的中间元素作为基准值
// 对数组进行排序
// list 数据
// lo 最小索引
// hi 最大索引
public void QuicksortByHoare(List<int> list, int low, int hight)
{
// 判断索引是否合法
if (low >= hight || low < 0)
{
return;
}
int p = _PartitionByHoare(list, low, hight);
QuicksortByHoare(list, low, p); // Note: the pivot is now included
QuicksortByHoare(list, p + 1, hight);
}
// 将数组分成两个区
private int _PartitionByHoare(List<int> list, int lo, int hi)
{
// 基准值
int pivot = list[(int)((hi + lo) / 2.0)]; // 取数组中间元素作为基准值
// Left index
int i = lo - 1;
// Right index
int j = hi + 1;
while (true)
{
// Move the left index to the right at least once and while the element at
// the left index is less than the pivot
i += 1;
while (list[i] < pivot)
{
i += 1;
}
// Move the right index to the left at least once and while the element at
// the right index is greater than the pivot
j -= 1;
while (list[j] > pivot)
{
j -= 1;
}
// If the indices crossed, return
if (i >= j)
{
return j;
}
// Swap the elements at the left and right indices
_Swap(list, i, j);
}
}
#endregion
private void _Swap(List<int> list, int index1, int index2)
{
int temp = list[index1];
list[index1] = list[index2];
list[index2] = temp;
}
}
}
四、复杂度分析
1、时间复杂度
快排的平均时间复杂度为
O
(
n
log
n
)
O(n\log n)
O(nlogn) ,具体求证果然如下所示,其中
n
−
1
n - 1
n−1 为查找基准值的花费,
i
i
i 代表分区后第一段数据的个数,
(
n
−
i
−
1
)
(n - i - 1)
(n−i−1) 为第二段数据的个数,这里的求和是罗列所有的分区情况并最终求平均
1
n
\frac{1}{n}
n1。
计算过程
求解递归给出
C
(
n
)
=
2
n
ln
n
≈
1.39
n
log
2
n
C ( n ) = 2 n \ln n ≈ 1.39 n \log_2 n
C(n)=2nlnn≈1.39nlog2n。
舍去常数最终结果为:
O
(
n
log
n
)
O(n\log n)
O(nlogn)
2、空间复杂度
快速排序所使用的空间,依照使用的版本而定。使用原地(in-place)分割的快速排序版本,即上面两个版本,在任何递归调用前,仅会使用固定的额外空间。然而,如果需要产生 O ( log n ) O(\log n) O(logn) 嵌套递归调用,它需要在他们每一个存储一个固定数量的信息。因为最好的情况下需要 O(log n) }次的嵌套递归调用,所以它需要 O ( log n ) O(\log n) O(logn) 的空间。最坏情况下需要 O ( n ) O(n) O(n) 次嵌套递归调用,因此需要 O ( n ) O(n) O(n) 的空间。
参考资料
[1] 快速排序:https://zh.wikipedia.org/wiki/%E5%BF%AB%E9%80%9F%E6%8E%92%E5%BA%8F
[2] Quicksort:https://en.wikipedia.org/wiki/Quicksort
欢迎关注个人公众号,实时推送最新博文!