快速排序
最后更新
快速排序是一种围绕“基准值”(pivot)进行排序的分治算法。它先选出一个基准元素,然后对数组分区,让所有更小的元素排在它前面、所有更大的元素排在它后面,这样基准值就固定在了最终的有序位置上。接着它对左右两个分区递归处理。这里的可视化使用 Lomuto 分区方案,以最后一个元素作为基准值。点击“播放”,观看分区和基准值归位的过程。
得益于良好的缓存表现和原地分区,快速排序在实际中通常是最快的通用排序算法,平均复杂度为 O(n log n)。它的最坏情况是 O(n²)(例如基准值选得不好时遇到已经有序的数组),三数取中或随机化等好的基准值策略可以避免这种情况。
时间复杂度与空间复杂度
| 情况 | 复杂度 | 说明 |
|---|---|---|
| 最好情况 | O(n log n) | 分区均衡 |
| 平均情况 | O(n log n) | 随机顺序 |
| 最坏情况 | O(n²) | 基准值始终导致分区失衡 |
| 空间 | O(log n) | 递归栈(原地分区) |
| 稳定性 | 不稳定 | 分区时的交换会打乱相等元素的顺序 |
分步说明
| 步骤 | 发生了什么 |
|---|---|
| 1 | 选择一个基准值(这里取区间的最后一个元素)。 |
| 2 | 分区:把所有比基准值小的元素移到它的左边。 |
| 3 | 把基准值交换到分界处,它现在已经在最终位置上。 |
| 4 | 对左分区递归地进行快速排序。 |
| 5 | 对右分区递归地进行快速排序。 |
实例演示
用 Lomuto 分区方案(以最后一个元素为基准值)对 [5, 2, 4, 1] 排序:
| 轮次 | 数组 | 操作 |
|---|---|---|
| 开始 | [5, 2, 4, 1] | 对整个区间分区;基准值是 1(最后一个元素)。 |
| 1 | [1, 2, 4, 5] | 没有元素比 1 小,所以把 1 交换到下标 0;基准值 1 已归位。对右侧的 [2, 4, 5] 递归。 |
| 2 | [1, 2, 4, 5] | 以 5 为基准值对 [2, 4, 5] 分区;2 和 4 都更小,所以 5 留在末尾并归位。对左侧的 [2, 4] 递归。 |
| 3 | [1, 2, 4, 5] | 以 4 为基准值对 [2, 4] 分区;2 更小,所以 4 原地不动并归位。2 只有一个元素,已经有序。 |
| 完成 | [1, 2, 4, 5] | 每个基准值都已固定到位,数组排序完成。 |
什么时候用快速排序
| 适合使用 | 不适合使用 |
|---|---|
| 你需要一种常数因子小、速度快的通用内存排序。 | 你需要有保证的 O(n log n) 最坏情况时间(改用堆排序或归并排序)。 |
内存紧张:分区是原地进行的,只需 O(log n) 的栈空间。 | 你需要稳定排序,保留相等键的原有顺序。 |
| 数据顺序随机或未知,并且你使用随机化或三数取中的基准值。 | 输入已经有序或接近有序,而基准值又是固定的,会触发 O(n²)。 |
| 看重缓存局部性,因为快速排序按顺序访问内存。 | 你要对链表排序,这时归并排序不需要快速排序所依赖的随机访问。 |
Quick Sort 代码
简洁、可运行的 Quick Sort 实现,提供 Python, JavaScript, Java, C++, C, Pseudocode 版本。选择一种语言,复制代码,或在 Coddy 在线编辑器中直接打开。
Quick Sort 的 Python 代码
1def quick_sort(a, low=0, high=None):2 if high is None:3 high = len(a) - 14 if low < high:5 p = partition(a, low, high)6 quick_sort(a, low, p - 1)7 quick_sort(a, p + 1, high)8 return a9
10
11def partition(a, low, high):12 # Lomuto partition: everything < pivot moves left of it13 pivot = a[high]14 i = low15 for j in range(low, high):16 if a[j] < pivot:17 a[i], a[j] = a[j], a[i]18 i += 119 a[i], a[high] = a[high], a[i]20 return i21
22
23nums = [10, 7, 8, 9, 1, 5]24print("Before:", nums)25quick_sort(nums)26print("After: ", nums)Quick Sort 的 JavaScript 代码
1function quickSort(a, lo = 0, hi = a.length - 1) {2 if (lo >= hi) return a;3 const p = partition(a, lo, hi);4 quickSort(a, lo, p - 1);5 quickSort(a, p + 1, hi);6 return a;7}8
9// Lomuto partition: last element is the pivot10function partition(a, lo, hi) {11 const pivot = a[hi];12 let i = lo;13 for (let j = lo; j < hi; j++) {14 if (a[j] < pivot) {15 [a[i], a[j]] = [a[j], a[i]];16 i++;17 }18 }19 [a[i], a[hi]] = [a[hi], a[i]];20 return i;21}22
23const data = [5, 2, 9, 1, 7, 3];24console.log("Before:", data);25console.log("Sorted:", quickSort([...data]));Quick Sort 的 Java 代码
1import java.util.Arrays;2
3public class Main {4 static void quickSort(int[] arr, int low, int high) {5 if (low >= high) return;6 int p = partition(arr, low, high);7 quickSort(arr, low, p - 1);8 quickSort(arr, p + 1, high);9 }10
11 // Lomuto partition: last element is the pivot12 static int partition(int[] arr, int low, int high) {13 int pivot = arr[high];14 int i = low - 1;15 for (int j = low; j < high; j++) {16 if (arr[j] < pivot) swap(arr, ++i, j);17 }18 swap(arr, i + 1, high);19 return i + 1;20 }21
22 static void swap(int[] arr, int a, int b) {23 int tmp = arr[a];24 arr[a] = arr[b];25 arr[b] = tmp;26 }27
28 public static void main(String[] args) {29 int[] arr = {10, 7, 8, 9, 1, 5};30 System.out.println("Before: " + Arrays.toString(arr));31 quickSort(arr, 0, arr.length - 1);32 System.out.println("After: " + Arrays.toString(arr));33 }34}Quick Sort 的 C++ 代码
1#include <iostream>2#include <utility>3#include <vector>4
5void printVec(const std::vector<int>& a) {6 for (int x : a) std::cout << x << " ";7 std::cout << "\n";8}9
10// Lomuto partition: place the pivot in its final position11int partition(std::vector<int>& a, int lo, int hi) {12 int pivot = a[hi];13 int i = lo;14 for (int j = lo; j < hi; ++j) {15 if (a[j] < pivot) std::swap(a[i++], a[j]);16 }17 std::swap(a[i], a[hi]);18 return i;19}20
21void quickSort(std::vector<int>& a, int lo, int hi) {22 if (lo >= hi) return;23 int p = partition(a, lo, hi);24 quickSort(a, lo, p - 1);25 quickSort(a, p + 1, hi);26}27
28int main() {29 std::vector<int> data = {10, 7, 8, 9, 1, 5};30 std::cout << "Before: ";31 printVec(data);32 quickSort(data, 0, static_cast<int>(data.size()) - 1);33 std::cout << "After: ";34 printVec(data);35 return 0;36}Quick Sort 的 C 代码
1#include <stdio.h>2
3void printArr(const int a[], int n) {4 for (int i = 0; i < n; i++) printf("%d ", a[i]);5 printf("\n");6}7
8void swap(int* x, int* y) {9 int tmp = *x;10 *x = *y;11 *y = tmp;12}13
14// Lomuto partition: place the pivot in its final position15int partition(int a[], int lo, int hi) {16 int pivot = a[hi];17 int i = lo;18 for (int j = lo; j < hi; j++) {19 if (a[j] < pivot) swap(&a[i++], &a[j]);20 }21 swap(&a[i], &a[hi]);22 return i;23}24
25void quickSort(int a[], int lo, int hi) {26 if (lo >= hi) return;27 int p = partition(a, lo, hi);28 quickSort(a, lo, p - 1);29 quickSort(a, p + 1, hi);30}31
32int main(void) {33 int data[] = {10, 7, 8, 9, 1, 5};34 int n = sizeof(data) / sizeof(data[0]);35 printf("Before: ");36 printArr(data, n);37 quickSort(data, 0, n - 1);38 printf("After: ");39 printArr(data, n);40 return 0;41}Quick Sort 的 Pseudocode 代码
1DECLARE nums : ARRAY[1:7] OF INTEGER2DECLARE n : INTEGER3n ← 74nums[1] ← 75nums[2] ← 36nums[3] ← 97nums[4] ← 18nums[5] ← 59nums[6] ← 810nums[7] ← 211DECLARE i : INTEGER12
13FUNCTION partition(lo : INTEGER, hi : INTEGER) RETURNS INTEGER14 DECLARE pivot : INTEGER15 DECLARE a : INTEGER16 DECLARE b : INTEGER17 DECLARE temp : INTEGER18 pivot ← nums[hi]19 a ← lo - 120 FOR b ← lo TO hi - 121 IF nums[b] <= pivot THEN22 a ← a + 123 temp ← nums[a]24 nums[a] ← nums[b]25 nums[b] ← temp26 ENDIF27 NEXT b28 temp ← nums[a + 1]29 nums[a + 1] ← nums[hi]30 nums[hi] ← temp31 RETURN a + 132ENDFUNCTION33
34PROCEDURE quickSort(lo : INTEGER, hi : INTEGER)35 DECLARE p : INTEGER36 IF lo < hi THEN37 // Place the pivot, then sort each side of it38 p ← partition(lo, hi)39 CALL quickSort(lo, p - 1)40 CALL quickSort(p + 1, hi)41 ENDIF42ENDPROCEDURE43
44CALL quickSort(1, n)45
46FOR i ← 1 TO n47 OUTPUT nums[i]48NEXT i快速排序常见问题
快速排序的时间复杂度是多少?
O(n log n),最好情况也是 O(n log n),但如果分区始终失衡,最坏情况会退化到 O(n²)。随机选取或三数取中的基准值能让最坏情况几乎不可能出现。快速排序是稳定的吗?
不是。标准的原地分区会交换相距较远的元素,可能改变相等键的相对顺序。稳定的变体确实存在,但会失去快速排序原地排序的优势。
为什么快速排序通常比归并排序快?
O(n log n),却要付出 O(n) 缓冲区和更多数据移动的代价。快速排序和归并排序,该用哪个?
O(n log n) 最坏情况,或者要对链表或内存放不下的外部数据排序,就选归并排序。为什么快速排序在有序数组上会变成 O(n²)?
n 层而不是 log n 层。随机选取基准值或使用三数取中可以打破这种模式,恢复 O(n log n) 的表现。Lomuto 和 Hoare 分区方案有什么区别?
Lomuto 方案用一个下标从左到右扫描,写起来更简单,所以这里的可视化采用了它。Hoare 方案用两个指针从两端向中间移动,交换次数通常更少,实际中更快,但它在分区步骤中不会把基准值放到最终位置上。