必须用Fisher-Yates算法实现数组重排才能保证抽奖公平,其核心是从后往前遍历,每步在当前位置及之前等概率随机选下标交换;错误写法如sort(() => Math.random() - 0.5)会导致概率偏差。

直接用数组重排(Shuffle)实现抽奖,本质是“先打乱,再按序取”,但**只有当重排本身绝对公平时,抽奖才真正公平**。常见误区是把“看起来乱了”等同于“概率均等”,而实际很多写法存在严重偏差——比如用 sort(() => Math.random() - 0.5),会导致首尾位置中奖概率显著偏离理论值。
必须用 Fisher-Yates(洗牌算法)做底层重排
这是唯一被数学证明为均匀随机的原地重排算法。核心逻辑:从后往前遍历,每一步都从当前位置及之前所有位置中**等概率随机选一个下标,与当前位交换**。
- JavaScript 示例:
function fisherYates(arr) {
const shuffled = [...arr];
for (let i = shuffled.length - 1; i > 0; i--) {
const j = Math.floor(Math.random() * (i + 1));
[shuffled[i], shuffled[j]] = [shuffled[j], shuffled[i]];
}
return shuffled;
}
- 关键点:
j的取值范围是[0, i](含端点),不是[0, arr.length);否则会破坏均匀性。 - 测试验证:对 3 元素数组运行 10 万次,6 种排列出现频次应基本接近 16667 次;错误写法可能让某几种排列出现超 2 万次。
抽奖 ≠ 简单取前 N 个,要匹配业务语义
重排只是工具,怎么用决定是否公平:
- 纯随机抽人(无权重):对参与者数组执行 Fisher-Yates,再取前 k 个。适合年会抽幸运观众、团建分组等场景。
- 带概率权重的抽奖(如 1% 一等奖):不能直接 shuffle 奖品数组!因为奖项不是等概率的。正确做法是用「区间划分法」或「别名法」,Fisher-Yates 在这里不适用。
- 防重复中奖(同一人不可中多次):shuffle 参与者数组后顺序抽取,天然避免重复;若需多次抽奖且允许同一人重复中奖,则每次都要独立 shuffle 或用带放回抽样。
避开高危陷阱:这些写法看似简洁,实则不公平
以下代码在大量实践中已被统计验证存在位置偏差:
-
arr.sort(() => Math.random() - 0.5):排序比较函数返回值非确定性,违反 sort 稳定性假设,导致分布倾斜。 -
arr.sort(() => Math.random() - Math.random()):虽看似更“随机”,但因比较逻辑不满足传递性,仍无法保证均匀。 - 用
Math.random() * arr.length循环取索引并删除元素:删除操作改变后续索引,未重新归一化概率,越往后抽取偏差越大。
生产环境建议:封装 + 验证 + 监控
不要裸写 shuffle 逻辑:
- 封装为可复用函数,强制要求传入
Random实例(便于测试时注入固定种子)。 - 上线前做小规模统计验证:对固定输入数组跑 10 万次 shuffle,检查每个元素出现在每个位置的频次是否在 ±1.5% 误差内。
- 关键抽奖活动(如金融类、高价值奖品)建议记录每次 shuffle 的随机种子和结果,支持事后审计。










