核心要点

  • 从后往前遍历 i 从 n-1 到 1,取 j = randint(0, i)(含两端),交换 a[i] 与 a[j]。

  • 每一步从「尚未定位」的前缀中等概率挑一个放到位置 i。

  • 正确性:每种排列出现概率恰为 1/n!,时间 O(n)、原地无额外空间。

  • 关键:j 的范围必须是 [0, i](含 i),不能是 [0, n-1]。

标准回答

Fisher-Yates(又称 Knuth Shuffle)原地生成均匀随机排列。从数组末尾开始:对 i = n-1, n-2, ..., 1,从区间 [0, i](闭区间,包含 i 本身)中等概率选一个下标 j,交换 a[i] 与 a[j]。这样位置 i 就被「定下来」,之后不再改动;下一轮在更短的前缀 [0, i-1] 上重复。

正确性证明:某元素最终落在最后一格(位置 n-1)的概率是 1/n;在它没被选到的前提下落到位置 n-2 的概率是 (1-1/n)·(1/(n-1)) = 1/n;以此类推,任一元素落到任一位置的概率都是 1/n,且各位置独立地构成一个排列,于是每个具体排列出现的概率为 1/n × 1/(n-1) × ... × 1 = 1/n!,即严格均匀。

时间复杂度 O(n),空间 O(1) 原地完成。下方代码给出标准实现并附频率验证。

python
import random

def fisher_yates(a):
    """原地均匀洗牌:每种排列概率严格 1/n!"""
    n = len(a)
    for i in range(n - 1, 0, -1):       # 从后往前 i = n-1 .. 1
        j = random.randint(0, i)        # j 在闭区间 [0, i]
        a[i], a[j] = a[j], a[i]         # 交换
    return a

if __name__ == '__main__':
    # 频率验证:n=3 共 6 种排列,应各约 1/6
    from collections import Counter
    cnt = Counter()
    for _ in range(600000):
        cnt[tuple(fisher_yates([0, 1, 2]))] += 1
    for perm, c in sorted(cnt.items()):
        print(perm, round(c / 600000, 3))  # 应都 ≈ 0.167

常见误区

⚠️ 常见踩坑

错误写法是每轮都用 j = randint(0, n-1)(固定全范围),这会产生 n^n 种等概率路径,无法被 n! 整除,导致某些排列偏多——是经典的洗牌偏置 bug。j 上界必须随 i 收缩到当前位置。

追问

追问 1为什么 j 的上界必须是 i 而不是 n-1?

因为只有 [0,i] 范围内各 i+1 个选择等概率,才能让总路径数恰为 n!,每种排列对应唯一一条路径。若用 [0,n-1],路径总数 n^n 不被 n! 整除,必然有排列出现概率偏高(如 n=3 时某些排列概率 4/27、某些 5/27)。

追问 2如何在数据流/未知长度下做洗牌(inside-out 变体)?

用 inside-out 算法:遍历输入第 i 个元素时,j=randint(0,i),令 out[i]=out[j],out[j]=输入[i]。它不需要预先知道 n,可边读边构造,同样保证 1/n! 均匀,且不修改原数组。

延伸学习

与本题相关的知识库文章、术语、工具与行业资讯。