JavaScript洗牌算法实践
在 JavaScript 中,最常用且公平的洗牌算法是 Fisher-Yates 洗牌算法(也称 Knuth 洗牌算法),核心逻辑是从数组末尾向前遍历,每次将当前元素与前面随机位置的元素交换,确保每个元素被打乱的概率均等。
1. 基础实现(原地洗牌)
直接修改原数组,空间复杂度为 O(1),适合对性能要求较高的场景。
javascript:/*** Fisher-Yates 原地洗牌算法* @param {Array} arr - 待洗牌的数组* @returns {Array} 洗牌后的数组(与原数组引用相同)*/
function shuffleArray(arr) {// 从数组最后一位开始向前遍历for (let i = arr.length - 1; i > 0; i--) {// 生成 [0, i] 范围内的随机整数(确保每个位置概率均等)const randomIndex = Math.floor(Math.random() * (i + 1));// 交换当前元素与随机位置元素[arr[i], arr[randomIndex]] = [arr[randomIndex], arr[i]];}return arr;
}// 测试
const arrs= ["猫", "狗", "兔", "仓鼠", "鹦鹉"];
shuffleArray(arrs);
console.log(arrs); // 示例输出:["兔", "猫", "鹦鹉", "狗", "仓鼠"](每次结果不同)
2. 非原地洗牌(不修改原数组)
通过创建原数组的副本进行洗牌,避免改变原数组,适合需要保留原始数据的场景。
javascript:/*** Fisher-Yates 非原地洗牌算法* @param {Array} arr - 待洗牌的数组* @returns {Array} 新的洗牌后数组(原数组不变)*/
function shuffleArrayWithoutMutate(arr) {// 创建原数组的浅拷贝(若数组元素是对象,需用深拷贝,如 JSON.parse(JSON.stringify(arr)))const newArr = [...arr];for (let i = newArr.length - 1; i > 0; i--) {const randomIndex = Math.floor(Math.random() * (i + 1));[newArr[i], newArr[randomIndex]] = [newArr[randomIndex], newArr[i]];}return newArr;
}// 测试
const numbers = [1, 2, 3, 4, 5];
const shuffledNumbers = shuffleArrayWithoutMutate(numbers);
console.log(numbers); // 输出:[1, 2, 3, 4, 5](原数组不变)
console.log(shuffledNumbers); // 示例输出:[3, 5, 1, 4, 2]
3. 常见错误:“随机交换”的陷阱
新手常犯的错误是用「遍历数组,每次与随机位置交换」的方式洗牌,这种方式会导致元素概率不均(靠前元素被交换的概率更高),不推荐:
javascript:// 错误示例:概率不均的洗牌
function badShuffle(arr) {for (let i = 0; i < arr.length; i++) {// 随机位置范围是 [0, arr.length-1],而非 [0, i],导致概率偏差const randomIndex = Math.floor(Math.random() * arr.length);[arr[i], arr[randomIndex]] = [arr[randomIndex], arr[i]];}return arr;
}
核心原理
Fisher-Yates 算法的公平性源于:
- 第 1 个元素(最终在末尾)有 1/n 的概率被选中;
- 第 2 个元素有 1/(n-1) 的概率被选中;
- 以此类推,每个元素最终在任意位置的概率均为 1/n ,确保绝对公平。