V

算法可视化

ZHEN
分享到微博分享到 XGitHub 仓库个人主页
学习工具算法复杂度速查算法学习路径
数据结构数组链表栈队列树堆哈希表图字典树并查集LRU 缓存跳表线段树B+ 树布隆过滤器树状数组
经典排序算法冒泡排序鸡尾酒排序双调排序选择排序插入排序二分插入排序希尔排序归并排序自顶向下归并快速排序三路快排双轴快排堆排序计数排序基数排序桶排序
图算法Dijkstra 最短路Kruskal 最小生成树Prim 最小生成树Bellman-Ford 最短路拓扑排序Floyd 多源最短路强连通分量2-SAT最大流二分图匹配LCA 倍增欧拉路径
动态规划编辑距离0-1 背包完全背包最长公共子序列最长递增子序列硬币找零方案数石子合并旅行商 TSP树形 DP数位 DP换根 DP
回溯与搜索N 皇后子集生成全排列组合总和迷宫寻路岛屿数量单词搜索数独A* 寻路
字符串KMP 字符串匹配Rabin-KarpBoyer-MooreManacher后缀数组LCP / height 数组AC 自动机Z 函数
数学与数论埃氏筛线性筛欧几里得算法快速幂扩展欧几里得中国剩余定理欧拉函数米勒-拉宾FFTPollard's Rho
计算几何凸包旋转卡壳最近点对线段相交扫描线求交
查找二分查找二分边界旋转数组搜索二分答案三分查找

双轴快排 Dual-Pivot Quicksort

交换排序 · 快排变体 · Java Arrays.sort 实际采用

从一根轴到两根轴

普通快排一趟围着一个基准把数组劈成两段。2009 年 Yaroslavskiy 提出:一趟干脆用两个基准(p ≤ q),把数组直接分成三段——< p / [p, q] / > q。段分得更碎、递归更浅,配合更少的元素搬动和更好的缓存局部性,实测普遍快过单轴快排——Java 7 起基本类型的 Arrays.sort 用的就是它。

双轴快排怎么做

取区间首尾两个元素当双基准:若 a[lo] > a[hi] 先交换两端,保证 p ≤ q(下面第 2、3 趟就会看到这一步)。然后还是熟悉的三指针 lt / i / gt 扫描中间:a[i] < p 就和 lt 交换、两者右移;a[i] > q 就和 gt 交换、gt 左移(i 不动,换来的值还没查);落在 [p, q] 之间就留在中段、i 右移。扫描完把两个基准分别换到三段的交界处——p 归位到左段右侧、q 归位到中段右侧,这两个位置直接钉死,再对剩下的三段分治。

下面固定 8 个数 [3, 5, 9, 1, 6, 2, 4, 7]。点「下一步」:首趟双基准 p=3、q=7(两根紫柱),一趟扫出 [2,1] | 3 | [5,6,4] | 7 | [9]——双基准归位钉死后,只剩左右两个小段进区间栈。留意第 2、3 趟弹出的子区间首尾是反的,都要先交换两端再选双基准。

3
5
9
1
6
2
4
7
区间栈 · 每格 = 一段待排序子数组 a[lo..hi](栈顶先弹出分区)
栈空 → 全部就位

弹出区间 [0,7]

1function dualPivotQuickSort(a: number[]): number[] {
2 const stack: [number, number][] = [[0, a.length - 1]];
3 while (stack.length > 0) {
4 const [lo, hi] = stack.pop()!;
5 if (a[lo] > a[hi]) [a[lo], a[hi]] = [a[hi], a[lo]];
6 const p = a[lo], q = a[hi];
7 let lt = lo + 1, i = lo + 1, gt = hi - 1;
8 while (i <= gt) {
9 if (a[i] < p) {
10 [a[lt], a[i]] = [a[i], a[lt]];
11 lt++; i++;
12 } else if (a[i] > q) {
13 [a[i], a[gt]] = [a[gt], a[i]];
14 gt--;
15 } else {
16 i++;
17 }
18 }
19 lt--; gt++;
20 [a[lo], a[lt]] = [a[lt], a[lo]];
21 [a[hi], a[gt]] = [a[gt], a[hi]];
22 if (hi > gt + 1) stack.push([gt + 1, hi]);
23 if (gt - 1 > lt + 1) stack.push([lt + 1, gt - 1]);
24 if (lt - 1 > lo) stack.push([lo, lt - 1]);
25 }
26 return a;
27}
n8
栈深0
lo0
hi7
p-
q-
lt-
i-
gt-
1 / 27

复杂度与为什么 Java 选它

平均 O(n log n)、原地、不稳定,和单轴快排同级。优势在常数:一趟分三段让递归树更浅(约 log₃ 级),元素跨段搬动更少、访存更连续。Yaroslavskiy 的基准测试赢了当时 JDK 的单轴实现,于是 Java 7 把基本类型的 Arrays.sort 换成了它(工程版还叠加了五点取样选轴、小段转插入排序等细节,本页取首尾双基准展示主线)。

快速排序:1 个基准 → 2 段(< / ≥)。
三路快排:1 个基准 → 3 段(< / == / >),中段是等值,治大量重复。
双轴快排:2 个基准 → 3 段(<p / [p,q] / >q),中段是区间,通用数据更快——Java Arrays.sort 实际采用。

它和三路快排是一对好对照:同样的 lt/i/gt 三指针、同样「一趟三段」,一个靠「等于」收中段、一个靠「第二根轴」撑中段。两页对着单步走一遍,三指针划分这个技巧就吃透了。