随机算法
概述
随机算法(Randomized Algorithm)使用随机数作为输入的一部分,可用于提高算法效率、简化算法设计,或解决确定性算法难以解决的问题。随机算法在排序、选择、图论、数值计算等领域有广泛应用。
核心思想:引入随机性可以打破输入的 adversarial 性质,避免最坏情况,从而获得更好的期望性能。虽然最坏情况可能仍然存在,但其发生概率很小。
随机算法的重要性
graph TB
subgraph "随机算法的优势"
A["随机算法"] --> B["打破最坏情况<br/>提高期望性能"]
A --> C["简化算法设计<br/>避免复杂分析"]
A --> D["解决难问题<br/>近似求解"]
A --> E["并行化<br/>负载均衡"]
end
style A fill:#E3F2FD,stroke:#2196F3,stroke-width:2px
随机算法分类
三种类型
graph TB
subgraph "随机算法分类"
A["随机算法"] --> B["Las Vegas<br/>总是正确<br/>运行时间随机"]
A --> C["Monte Carlo<br/>可能出错<br/>运行时间确定"]
A --> D["Sherwood<br/>总是正确<br/>改善最坏情况"]
end
B --> B1["例:随机快排<br/>随机选择"]
C --> C1["例:素性测试<br/>蒙特卡洛积分"]
D --> D1["例:随机化中位数"]
style B fill:#E8F5E9
style C fill:#FFF3E0
style D fill:#F3E5F5
| 类型 |
正确性 |
运行时间 |
特点 |
示例 |
| Las Vegas |
总是正确 |
随机 |
可能运行很长时间 |
随机快排、随机选择 |
| Monte Carlo |
可能出错 |
确定 |
可以控制错误概率 |
素性测试、MC积分 |
| Sherwood |
总是正确 |
随机 |
消除最坏情况 |
随机化算法 |
随机快速排序
算法思想
传统快速排序在选择枢轴时,如果总是选择第一个或最后一个元素,对于已排序或接近排序的数组会退化到 O(n²)。随机快速排序通过**随机选择枢轴**,打破这种 adversarial 输入。
graph TB
subgraph "随机快排优势"
A["确定性快排"] --> B["最坏输入<br/>O n² "]
A --> C["随机快排"]
C --> D["期望 O n log n "]
D --> E["高概率保证<br/>无最坏输入"]
end
style E fill:#E8F5E9
工作原理
| Text Only |
|---|
| 随机快排的核心:随机选择枢轴
传统快排:
pivot = arr[low] 或 arr[high]
问题:已排序数组总是选择最小/最大元素
随机快排:
random = low + rand % (high - low + 1)
swap(arr[random], arr[high])
pivot = arr[high]
优势:枢轴位置随机,期望分割均匀
|
分割过程可视化
| Text Only |
|---|
| 数组: [3, 1, 4, 1, 5, 9, 2, 6, 5, 3]
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
随机选择枢轴:假设选择索引 5,值 = 9
═══════════════════════════════════════════════════════════════
分割过程
═══════════════════════════════════════════════════════════════
步骤1:交换枢轴到末尾
[3, 1, 4, 1, 5, 3, 2, 6, 5, 9]
↑ 枢轴
步骤2:遍历并分割
i = -1(小于枢轴区域的边界)
j=0: arr[0]=3 < 9, i=0, swap(arr[0],arr[0])
[3, 1, 4, 1, 5, 3, 2, 6, 5, 9]
j=1: arr[1]=1 < 9, i=1, swap(arr[1],arr[1])
[3, 1, 4, 1, 5, 3, 2, 6, 5, 9]
...(类似处理)
j=8: arr[8]=5 < 9, i=8, swap(arr[8],arr[8])
[3, 1, 4, 1, 5, 3, 2, 6, 5, 9]
步骤3:将枢轴放到正确位置
i=9, swap(arr[9], arr[9])
[3, 1, 4, 1, 5, 3, 2, 6, 5, 9]
↑ 枢轴位置
结果:枢轴 9 在位置 9,左边都 < 9,右边为空
═══════════════════════════════════════════════════════════════
|
实现
| C |
|---|
| #include <stdlib.h>
#include <time.h>
void swap(int *a, int *b) {
int temp = *a;
*a = *b;
*b = temp;
}
int randomPartition(int arr[], int low, int high) {
// 随机选择枢轴
int random = low + rand() % (high - low + 1);
swap(&arr[random], &arr[high]);
int pivot = arr[high];
int i = low - 1;
for (int j = low; j < high; j++) {
if (arr[j] <= pivot) {
swap(&arr[++i], &arr[j]);
}
}
swap(&arr[i + 1], &arr[high]);
return i + 1;
}
void randomQuickSort(int arr[], int low, int high) {
if (low < high) {
int pi = randomPartition(arr, low, high);
randomQuickSort(arr, low, pi - 1);
randomQuickSort(arr, pi + 1, high);
}
}
|
时间复杂度分析
| Text Only |
|---|
| 期望时间复杂度:E[T(n)] = O(n log n)
推导:
设 T(n) 为排序 n 个元素的期望时间
T(n) = n + Σ (1/n) × [T(i) + T(n-1-i)]
i=0 to n-1
= n + (2/n) × Σ T(i)
i=0 to n-1
解得:T(n) = O(n log n)
高概率保证:
Pr[T(n) > c × n log n] < 1/n^k (对某个常数 c 和任意 k)
即:超过 O(n log n) 的概率指数级减小
|
随机选择(第K小元素)
算法思想
随机选择算法用于在 O(n) 期望时间内找到数组中第 k 小的元素。与确定性选择算法相比,随机版本更简单且实际运行更快。
flowchart TB
A["查找第k小元素"] --> B["随机选择枢轴"]
B --> C["分割数组"]
C --> D["枢轴位置 pi"]
D --> E{"k == pi-low+1?"}
E -->|是| F["返回枢轴值"]
E -->|否| G{"k < pi-low+1?"}
G -->|是| H["递归左半区<br/>查找第k小"]
G -->|否| I["递归右半区<br/>查找第 k-rank 小"]
style F fill:#E8F5E9
实现
| C |
|---|
| int randomSelect(int arr[], int low, int high, int k) {
if (low == high) return arr[low];
int pi = randomPartition(arr, low, high);
int rank = pi - low + 1; // 枢轴是第 rank 小
if (k == rank) {
return arr[pi];
} else if (k < rank) {
return randomSelect(arr, low, pi - 1, k);
} else {
return randomSelect(arr, pi + 1, high, k - rank);
}
}
|
执行示例
| Text Only |
|---|
| 数组: [3, 2, 9, 5, 7, 1, 8]
查找第 3 小元素
═══════════════════════════════════════════════════════════════
第1次调用
═══════════════════════════════════════════════════════════════
随机选择枢轴:假设选择 5
分割后:[3, 2, 1, 5, 7, 9, 8]
↑ pi=3
rank = 3 - 0 + 1 = 4
k = 3 < rank = 4
→ 递归左半区 [3, 2, 1],查找第 3 小
═══════════════════════════════════════════════════════════════
第2次调用
═══════════════════════════════════════════════════════════════
数组: [3, 2, 1]
随机选择枢轴:假设选择 2
分割后:[1, 2, 3]
↑ pi=1
rank = 1 - 0 + 1 = 2
k = 3 > rank = 2
→ 递归右半区 [3],查找第 3-2=1 小
═══════════════════════════════════════════════════════════════
第3次调用
═══════════════════════════════════════════════════════════════
数组: [3]
low = high,返回 arr[0] = 3
结果:第 3 小元素是 3
═══════════════════════════════════════════════════════════════
|
蓄水池抽样
问题定义
从大小未知的数据流中随机选取 k 个元素,保证每个元素被选中的概率相等。
graph LR
A["数据流<br/>大小未知"] --> B["蓄水池抽样"]
B --> C["输出 k 个样本"]
C --> D["每个元素<br/>概率 = k/n"]
style D fill:#E8F5E9
算法思想
| Text Only |
|---|
| 算法步骤:
1. 前 k 个元素直接放入蓄水池
2. 对于第 i 个元素(i > k):
- 以 k/i 的概率决定是否放入蓄水池
- 如果放入,随机替换蓄水池中的一个元素
正确性证明:
对于第 i 个元素(i > k):
- 被选中的概率 = k/i
- 在处理第 j 个元素后(j > i)仍留在蓄水池的概率
= ∏ (1 - 1/j) = i/n
- 最终在蓄水池的概率 = (k/i) × (i/n) = k/n
|
实现
| C |
|---|
| void reservoirSample(int stream[], int n, int k, int result[]) {
// 前 k 个元素直接放入
for (int i = 0; i < k; i++) {
result[i] = stream[i];
}
// 处理剩余元素
for (int i = k; i < n; i++) {
// 以 k/(i+1) 的概率选择
int j = rand() % (i + 1);
// 如果 j < k,替换蓄水池中的第 j 个元素
if (j < k) {
result[j] = stream[i];
}
}
}
|
示例
| Text Only |
|---|
| 数据流: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
k = 3
═══════════════════════════════════════════════════════════════
处理过程
═══════════════════════════════════════════════════════════════
i=0,1,2: 直接放入
蓄水池: [1, 2, 3]
i=3: j = rand() % 4, 假设 j = 2 < 3
替换 result[2] = 4
蓄水池: [1, 2, 4]
i=4: j = rand() % 5, 假设 j = 4 ≥ 3
不替换
蓄水池: [1, 2, 4]
i=5: j = rand() % 6, 假设 j = 1 < 3
替换 result[1] = 6
蓄水池: [1, 6, 4]
...继续处理...
最终蓄水池(示例): [1, 6, 9]
每个元素被选中的概率 = 3/10
═══════════════════════════════════════════════════════════════
|
洗牌算法(Fisher-Yates)
算法思想
生成数组的随机排列,保证每种排列出现的概率相等(1/n!)。
flowchart TB
A["Fisher-Yates洗牌"] --> B["从后向前遍历"]
B --> C["对于位置 i"]
C --> D["随机选择 0~i 中的一个位置 j"]
D --> E["交换 arr i 和 arr j "]
E --> F{"i > 0?"}
F -->|是| C
F -->|否| G["完成"]
style G fill:#E8F5E9
正确性证明
| Text Only |
|---|
| 证明每个排列的概率 = 1/n!
第 n-1 个位置的元素:
- 从 n 个元素中随机选择,概率 = 1/n
第 n-2 个位置的元素:
- 从剩余 n-1 个元素中随机选择,概率 = 1/(n-1)
...
第 0 个位置的元素:
- 最后剩余的 1 个元素,概率 = 1
特定排列的概率:
= (1/n) × (1/(n-1)) × ... × 1 = 1/n!
|
实现
| C |
|---|
| void shuffle(int arr[], int n) {
for (int i = n - 1; i > 0; i--) {
int j = rand() % (i + 1); // 随机选择 [0, i]
swap(&arr[i], &arr[j]);
}
}
|
洗牌过程可视化
| Text Only |
|---|
| 数组: [1, 2, 3, 4, 5]
═══════════════════════════════════════════════════════════════
Fisher-Yates 洗牌过程
═══════════════════════════════════════════════════════════════
i=4: rand() % 5 = 2
交换 arr[4] 和 arr[2]
[1, 2, 5, 4, 3]
↑ ↑
i=3: rand() % 4 = 0
交换 arr[3] 和 arr[0]
[4, 2, 5, 1, 3]
↑ ↑
i=2: rand() % 3 = 1
交换 arr[2] 和 arr[1]
[4, 5, 2, 1, 3]
↑ ↑
i=1: rand() % 2 = 1
交换 arr[1] 和 arr[1](自己)
[4, 5, 2, 1, 3]
完成!最终排列: [4, 5, 2, 1, 3]
═══════════════════════════════════════════════════════════════
|
Miller-Rabin 素性测试
问题背景
判断一个大数是否为素数。确定性算法复杂度高,Miller-Rabin 是一种 Monte Carlo 算法,可以在可接受的错误概率下快速判断。
数学基础
费马小定理:如果 p 是素数,则对于任意 a(1 < a < p):
二次探测定理:如果 p 是素数,且 x² ≡ 1 (mod p),则:
| Text Only |
|---|
| x ≡ 1 (mod p) 或 x ≡ -1 (mod p)
|
算法思想
flowchart TB
A["测试 n 是否素数"] --> B["写成 n-1 = 2^r × d"]
B --> C["随机选择 a ∈ 2, n-2 "]
C --> D["计算 x = a^d mod n"]
D --> E{"x == 1 或 x == n-1?"}
E -->|是| F["可能是素数<br/>换 a 继续测试"]
E -->|否| G["重复平方 r 次"]
G --> H{"某次得到 n-1?"}
H -->|是| F
H -->|否| I["确定为合数"]
style F fill:#E8F5E9
style I fill:#FFCDD2
实现
| C |
|---|
| long long power(long long base, long long exp, long long mod) {
long long result = 1;
base %= mod;
while (exp > 0) {
if (exp & 1) {
result = result * base % mod;
}
base = base * base % mod;
exp >>= 1;
}
return result;
}
int millerRabin(long long n, int k) {
// 处理小数
if (n <= 1 || n == 4) return 0;
if (n <= 3) return 1;
// 写成 n-1 = 2^r × d
long long d = n - 1;
while (d % 2 == 0) {
d /= 2;
}
// 进行 k 次测试
for (int i = 0; i < k; i++) {
long long a = 2 + rand() % (n - 4);
long long x = power(a, d, n);
if (x == 1 || x == n - 1) continue;
// 二次探测
while (d != n - 1) {
x = x * x % n;
d *= 2;
if (x == 1) return 0; // 确定是合数
if (x == n - 1) break; // 可能是素数
}
if (x != n - 1) return 0; // 确定是合数
}
return 1; // 很可能是素数
}
|
错误概率分析
| Text Only |
|---|
| Miller-Rabin 是 Monte Carlo 算法:
- 如果返回"合数":n 一定是合数(正确)
- 如果返回"素数":n 可能是素数,也可能是合数(错误)
错误概率:
- 对于奇合数 n,至少 3/4 的 a 会检测出它是合数
- k 次测试后,错误概率 ≤ (1/4)^k
实际应用:
- k = 10: 错误概率 < 10^-6
- k = 20: 错误概率 < 10^-12
- k = 40: 错误概率 < 10^-24(实际使用)
|
测试示例
| Text Only |
|---|
| 测试 n = 561(Carmichael数,最小的伪素数)
═══════════════════════════════════════════════════════════════
n - 1 = 560 = 2^4 × 35,所以 r = 4,d = 35
═══════════════════════════════════════════════════════════════
测试 a = 2:
x = 2^35 mod 561 = 263
x ≠ 1 且 x ≠ 560
x = 263^2 mod 561 = 166
x = 166^2 mod 561 = 67
x = 67^2 mod 561 = 1
得到 1,但前一步不是 560
→ 检测出 561 是合数!
实际:561 = 3 × 11 × 17
═══════════════════════════════════════════════════════════════
|
随机游走
算法思想
在图中从起点出发,每步随机选择一个邻居移动,用于图搜索、采样等场景。
graph LR
A["起点"] -->|随机| B["节点1"]
B -->|随机| C["节点2"]
C -->|随机| D["..."]
D --> E["终点/收敛"]
style A fill:#E3F2FD
style E fill:#E8F5E9
实现
| C |
|---|
| int randomWalk(int graph[][100], int n, int start, int target, int maxSteps) {
int current = start;
int steps = 0;
while (current != target && steps < maxSteps) {
// 收集邻居
int degree = 0;
int neighbors[100];
for (int i = 0; i < n; i++) {
if (graph[current][i]) {
neighbors[degree++] = i;
}
}
if (degree == 0) return -1; // 无法继续
// 随机选择邻居
current = neighbors[rand() % degree];
steps++;
}
return (current == target) ? steps : -1;
}
|
| Text Only |
|---|
| 随机游走在 PageRank 中的应用:
网页排名 = 随机游走的稳态分布
从任意网页出发:
- 以概率 α 跳转到随机网页
- 以概率 1-α 跳转到当前网页的随机链接
α 通常取 0.15("阻尼因子")
稳态分布给出网页的重要性排名
|
哈希函数随机化
全域哈希
通过随机选择哈希函数,避免 adversarial 输入导致的最坏情况。
| C |
|---|
| typedef struct {
int a;
int b;
int p;
int m;
} HashFunction;
// 从全域哈希族中随机选择一个函数
HashFunction createRandomHash(int m, int p) {
HashFunction hf;
hf.a = 1 + rand() % (p - 1); // a ∈ [1, p-1]
hf.b = rand() % p; // b ∈ [0, p-1]
hf.p = p;
hf.m = m;
return hf;
}
// 计算哈希值
int hashValue(HashFunction hf, int key) {
return ((hf.a * key + hf.b) % hf.p) % hf.m;
}
|
冲突概率分析
| Text Only |
|---|
| 全域哈希性质:
对于任意两个不同的键 k1 ≠ k2:
Pr[h(k1) == h(k2)] ≤ 1/m
证明:
h(k) = ((a × k + b) mod p) mod m
对于固定的 k1 ≠ k2,随机选择 a、b:
- h(k1) 有 m 种可能
- h(k2) 有 m 种可能
- 冲突的情况 ≤ p/m
- 总情况 = p(p-1)
- 冲突概率 = (p/m) / (p(p-1)) ≈ 1/m
|
蒙特卡洛积分
算法思想
使用随机采样估计定积分的值。
| Text Only |
|---|
| ∫[a,b] f(x) dx ≈ (b-a) × (1/n) × Σ f(xi)
其中 xi 是 [a,b] 上的随机点
|
实现
| C |
|---|
| double monteCarloIntegrate(double (*f)(double), double a, double b, int n) {
double sum = 0;
for (int i = 0; i < n; i++) {
double x = a + (double)rand() / RAND_MAX * (b - a);
sum += f(x);
}
return (b - a) * sum / n;
}
|
示例:估计 π
| C |
|---|
| double estimatePi(int n) {
int inside = 0;
for (int i = 0; i < n; i++) {
double x = (double)rand() / RAND_MAX;
double y = (double)rand() / RAND_MAX;
if (x * x + y * y <= 1) {
inside++;
}
}
return 4.0 * inside / n;
}
|
| Text Only |
|---|
| 估计 π 的可视化:
在单位正方形内随机撒点,统计落入 1/4 圆的比例
1 | ● ●
|● ● ● ●
| ●●● ●
|● ●●●● ●
0 |________
0 1
π ≈ 4 × (圆内点数) / (总点数)
样本数 n = 10^6:π ≈ 3.14159
样本数 n = 10^7:π ≈ 3.141592
|
C++ 随机数工具
现代C++随机数
| C++ |
|---|
| #include <random>
#include <algorithm>
std::mt19937 rng(std::random_device{}());
int randomInt(int min, int max) {
std::uniform_int_distribution<int> dist(min, max);
return dist(rng);
}
double randomDouble(double min, double max) {
std::uniform_real_distribution<double> dist(min, max);
return dist(rng);
}
template<typename T>
void shuffleVector(std::vector<T>& v) {
std::shuffle(v.begin(), v.end(), rng);
}
|
时间复杂度汇总
| 算法 |
期望时间 |
最坏时间 |
类型 |
| 随机快排 |
O(n log n) |
O(n²) |
Las Vegas |
| 随机选择 |
O(n) |
O(n²) |
Las Vegas |
| 蓄水池抽样 |
O(n) |
O(n) |
确定 |
| Fisher-Yates |
O(n) |
O(n) |
确定 |
| Miller-Rabin |
O(k log³ n) |
O(k log³ n) |
Monte Carlo |
| 蒙特卡洛积分 |
O(n) |
O(n) |
Monte Carlo |
应用场景总结
graph TB
subgraph "随机算法应用"
A["排序与选择"] --> A1["随机快排<br/>随机选择"]
B["采样与洗牌"] --> B1["蓄水池抽样<br/>Fisher-Yates"]
C["数论"] --> C1["素性测试<br/>随机分解"]
D["数值计算"] --> D1["蒙特卡洛积分<br/>随机优化"]
E["图论"] --> E1["随机游走<br/>随机图算法"]
F["数据结构"] --> F1["全域哈希<br/>跳表"]
end
style A1 fill:#E8F5E9
style B1 fill:#E8F5E9
style C1 fill:#E8F5E9
style D1 fill:#E8F5E9
style E1 fill:#E8F5E9
style F1 fill:#E8F5E9
| 应用领域 |
算法 |
优势 |
| 排序算法 |
随机快排 |
避免最坏情况 |
| 选择问题 |
随机选择 |
简单高效 |
| 大数据采样 |
蓄水池抽样 |
内存友好 |
| 游戏开发 |
Fisher-Yates |
公平洗牌 |
| 密码学 |
素性测试 |
快速判断大素数 |
| 数值计算 |
蒙特卡洛 |
高维积分 |
| 图算法 |
随机游走 |
采样、排名 |
| 哈希表 |
全域哈希 |
避免冲突攻击 |
参考资料