真实面经题目 · 原创解析
如何实现洗牌算法?
公平洗牌应使用 Fisher-Yates 算法,从后往前随机选择一个未固定位置交换,保证每种排列出现概率相同。
出现于:网易 · C/C++
真实面经题目 · 原创解析
公平洗牌应使用 Fisher-Yates 算法,从后往前随机选择一个未固定位置交换,保证每种排列出现概率相同。
洗牌算法最标准的回答是 Fisher-Yates。假设数组长度为 n,从 i = n - 1 遍历到 1,每次在 [0, i] 中均匀随机选 j,然后交换 a[i] 和 a[j]。这样第 i 个位置会从尚未固定的 i+1 个元素中均匀选出一个,递归到前面位置,最终每个排列概率都是 1/n!。复杂度 O(n),原地 O(1)。要强调随机数必须均匀,不能用 sort(() => random - 0.5),也要避免取模偏差。
公平洗牌不是看起来乱,而是 n 个元素的 n! 种排列出现概率相等。面试中要先说清这个目标,否则容易写出有偏算法。
Fisher-Yates 每轮把位置 i 固定下来,从下标 0 到 i 的未固定元素中均匀随机选择一个与它交换,之后不再修改 i 位置。
最后一个位置选中任一元素概率是 1/n,倒数第二个位置在剩余元素中概率是 1/(n-1),依此类推,任意排列概率都是这些概率乘积。
算法只需要一次线性遍历和常数额外空间,适合数组原地打乱。实现时要注意随机区间是闭区间 [0, i],包含当前位置。
如果用于安全场景,普通伪随机数不够,需要密码学安全随机源;如果用随机整数取模,也要注意随机源范围不是倍数时的取模偏差。
function shuffle(items, random = Math.random) {
const arr = [...items];
for (let i = arr.length - 1; i > 0; i -= 1) {
const j = Math.floor(random() * (i + 1));
[arr[i], arr[j]] = [arr[j], arr[i]];
}
return arr;
} 排序算法期望比较器满足传递性,随机比较会破坏排序假设,结果分布也不均匀。
当前位置也可能保持原元素不变,包含 i 才能保证所有排列都有正确概率。
要使用密码学安全随机源、记录可审计种子或过程,并避免可预测的伪随机实现。