快速排序
本页面将简要介绍快速排序。
定义
快速排序(英语:Quicksort),又称分区交换排序(英语:partition-exchange sort),简称「快排」,是一种被广泛运用的排序算法。
基本原理与实现
过程
快速排序的工作原理是通过 分治 的方式来将一个数组排序。
快速排序分为三个过程:
- 将数列划分为两部分(要求保证相对大小关系);
- 递归到两个子序列中分别进行快速排序;
- 不用合并,因为此时数列已经完全有序。
和归并排序不同,第一步并不是直接分成前后两个序列,而是在分的过程中要保证相对大小关系。具体来说,第一步要是要把数列分成两个部分,然后保证前一个子数列中的数都小于后一个子数列中的数。为了保证平均时间复杂度,一般是随机选择一个数
之后,维护一前一后两个指针
其实,快速排序没有指定应如何具体实现第一步,不论是选择
第三步中的序列已经分别有序且第一个序列中的数都小于第二个数,所以直接拼接起来就好了。
=== "C++1"
```cpp
struct Range {
int start, end;
Range(int s = 0, int e = 0) { start = s, end = e; }
};
template <typename T>
void quick_sort(T arr[], const int len) {
if (len <= 0) return;
Range r[len];
int p = 0;
r[p++] = Range(0, len - 1);
while (p) {
Range range = r[--p];
if (range.start >= range.end) continue;
T mid = arr[range.end];
int left = range.start, right = range.end - 1;
while (left < right) {
while (arr[left] < mid && left < right) left++;
while (arr[right] >= mid && left < right) right--;
std::swap(arr[left], arr[right]);
}
if (arr[left] >= arr[range.end])
std::swap(arr[left], arr[range.end]);
else
left++;
r[p++] = Range(range.start, left - 1);
r[p++] = Range(left + 1, range.end);
}
}
```
=== "Python1"
```python
def quick_sort(alist, first, last):
if first >= last:
return
mid_value = alist[first]
low = first
high = last
while low < high:
while low < high and alist[high] >= mid_value:
high -= 1
alist[low] = alist[high]
while low < high and alist[low] < mid_value:
low += 1
alist[high] = alist[low]
alist[low] = mid_value
quick_sort(alist, first, low - 1)
quick_sort(alist, low + 1, last)
```
性质
稳定性
快速排序是一种不稳定的排序算法。
时间复杂度
快速排序的最优时间复杂度和平均时间复杂度为
对于最优情况,每一次选择的分界值都是序列的中位数,此时算法时间复杂度满足的递推式为
对于最坏情况,每一次选择的分界值都是序列的最值,此时算法时间复杂度满足的递推式为
对于平均情况,每一次选择的分界值可以看作是等概率随机的。
下面我们来证明这种情况下算法的时间复杂度是
引理 1: 当对
由于在每次划分元素的过程中,都会选择一个元素作为分界,所以划分元素的过程至多发生
设
显然每次选取的分界值是不同的,而元素只会和分界值比较,所以总比较次数
由期望的线性性,
引理 2:
先证必要性,即若
若
再证充分性,即若
不失一般地,假设
考虑计算
所以
由此,快速排序的期望时间复杂度为
在实践中,几乎不可能达到最坏情况,而快速排序的内存访问遵循局部性原理,所以多数情况下快速排序的表现大幅优于堆排序等其他复杂度为
优化
朴素优化思想
如果仅按照上文所述的基本思想来实现快速排序(或者是直接照抄模板)的话,那大概率是通不过 P1177【模板】快速排序 这道模板的。因为有毒瘤数据能够把朴素的快速排序卡成
所以,我们需要对朴素快速排序思想加以优化。较为常见的优化思路有以下三种3。
- 通过 三数取中(即选取第一个、最后一个以及中间的元素中的中位数) 的方法来选择两个子序列的分界元素(即比较基准)。这样可以避免极端数据(如升序序列或降序序列)带来的退化;
- 当序列较短时,使用 插入排序 的效率更高;
- 每趟排序后,将与分界元素相等的元素聚集在分界元素周围,这样可以避免极端数据(如序列中大部分元素都相等)带来的退化。
下面列举了几种较为成熟的快速排序优化方式。
三路快速排序
定义
三路快速排序(英语:3-way Radix Quicksort)是快速排序和 基数排序 的混合。它的算法思想基于 荷兰国旗问题 的解法。
过程
与原始的快速排序不同,三路快速排序在随机选取分界点
性质
三路快速排序在处理含有多个重复值的数组时,效率远高于原始快速排序。其最佳时间复杂度为
实现
三路快速排序实现起来非常简单,下面给出了一种三路快排的 C++ 实现。
=== "C++"
```cpp
// 模板的 T 参数表示元素的类型,此类型需要定义小于(<)运算
template <typename T>
// arr 为需要被排序的数组,len 为数组长度
void quick_sort(T arr[], const int len) {
if (len <= 1) return;
// 随机选择基准(pivot)
const T pivot = arr[rand() % len];
// i:当前操作的元素
// j:第一个等于 pivot 的元素
// k:第一个大于 pivot 的元素
int i = 0, j = 0, k = len;
// 完成一趟三路快排,将序列分为:
// 小于 pivot 的元素| 等于 pivot 的元素 | 大于 pivot 的元素
while (i < k) {
if (arr[i] < pivot)
swap(arr[i++], arr[j++]);
else if (pivot < arr[i])
swap(arr[i], arr[--k]);
else
i++;
}
// 递归完成对于两个子序列的快速排序
quick_sort(arr, j);
quick_sort(arr + k, len - k);
}
```
=== "Python1"
```python
def quick_sort(arr, l, r):
if l >= r:
return
random_index = random.randint(l, r)
pivot = arr[random_index]
arr[l], arr[random_index] = arr[random_index], arr[l]
i = l + 1
j = l
k = r + 1
while i < k:
if arr[i] < pivot:
arr[i], arr[j + 1] = arr[j + 1], arr[i]
j += 1
i += 1
elif arr[i] > pivot:
arr[i], arr[k - 1] = arr[k - 1], arr[i]
k -= 1
else:
i += 1
arr[l], arr[j] = arr[j], arr[l]
quick_sort(arr, l, j - 1)
quick_sort(arr, k, r)
```
内省排序
定义
内省排序(英语:Introsort 或 Introspective sort)4是快速排序和 堆排序 的结合,由 David Musser 于 1997 年发明。内省排序其实是对快速排序的一种优化,保证了最差时间复杂度为
性质
内省排序将快速排序的最大递归深度限制为
实现
从 2000 年 6 月起,SGI C++ STL 的 stl_algo.h
中 sort()
函数的实现采用了内省排序算法。
线性找第 k 大的数
在下面的代码示例中,第
找第
我们可以借助快速排序的思想解决这个问题。考虑快速排序的划分过程,在快速排序的「划分」结束后,数列
和快速排序一样,该方法的时间复杂度依赖于每次划分时选择的分界值。如果采用随机选取分界值的方式,可以证明在期望意义下,程序的时间复杂度为
实现(C++)
改进:中位数中的中位数
中位数中的中位数(英文:Median of medians),提供了一种确定性的选择划分过程中分界值的方法,从而能够让找第
该算法的流程如下:
- 将整个序列划分为
组,每组元素数不超过 5 个; - 寻找每组元素的中位数(因为元素个数较少,可以直接使用 插入排序 等算法)。
- 找出这
组元素中位数中的中位数。将该元素作为前述算法中每次划分时的分界值即可。
时间复杂度证明
下面将证明,该算法在最坏情况下的时间复杂度为
先分析前两步——划分与寻找中位数。由于划分后每组内的元素数量非常少,可以认为寻找一组元素的中位数的时间复杂度为
接下来分析第三步——递归过程。这一步进行了两次递归调用:第一次是寻找各组中位数中的中位数,需要的开销显然为
综上,我们可以列出这样的不等式:
假设
到这里我们就证明了,该算法在最坏情况下也具有
参考资料与注释
Footnotes
贡献者:@Yufan@Shuhao@Menci@Flex@WenzelTian@Ir1dXD@ksyx@Alisa@Nathan@mgt@Banson@Xeonacid@夜轮_NachtgeistW@sbofgayschool@Zhikai@Nano@sshwy@ouuan@Billchenchina@partychicken
本页面最近更新:2/3/2023, 12:00:00 AM,更新历史
发现错误?想一起完善? 在 GitHub 上编辑此页!
本页面的全部内容在 CC BY-SA 4.0 和 SATA 协议之条款下提供,附加条款亦可能应用