1229 字
6 分钟
随机算法:洗牌、蓄水池抽样与蒙特卡洛

概述#

随机算法(Randomized Algorithms) 利用随机数做选择、抽样或近似估计。它们常用于打乱顺序、从数据流中均匀抽样、按权重分配流量,以及用随机采样估算复杂结果。

前置知识

  • 概率基础:理解均匀分布和加权概率
  • 前缀和 / 二分:用于高效加权随机选择
  • 哈希函数:用于稳定分流,而不是每次完全随机

问题定义#

在不确定性参与的场景中,设计满足概率要求的算法,并控制时间、空间和稳定性。

要素说明
输入数组、数据流、权重表或采样次数
输出随机排列、随机样本、加权结果或近似值
核心要求概率正确、复杂度可控、必要时可复现
典型算法Fisher-Yates、蓄水池抽样、加权随机、蒙特卡洛

核心原理:分步图解#

Fisher-Yates 洗牌从后往前固定位置,每次在 [0, i] 中等概率选一个元素交换:

graph LR
    A[待洗牌数组] --> B[从最后一位开始]
    B --> C[随机选择 0..i]
    C --> D[交换到位置 i]
    D --> E[i 左移]
    E --> C

因为第 i 个位置从剩余 i + 1 个元素中等概率选择,所以每个排列出现概率相同。


算法精细步骤#

算法:FisherYatesShuffle(arr)
输入:数组 arr
输出:原地打乱后的数组
1. for i from arr.length - 1 downto 1:
2. j ← random integer in [0, i]
3. swap arr[i] and arr[j]
4. return arr

复杂度分析

算法时间复杂度空间复杂度适用场景
Fisher-YatesO(n)O(1)均匀随机排列
蓄水池抽样O(n)O(k)未知长度数据流抽样
加权随机线性版O(n)O(1)少量选择
前缀和 + 二分O(log n) / 次O(n)多次加权选择
蒙特卡洛O(samples)O(1)近似估计

TypeScript 实现#

1. Fisher-Yates 洗牌#

function shuffle<T>(arr: T[]): T[] {
for (let i = arr.length - 1; i > 0; i--) {
const j = Math.floor(Math.random() * (i + 1));
[arr[i], arr[j]] = [arr[j], arr[i]];
}
return arr;
}

2. 蓄水池抽样#

function reservoirSampling<T>(stream: T[], k: number): T[] {
const reservoir: T[] = [];
for (let i = 0; i < stream.length; i++) {
if (i < k) {
reservoir.push(stream[i]);
} else {
const j = Math.floor(Math.random() * (i + 1));
if (j < k) reservoir[j] = stream[i];
}
}
return reservoir;
}

3. 加权随机选择#

interface WeightedItem<T> {
value: T;
weight: number;
}
function weightedRandom<T>(items: WeightedItem<T>[]): T {
const total = items.reduce((sum, item) => sum + item.weight, 0);
let random = Math.random() * total;
for (const item of items) {
random -= item.weight;
if (random <= 0) return item.value;
}
return items[items.length - 1].value;
}

4. 前缀和 + 二分优化#

class WeightedPicker<T> {
private values: T[];
private prefix: number[] = [];
private total = 0;
constructor(items: WeightedItem<T>[]) {
this.values = items.map(item => item.value);
for (const item of items) {
this.total += item.weight;
this.prefix.push(this.total);
}
}
pick(): T {
const target = Math.random() * this.total;
let left = 0;
let right = this.prefix.length - 1;
while (left < right) {
const mid = left + Math.floor((right - left) / 2);
if (this.prefix[mid] < target) left = mid + 1;
else right = mid;
}
return this.values[left];
}
}

5. 蒙特卡洛估算 π#

function estimatePi(samples = 1_000_000): number {
let inside = 0;
for (let i = 0; i < samples; i++) {
const x = Math.random();
const y = Math.random();
if (x * x + y * y <= 1) inside++;
}
return (inside / samples) * 4;
}

6. 稳定 A/B 分流#

class ABTesting {
private variants: string[];
private weights: number[];
constructor(config: Record<string, number>) {
this.variants = Object.keys(config);
this.weights = Object.values(config);
}
assign(userId: string): string {
const total = this.weights.reduce((sum, weight) => sum + weight, 0);
const bucket = this.hashCode(userId) % total;
let current = 0;
for (let i = 0; i < this.variants.length; i++) {
current += this.weights[i];
if (bucket < current) return this.variants[i];
}
return this.variants[this.variants.length - 1];
}
private hashCode(str: string): number {
let hash = 0;
for (let i = 0; i < str.length; i++) {
hash = ((hash << 5) - hash + str.charCodeAt(i)) | 0;
}
return Math.abs(hash);
}
}

工程优化:随机性、安全性与可复现#

场景推荐做法原因
普通 UI 随机Math.random()简单够用
安全敏感随机crypto.getRandomValues()Math.random() 不适合安全用途
A/B 测试用户 ID 哈希保证同一用户稳定分组
测试随机算法注入随机数生成器便于复现和单元测试
多次加权选择前缀和 + 二分预处理后单次 O(log n)

随机算法的正确性通常是概率意义上的,不应只用单次结果判断实现是否正确。


应用与局限#

典型应用#

  • 列表洗牌、随机播放、抽奖
  • 日志流、数据流抽样
  • A/B 测试和灰度分流
  • 蒙特卡洛估计、随机化 QuickSelect

局限性#

局限说明
结果不稳定需要可复现时必须控制随机源
概率验证困难需要统计测试而不是单个样例
安全要求不同业务随机和密码学随机不能混用

总结#

graph LR
    A[明确概率目标] --> B[选择随机模型]
    B --> C[实现抽样或分流]
    C --> D[验证分布]
    D --> E[处理可复现和安全要求]

核心要点

  1. Fisher-Yates 是均匀洗牌的标准算法。
  2. 蓄水池抽样适合未知长度的数据流。
  3. 多次加权选择可用前缀和和二分优化。
  4. 安全场景不要使用 Math.random()
随机算法:洗牌、蓄水池抽样与蒙特卡洛
https://www.hehonglei.cn/technology/randomized-algorithms/
作者
Honglei He
发布于
2026-04-26
许可协议
CC BY-NC-SA 4.0