数学

本文内容参考《算法导论》,OI Wiki。(数学好难,暂时搁置)

快速幂

例题

整数

时间复杂度:\(O(\log{n})\)。

1
2
3
4
5
6
7
8
9
10
11
private static final long MOD = 1_000_000_007;

private static long pow(long x, long n) {
long res = 1L;
for (; n != 0; x = x * x % MOD, n >>= 1) {
if ((n & 1) == 1) {
res = res * x % MOD;
}
}
return res;
}

矩阵

时间复杂度:\(O(\log{n})\)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
private static final int MOD = 1_000_000_007;

private static long[][] pow(long[][] x, long n) {
long[][] res = {{1, 0}, {0, 1}};
for (; n != 0; x = mul(x, x), n >>= 1) {
if ((n & 1) == 1) {
res = mul(res, x);
}
}
return res;
}

private static long[][] mul(long[][] a, long[][] b) {
long[][] c = new long[2][2];
for (int i = 0; i < 2; i++) {
for (int j = 0; j < 2; j++) {
for (int k = 0; k < 2; k++) {
c[i][j] = (c[i][j] + a[i][k] * b[k][j]) % MOD;
}
}
}
return c;
}

数论

例题

判断质数

时间复杂度 \(O(n\sqrt{n})\)。

1
2
3
4
5
6
7
private static boolean isPrime(int x) {
if (x < 2) return false;
for (int i = 2; i <= x / i; i++) {
if (x % i == 0) return false;
}
return true;
}

质数筛法

埃氏筛

时间复杂度 \(O(n\log{\log{n}})\),筛掉质数的倍数,每个合数都会被筛它的质因数的个数次。

1
2
3
4
5
6
7
8
9
10
11
12
private static boolean[] sieveOfEratosthenes(int n) {
boolean[] np = new boolean[n + 1];
np[0] = np[1] = true;

for (int i = 2; i <= n / i; i++) {
if (np[i]) continue;
for (int j = i; j <= n / i; j++) {
np[j * i] = true;
}
}
return np;
}

欧拉筛

时间复杂度 \(O(n)\),每个合数都只被它的最小质因数筛掉。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
private static boolean[] sieveOfEuler(int n) {
int cnt = 0;
int[] p = new int[n + 1];
boolean[] np = new boolean[n + 1];
np[0] = np[1] = true;

for (int i = 2; i <= n; i++) {
if (!np[i]) p[cnt++] = i;
for (int j = 0; p[j] <= n / i; j++) {
np[p[j] * i] = true;
if (i % p[j] == 0) break;
}
}
return np;
}

分解质因数

时间复杂度 \(O(\sqrt{n})\)。

1
2
3
4
5
6
7
8
9
10
11
private static Map<Integer, Integer> primeFactors(int x) {
Map<Integer, Integer> map = new HashMap<>();
for (int i = 2; i <= x / i; i++) {
while (x % i == 0) {
x /= i;
map.merge(i, 1, Integer::sum);
}
}
if (x > 1) map.merge(x, 1, Integer::sum);
return map;
}

欧拉函数

欧拉函数 \(\phi(n)\),表示 \([1,n]\) 范围内和 \(n\) 互质的数的个数,有如下性质:

  • 若 \(\gcd(a,b)=1\),则 \(\phi(a\times b)=\phi(a)\times \phi(b)\)。
  • 若 \(n=\prod_{i=1}^{k}{p_{i}^{c_{i}}}\),其中 \(p_{i}\) 为质数,则 \(\phi(n)=n\times\prod_{i=1}^{k}(1-\frac{1}{p_{i}})\)。

我们可以使用分解质因数求解某个数的欧拉函数,时间复杂度为 \(O(\sqrt{n})\);也可以使用欧拉筛来求解 \([1,n]\) 范围内所有数的欧拉函数,时间复杂度为 \(O(n)\)。在欧拉筛中,每个合数都是被它的最小质因子筛掉,设 \(p\) 是 \(n\) 的最小质因子,则 \(n=n^{\prime}\times p\),分类讨论:

  • 如果 \(n^{\prime}\bmod p\neq 0\),因为 \(p\) 是质数,所以 \(\gcd(n^{\prime},p)=1\),则 \(\phi(n)=\phi(n^{\prime})\times \phi(p)=\phi(n^{\prime})\times (p-1)\)。
  • 如果 \(n^{\prime}\bmod p=0\),则 \(\phi(n)=p\times n^{\prime}\times\prod_{i=1}^{k}(1-\frac{1}{p_{i}})=p\times \phi(n^{\prime})\)。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
private static int[] sieveOfEuler(int n) {
int cnt = 0;
int[] p = new int[n + 1];
int[] phi = new int[n + 1];
boolean[] np = new boolean[n + 1];
phi[1] = 1;
np[0] = np[1] = true;

for (int i = 2; i <= n; i++) {
if (!np[i]) {
p[cnt++] = i;
phi[i] = i - 1;
}
for (int j = 0; p[j] <= n / i; j++) {
np[p[j] * i] = true;
if (i % p[j] == 0) {
phi[p[j] * i] = p[j] * phi[i];
break;
}
phi[p[j] * i] = (p[j] - 1) * phi[i];
}
}
return phi;
}

最大公约数

欧几里得算法

时间复杂度 \(O(\log{\max(a,b)})\)。求得最大公约数之后,使用 \(\gcd(a,b)\times\operatorname{lcm}(a,b)=a\times b\) 公式可以得到最小公倍数。

1
2
3
4
private static int gcd(int a, int b) {
if (b == 0) return a;
return gcd(b, a % b);
}

扩展欧几里得算法

常用于求解 \(ax+by=\gcd(a,b)\) 的一组解。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
private static int x, y;

private static int exgcd(int a, int b) {
if (b == 0) {
x = 1;
y = 0;
return a;
}
int d = exgcd(b, a % b);
int t = x - a / b * y;
x = y;
y = t;
return d;
}

其他知识

约数个数

若 \(n=\prod_{i=1}^{k}{p_{i}^{c_{i}}}\),则 \(d_{n}=\prod_{i=1}^{k}(c_{i}+1)\)。

约数之和

若 \(n=\prod_{i=1}^{k}{p_{i}^{c_{i}}}\),则 \(s_{n}=\prod_{i=1}^{k}\sum_{j=0}^{c_{i}}{p_{i}^{j}}\)。

裴蜀定理

若 \(a,b\) 是整数,则对于任意的整数 \(x,y\),\(ax+by\) 总是 \(\gcd(a,b)\) 的倍数,并且存在整数 \(x,y\),使得 \(ax+by=\gcd(a,b)\)。特别的,若存在整数 \(x,y\) 使得 \(ax+by=1\),则 \(\gcd(a,b)=1\),即 \(a,b\) 互质。

费马小定理

若 \(p\) 是质数,\(\gcd(a,p)=1\),则 \(a^{p-1}\equiv 1\pmod{p}\)。

欧拉定理

若 \(\gcd(a,n)=1\),则 \(a^{\phi(n)}\equiv 1\pmod{n}\)。

模乘法逆元

若 \(p\) 是质数,根据费马小定理,有 \(a\times a^{-1}\equiv 1\equiv a^{p-1}\pmod{p}\),得到 \(a^{-1}\equiv a^{p-2}\pmod{p}\)。

若 \(b\) 是任意整数,求 \(a\) 的逆元,等价于求 \(ax\equiv 1\pmod{b}\) 的解,等价于求 \(ax+by=1\) 的解。如果 \(\gcd(a,b)=1\),则可以使用扩展欧几里得算法求解该方程。如果 \(\gcd(a,b)\neq 1\),根据裴蜀定理可知方程无解(或者可以将方程变换为 \(\frac{a}{g}x+\frac{b}{g}y=\frac{1}{g}\),等式左边是整数,右边不是整数,方程无解),即逆元不存在。

线性同余方程

求 \(ax\equiv c\pmod{b}\) 的解,等价于求 \(ax+by=c\) 的解,同样的,当 \(\gcd(a,b)\mid c\) 时方程有解。使用扩展欧几里得算法可以求出 \(ax+by=\gcd(a,b)\) 的解,然后将方程变换为 \(a\frac{c}{\gcd(a,b)}x_{0}+b\frac{c}{\gcd(a,b)}y_{0}=c\),可以得到方程的解。

字符串

本文内容参考《算法》,《算法导论》,OI Wiki

字符串匹配

例题

暴力

  • 时间复杂度:最坏 \(O(NM)\),平均 \(O(N)\)。
  • 空间复杂度:\(O(1)\)。
1
2
3
4
5
6
7
8
9
10
11
12
13
private static int bruteForce(String text, String pattern) {
int i, j;
int n = text.length(), m = pattern.length();
for (i = 0, j = 0; i < n && j < m; i++) {
if (text.charAt(i) == pattern.charAt(j)) {
j++;
} else {
i -= j;
j = 0;
}
}
return j == m ? i - m : -1;
}

KMP

  • 时间复杂度:\(O(N)\)。
  • 空间复杂度:\(O(M)\)。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
private static int kmp(String text, String pattern) {
int n = text.length(), m = pattern.length();
if (m > n) return -1;

// 处理模式串
// next[i] 表示 pattern 的子串 [0, i] 的最长相等前后缀的长度
int[] next = new int[m];
for (int i = 1, j = 0; i < m; i++) {
while (j > 0 && pattern.charAt(i) != pattern.charAt(j)) {
j = next[j - 1];
}
if (pattern.charAt(i) == pattern.charAt(j)) {
j++;
}
next[i] = j;
}

// 匹配文本串
for (int i = 0, j = 0; i < n; i++) {
while (j > 0 && text.charAt(i) != pattern.charAt(j)) {
j = next[j - 1];
}
if (text.charAt(i) == pattern.charAt(j)) {
j++;
}
if (j == m) {
return i - m + 1;
}
}
return -1;
}

Boyer-Moore

  • 时间复杂度:最坏 \(O(NM)\),平均 \(O(\frac{N}{M})\)。
  • 空间复杂度:\(O®\)。
1
System.out.println("TODO");

Rabin-Karp

  • 时间复杂度:\(O(N)\)。
  • 空间复杂度:\(O(1)\)。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
private static final int P = 13331;

// MOD = 2^64
private static int rabinKarp(String text, String pattern) {
int n = text.length(), m = pattern.length();
if (m > n) return -1;

long PM = pow(P, m - 1);
long patHash = hash(pattern, m);
long txtHash = hash(text, m);
if (txtHash == patHash) {
return 0;
}

for (int i = m; i < n; i++) {
txtHash = txtHash - text.charAt(i - m) * PM;
txtHash = txtHash * P + text.charAt(i);
if (txtHash == patHash) {
return i - m + 1;
}
}
return -1;
}

private static long pow(int a, int n) {
long res = 1L, x = a;
for (; n != 0; x *= x, n >>= 1) {
if ((n & 1) == 1) {
res *= x;
}
}
return res;
}

private static long hash(String s, int m) {
long h = 0L;
for (int i = 0; i < m; i++) {
h = h * P + s.charAt(i);
}
return h;
}

字符串哈希

例题

  • E. Compress Words(这题使用 \(MOD=2^{64}\) 一直在第 65 个测试点 WA,看来还是模质数比较好)。

实现

  • 时间复杂度:预处理 \(O(N)\),获取哈希值 \(O(1)\)。
  • 空间复杂度:\(O(N)\)。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
class StringHash {
private final int K = 2;
private static final long[] b = {131, 13331};
private static final long[] m = {1_000_000_007, 998244353};

private int index;
private final long[][] h, p;

public StringHash(int n) {
h = new long[K][n + 1];
p = new long[K][n + 1];
for (int i = 0; i < K; i++) {
p[i][0] = 1;
}
}

public StringHash(char[] s) {
this(s.length);
for (char c : s) add(c);
}

public void add(char c) {
for (int j = 0; j < K; j++) {
p[j][index + 1] = p[j][index] * b[j] % m[j];
h[j][index + 1] = (h[j][index] * b[j] + c) % m[j];
}
index++;
}

public long[] get(int l, int r) {
long[] res = new long[K];
for (int i = 0; i < K; i++) {
long t = h[i][r + 1] - h[i][l] * p[i][r - l + 1];
res[i] = (t % m[i] + m[i]) % m[i];
}
return res;
}

public int length() {
return index;
}
}

字典树(Trie)

例题

实现

  • 时间复杂度:插入和查找都是 \(O(k)\),其中 \(k\) 为字符串的长度。
  • 空间复杂度:\(O(nR)\),其中 \(n\) 为节点总数,\(R\) 为字母表大小。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
class Trie {
private static final int R = 26;
private final Node root;

private static class Node {
boolean exist;
Node[] next = new Node[R];
}

public Trie() {
root = new Node();
}

public void insert(String word) {
Node node = root;
for (int i = 0; i < word.length(); i++) {
int idx = word.charAt(i) - 'a';
if (node.next[idx] == null) {
node.next[idx] = new Node();
}
node = node.next[idx];
}
node.exist = true;
}

public boolean search(String word) {
Node node = root;
for (int i = 0; i < word.length(); i++) {
int idx = word.charAt(i) - 'a';
if (node.next[idx] == null) {
return false;
}
node = node.next[idx];
}
return node.exist;
}
}

AC 自动机

例题

实现

  • 时间复杂度:插入 \(O(k)\),构建 \(O(nR)\),查询 \(O(k+n)\),其中 \(k\) 为字符串的长度。
  • 空间复杂度:\(O(nR)\),其中 \(n\) 为节点总数,\(R\) 为字母表大小。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
class AhoCorasickAutomaton {
private static final int R = 26;
private final Node root;

// 按插入顺序存储模式串尾字符对应的节点
private final List<Node> patternNodes;

// 所有节点按层次遍历顺序存储(代码没有存根节点)
private final List<Node> levelOrderNodes;

private static class Node {
int cnt; // 节点出现在文本串中的次数,懒更新
Node fail; // 节点的失配指针,指向当前节点的最长后缀节点
Node[] next = new Node[R];
}

public AhoCorasickAutomaton() {
root = new Node();
patternNodes = new ArrayList<>();
levelOrderNodes = new ArrayList<>();
}

// 插入模式串
public void insert(String pattern) {
Node node = root;
for (int i = 0; i < pattern.length(); i++) {
int idx = pattern.charAt(i) - 'a';
if (node.next[idx] == null) {
node.next[idx] = new Node();
}
node = node.next[idx];
}
patternNodes.add(node);
}

// 构建失配指针和 Trie 图
public void build() {
// 将根节点的直接子节点入队,并构建节点的失配指针和 Trie 图
// 提前入队是因为根节点和其直接子节点与其他节点的处理逻辑不同
root.fail = root;
Queue<Node> q = new LinkedList<>();
for (int i = 0; i < R; i++) {
if (root.next[i] == null) {
root.next[i] = root;
} else {
root.next[i].fail = root;
q.offer(root.next[i]);
}
}
// 层次遍历,构建失配指针和 Trie 图
while (!q.isEmpty()) {
Node node = q.poll();
levelOrderNodes.add(node);
for (int i = 0; i < R; i++) {
if (node.next[i] == null) {
node.next[i] = node.fail.next[i];
} else {
node.next[i].fail = node.fail.next[i];
q.offer(node.next[i]);
}
}
}
}

// 查询每个模式串在文本串中的出现次数
public void query(String text, int[] cnt) {
// 懒更新出现次数
Node node = root;
for (int i = 0; i < text.length(); i++) {
int idx = text.charAt(i) - 'a';
node = node.next[idx];
node.cnt++;
}
// 倒序层次遍历,进一步沿着失配指针传递出现次数
// 倒序遍历是因为失配指针必然在更上层,所以从下向上传递可以保证正确性
for (int i = levelOrderNodes.size() - 1; i >= 0; i--) {
node = levelOrderNodes.get(i);
node.fail.cnt += node.cnt;
}
// 获取每个模式串的出现次数
for (int i = 0; i < patternNodes.size(); i++) {
cnt[i] += patternNodes.get(i).cnt;
}
}
}

正则表达式

例题

实现

  • 时间复杂度:构造 NFA \(O(m)\),匹配 \(O(nm)\),其中 \(m\) 为正则表达式的长度,\(n\) 为文本串的长度。
  • 空间复杂度:构造 NFA \(O(m)\),匹配 \(O(m)\)。
1
System.out.println("TODO");

排序算法

本文内容参考 sorting-algorithms,《算法》,《算法导论》。(强迫症,最后还是把描述性的文字删了,只贴代码

验证算法的正确性,可以使用:912. 排序数组148. 排序链表164. 最大间距

简单概述

理想的排序算法

  • 稳定性:相同键不会重新排序。
  • 原地排序:不使用额外空间存储排序数据。
  • 最坏情况 \(O(n\log{n})\) 次比较,\(O(n)\) 次交换。
  • 适应性:当数据接近排序或唯一键很少时,时间复杂度为 \(O(n)\)。

如何看待常数因子的改进方案

摘抄自《算法》的一段内容:在每一节中,我们会将书中的每个算法都看做某种应用的关键。但在整体上,我们希望学习的是为每种应用找到最合适的算法。我们并不是在推荐读者一定要实现所有提到的改进方法,而是提醒大家不要对算法初始实现的性能盖棺定论。研究一个新问题时,最好的方法是先实现一个你能想到的最简单的程序,当它成为瓶颈的时候再继续改进它。实现那些只能把运行时间缩短某个常数因子的改进措施可能并不值得。你需要用实验来检验一项改进,正如本书练习所演示的那样。

冒泡排序

  • 稳定性:稳定。
  • 时间复杂度:\(O(n^{2})\)。
  • 空间复杂度:\(O(1)\)。
  • 数据接近排序时,时间复杂度为 \(O(n)\)。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
private static void bubbleSort(int[] nums) {
int n = nums.length, pos;
for (int i = n - 1; i > 0; i = pos) {
pos = 0;
for (int j = 0; j < i; j++) {
// 稳定性的关键
if (nums[j] > nums[j + 1]) {
swap(nums, j, j + 1);
// 将最后交换的位置,作为下次冒泡的上界
pos = j;
}
}
}
}

选择排序

  • 稳定性:不稳定。
  • 时间复杂度:\(O(n^{2})\)。元素的交换次数最少的排序算法。
  • 空间复杂度:\(O(1)\)。
1
2
3
4
5
6
7
8
9
10
11
12
private static void selectionSort(int[] nums) {
int n = nums.length;
for (int i = 0; i < n; i++) {
int minIndex = i;
for (int j = i + 1; j < n; j++) {
if (nums[j] < nums[minIndex]) {
minIndex = j;
}
}
if (minIndex != i) swap(nums, minIndex, i);
}
}

插入排序

  • 稳定性:稳定。
  • 时间复杂度:\(O(n^{2})\)。元素的交换次数等于逆序数。
  • 空间复杂度:\(O(1)\)。
  • 数据接近排序时,时间复杂度为 \(O(n)\)。
1
2
3
4
5
6
7
8
9
10
11
private static void insertionSort(int[] nums) {
int n = nums.length;
for (int i = 1; i < n; i++) {
int j = i - 1, t = nums[i];
// 稳定性的关键
for (; j >= 0 && nums[j] > t; j--) {
nums[j + 1] = nums[j];
}
nums[j + 1] = t;
}
}

希尔排序

  • 稳定性:不稳定。
  • 时间复杂度:使用 Knuth 增量序列 \(O(n^{\frac{3}{2}})\),。Knuth 增量序列:\(1,4,13,40,\dots,\frac{3^{k}-1}{2}\),相当于首项为 \(1\),公比为 \(3\) 的数列的前缀和。
  • 空间复杂度:\(O(1)\)。
  • 数据接近排序时,时间复杂度为 \(O(n\log{n})\)。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
private static void insertionSort(int[] nums) {
int n = nums.length, h = 1;
while (h < n / 3) h = 3 * h + 1;
while (h >= 1) {
for (int i = h; i < n; i++) {
int j = i - h, t = nums[i];
for (; j >= 0 && nums[j] > t; j -= h) {
nums[j + h] = nums[j];
}
nums[j + h] = t;
}
h /= 3;
}
}

归并排序

  • 稳定性:稳定的。
  • 时间复杂度:\(O(n\log{n})\)。
  • 空间复杂度:排序数组 \(O(n)\);自顶向下排序链表 \(O(\log{n})\)(递归的空间),自底向上排序链表 \(O(1)\)。
  • 顺序访问数据,缓存友好。

实现一:自顶向下(数组)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
private static void mergeSort(int[] nums) {
int n = nums.length;
int[] aux = new int[n];
mergeSort(nums, aux, 0, n - 1);
}

private static void mergeSort(int[] nums, int[] aux, int lo, int hi) {
if (lo >= hi) return;
int mid = lo + (hi - lo) / 2;
mergeSort(nums, aux, lo, mid);
mergeSort(nums, aux, mid + 1, hi);
merge(nums, aux, lo, mid, hi);
}

private static void merge(int[] nums, int[] aux, int lo, int mid, int hi) {
for (int i = lo; i <= hi; i++) {
aux[i] = nums[i];
}
int i = lo, j = mid + 1;
for (int k = lo; k <= hi; k++) {
if (i > mid) nums[k] = aux[j++];
else if (j > hi) nums[k] = aux[i++];
// 稳定性的关键
else if (aux[i] <= aux[j]) nums[k] = aux[i++];
else nums[k] = aux[j++];
}
}

实现二:自底向上(数组)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
private static void mergeSort(int[] nums) {
int n = nums.length;
int[] aux = new int[n];
for (int len = 1; len < n; len += len) {
for (int i = 0; i + len < n; i += len << 1) {
merge(nums, aux, i, i + len - 1, Math.min(n - 1, i + (len << 1) - 1));
}
}
}

private static void merge(int[] nums, int[] aux, int lo, int mid, int hi) {
for (int i = lo; i <= hi; i++) {
aux[i] = nums[i];
}
int i = lo, j = mid + 1;
for (int k = lo; k <= hi; k++) {
if (i > mid) nums[k] = aux[j++];
else if (j > hi) nums[k] = aux[i++];
// 稳定性的关键
else if (aux[i] <= aux[j]) nums[k] = aux[i++];
else nums[k] = aux[j++];
}
}

实现三:自顶向下(链表)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
private static ListNode mergeSort(ListNode head) {
if (head == null || head.next == null) {
return head;
}

// 分为两个子链表
ListNode mid = middleNode(head);
ListNode next = mid.next;
mid.next = null;

head = mergeSort(head);
next = mergeSort(next);
return merge(head, next);
}

// 返回合并链表的头节点
private static ListNode merge(ListNode head1, ListNode head2) {
ListNode dummy = new ListNode(), curr = dummy;
while (head1 != null && head2 != null) {
// 稳定性的关键
if (head1.val <= head2.val) {
curr.next = head1;
head1 = head1.next;
} else {
curr.next = head2;
head2 = head2.next;
}
curr = curr.next;
}
curr.next = head1 != null ? head1 : head2;
return dummy.next;
}

// 调用保证 head != null,并且如果有两个中间节点,则返回前一个(不然会无限递归)
// 使用 fast = head.next 可以保证返回前一个中间节点
// 使用 fast = head 可以保证返回后一个中间节点
private static ListNode middleNode(ListNode head) {
if (head == null) return head;
ListNode slow = head, fast = head.next;
while (fast != null && fast.next != null) {
slow = slow.next;
fast = fast.next.next;
}
return slow;
}

实现四:自底向上(链表)

下面这个是没有提前计算链表长度,没有断开链表的解法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
private static ListNode mergeSort(ListNode head) {
if (head == null || head.next == null) return head;

int len = 1;
ListNode dummy = new ListNode(-1, head);

while (true) {
// 两个子链表的头节点的前一个节点
ListNode prev1, prev2;
prev1 = prev2 = dummy;
while (true) {
for (int i = 0; i < len && prev2 != null; i++) {
prev2 = prev2.next;
}
if (prev2 == null || prev2.next == null) break;

prev1 = prev2 = merge(prev1, prev2, len);
}
// 如果该轮没有进行合并操作,则排序完成
if (prev1 == dummy) break;
len += len;
}
return dummy.next;
}

// 返回合并链表的尾节点
private static ListNode merge(ListNode prev1, ListNode prev2, int len) {
// 判断条件:第一个链表未结束 && 第二个链表未结束(考虑长度不足的情况)
while (prev1 != prev2 && len != 0 && prev2.next != null) {
// 稳定性的关键
if (prev1.next.val > prev2.next.val) {
// 将 prev2.next 插入到 prev1 之后
ListNode temp = prev2.next;
prev2.next = prev2.next.next;
temp.next = prev1.next;
prev1.next = temp;
len--;
}
prev1 = prev1.next;
}
// 使 prev2 指向合并链表的尾节点
while (len-- != 0 && prev2.next != null) prev2 = prev2.next;
return prev2;
}

快速排序

  • 稳定性:不稳定。
  • 时间复杂度:期望 \(O(n\log{n})\)。
  • 空间复杂度:期望 \(O(\log{n})\)。
  • 如果使用三路快排,数据包含大量重复元素时,时间复杂度为 \(O(n)\)。

实现一:二路快排

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
private static void quickSort(int[] nums) {
int n = nums.length;
quickSort(nums, 0, n - 1);
}

private static void quickSort(int[] nums, int lo, int hi) {
if (lo >= hi) return;
int index = partition(nums, lo, hi);
quickSort(nums, lo, index - 1);
quickSort(nums, index + 1, hi);
}

private static int partition(int[] nums, int lo, int hi) {
// 三数取中
int mid = median3(nums, lo, lo + (hi - lo) / 2, hi);
swap(nums, lo, mid);

int key = nums[lo];
int i = lo, j = hi + 1;
while (true) {
// 和切分元素相等的元素也会停顿并交换
while (nums[++i] < key && i != hi);
while (nums[--j] > key);
if (i >= j) break;
swap(nums, i, j);
}
swap(nums, lo, j);
return j;
}

实现二:三路快排

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
private static final Random RANDOM = new Random();

private static void quickSort(int[] nums) {
int n = nums.length;
quickSort(nums, 0, n - 1);
}

private static void quickSort(int[] nums, int lo, int hi) {
if (lo >= hi) return;

// 随机选择
int randomIndex = lo + RANDOM.nextInt(hi - lo + 1);
swap(nums, randomIndex, lo);

int key = nums[lo];
int lt = lo, gt = hi, i = lo + 1;
while (i <= gt) {
if (nums[i] < key) swap(nums, i++, lt++);
else if (nums[i] > key) swap(nums, i, gt--);
else i++;
}

quickSort(nums, lo, lt - 1);
quickSort(nums, gt + 1, hi);
}

堆排序

  • 稳定性:不稳定。
  • 时间复杂度:\(O(n\log{n})\)。
  • 空间复杂度:\(O(1)\)。
  • 随机访问数据,缓存不友好。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
public static void heapSort(int[] nums) {
int n = nums.length;

// 建堆 - 时间复杂度 O(n)
for (int i = n / 2 - 1; i >= 0; i--) {
sink(nums, i, n - 1);
}

// 排序 - 时间复杂度 O(nlog(n))
for (int i = n - 1; i > 0; ) {
swap(nums, 0, i--);
sink(nums, 0, i);
}
}

private static void sink(int[] nums, int i, int n) {
int t = nums[i];
while (2 * i + 1 <= n) {
int j = 2 * i + 1;
if (j + 1 <= n && nums[j + 1] > nums[j]) {
j++;
}
if (t >= nums[j]) break;
nums[i] = nums[j];
i = j;
}
nums[i] = t;
}

计数排序

  • 稳定性:稳定。
  • 时间复杂度:\(O(n+k)\)。
  • 空间复杂度:\(O(n+k)\)。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// 元素取值范围 [0,k)
private static int[] countingSort(int[] nums, int k) {
int n = nums.length;
int[] cnt = new int[k];
int[] res = new int[n];

// 计数
for (int x : nums) {
cnt[x]++;
}

// 前缀和
for (int i = 1; i < k; i++) {
cnt[i] += cnt[i - 1];
}

// 排序(倒序遍历,保证稳定性)
for (int i = n - 1; i >= 0; i--) {
res[--cnt[nums[i]]] = nums[i];
}
return res;
}

基数排序

  • 稳定性:稳定。
  • 时间复杂度:\(O(d\cdot (n+k))\)。
  • 空间复杂度:\(O(n+k)\)。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
// 元素最多有 d 位
private static int[] radixSort(int[] nums, int d) {
int n = nums.length;
int[] cnt = new int[10];
int[] aux = new int[n];
int divisor = 1;
for (int i = 0; i < d; i++) {
countingSort(nums, cnt, aux, divisor);

Arrays.fill(cnt, 0);
divisor *= 10;

int[] t = nums;
nums = aux;
aux = t;
}
return nums;
}

private static void countingSort(int[] nums, int[] cnt, int[] aux, int divisor) {
int n = nums.length;

// 计数
for (int x : nums) {
cnt[x / divisor % 10]++;
}

// 前缀和
for (int i = 1; i < 10; i++) {
cnt[i] += cnt[i - 1];
}

// 排序(倒序遍历,保证稳定性)
for (int i = n - 1; i >= 0; i--) {
aux[--cnt[nums[i] / divisor % 10]] = nums[i];
}
}

桶排序

  • 稳定性:取决于桶内的排序策略。
  • 时间复杂度:期望 \(O(n)\)。(输入数据均匀分布时)
  • 空间复杂度:\(O(n+m)\)。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
private static void bucketSort(int[] nums) {
int n = nums.length;

if (n <= 1) return;

int min = Integer.MAX_VALUE;
int max = Integer.MIN_VALUE;
for (int x : nums) {
min = Math.min(min, x);
max = Math.max(max, x);
}

// 小心除以零
int inter = Math.max(1, (max - min) / (n - 1));
int bucketSize = (max - min) / inter + 1;

List<Integer>[] buckets = new List[bucketSize];
Arrays.setAll(buckets, k -> new ArrayList<>());

for (int x : nums) {
int i = (x - min) / inter;
buckets[i].add(x);

// 插入排序
int j = buckets[i].size() - 1;
for (; j > 0 && buckets[i].get(j - 1) > x; j--) {
buckets[i].set(j, buckets[i].get(j - 1));
}
buckets[i].set(j, x);
}

int idx = 0;
for (var bucket : buckets) {
for (int x : bucket) {
nums[idx++] = x;
}
}
}