js数组乱序洗牌算法
别再写 sort(Math.random()) 了,JS 洗牌的正确姿势
做过抽奖功能或者卡片匹配游戏的朋友,大概率都写过这样一个函数:拿到一个数组,让它“随机”打乱顺序。初学前端时,咱们往往习惯用最省事的方法,顺手写出 arr.sort(() => Math.random() - 0.5)。代码确实短,复制粘贴也方便,运行起来似乎也能达到打乱的效果。
然而,这种写法在正式项目中是个巨大的隐患。
问题不在于代码跑不跑得通,而在于概率分布的公平性。sort 方法的核心逻辑是排序,浏览器引擎为了优化性能,对比较函数的调用次数和时机有特定预期。当你传入一个完全随机的返回值时,底层的排序算法(通常是快速排序或归并排序)会陷入不可预测的状态。这导致某些元素被留在原位的概率更高,而另一些位置则几乎不可能出现特定数值。简单说,就是看似乱序,实则暗箱操作,这对于需要绝对公平的抽奖场景来说,是绝对不能接受的。
想要真正均匀地打乱数组,业界的标准解法是 Fisher-Yates 洗牌算法(也叫 Knuth Shuffle)。它的核心逻辑非常直观:从数组末尾开始向前遍历,将当前元素与它前面任意一个随机位置的元素交换。
想象一下你在洗一副扑克牌。你拿起最后一张牌,把它和剩下的任意一张牌对调;接着看倒数第二张,再和剩下任意一张对调。这样每一轮都能保证当前位置的元素是从剩余未确定的池中随机选出的,从而确保了每一种排列组合出现的概率完全相等。
下面这段代码就是经过验证的标准实现:
function shuffleArray(array) {
// 建议创建副本,避免直接修改原数据
const result = [...array];
let currentIndex = result.length;
while (currentIndex !== 0) {
// 获取 [0, currentIndex-1] 之间的随机索引
const randomIndex = Math.floor(Math.random() * currentIndex);
currentIndex--;
// 执行交换
[result[currentIndex], result[randomIndex]] = [result[randomIndex], result[currentIndex]];
}
return result;
}
这段代码有几个关键点值得注意。第一,原地修改风险。上面的写法虽然高效,但如果你直接传引用进去,原数组会被污染。在很多业务场景下,比如列表渲染后的重新排序,直接篡改原始数据源会导致 UI 状态同步异常。因此,我在开头加入了扩展运算符 [...] 生成浅拷贝,确保安全性。如果你的内存极度敏感且确定不需要保留原数据,可以改为原地修改模式以节省性能开销。
第二,随机数区间。很多人容易混淆 Math.random() * n 的范围,这里必须用 currentIndex 来控制范围,随着循环推进,参与交换的池子越来越小,最终实现完美覆盖。如果范围写错,就会导致部分数字永远无法出现在特定位置,破坏随机性。
第三,时间复杂度。该算法的时间复杂度稳定在 O(n),意味着无论数组多长,计算步骤都是线性的。相比那些试图通过多次 sort 来模拟随机的高消耗方案,这才是前端工程化中该有的效率。
在实际应用中,除了基本的乱序,你可能还需要处理稳定性需求。比如某些商品展示位需要固定,仅随机移动其他位置。这时候可以在算法内部增加判断逻辑,跳过固定索引。灵活调整算法边界,比单纯套用模板更能解决实际问题。
技术选型的本质是对结果的负责。一个简单的打乱函数,背后关乎用户体验的信任度。当你的应用涉及利益分配或公平展示时,请务必放弃那种投机取巧的写法。掌握正确的算法逻辑,不仅能避开潜在 Bug,更是展现专业度的最好方式。毕竟,真正的随机,从来都不是靠运气碰出来的。


还没有评论,来说两句吧...