Fisher–Yates洗牌算法

65 阅读2分钟

我在做一个类似吃豆人的小游戏,需要让豆子和玩家随机生成在grid网格的某个坐标点,且不能重合

起初我想到的方案是用 Math.random() 随机生成,再进行判重,重复生成直到不冲突为止。这种逻辑没问题,但代码一多就显得繁琐,执行效率也不高,尤其是在网格密集时,判重次数随之暴增。之后转而采用一种更常见、也更简洁的方式:

function shuffle(array) {
	return array.sort(() => Math.random() - 0.5);
}

但这种算法并不能保证每种排列等概率出现,因为 Array.prototype.sort() 的实现依赖排序算法和比较函数的稳定性,而 Math.random() - 0.5 是不稳定的比较函数,会破坏排序的一致性,导致结果会偏向某些特定排列[2],也就是概率分布不均匀。

后来我了解到了更完美的解决方案——Fisher–Yates洗牌算法

function shuffle(arr) {
	for (let i = arr.length - 1; i > 0; i--) {
		const j = Math.floor(Math.random() * (i + 1));
		[arr[i], arr[j]] = [arr[j], arr[i]];
	}
	return arr;
}

这种算法的思路是,从数组末尾往前,每次迭代都等概率地把当前元素和前面任意一个(包括自己)的元素进行交换,直至遍历结束。

其结果完全服从均匀随机性,即每个元素在任意位置等概率出现/每种排列等概率出现。在实际项目中,哪怕只是这么一个“洗牌”的过程,概率分布正确性、算法复杂度都会影响到用户体验和游戏公平性。而且它的实现也很简洁优雅,足以让人惊艳。Fisher–Yates洗牌算法对完美概率分布的工程实现,及其简洁性和高效性,也算是一种编程之美吧。

参考

  1. [^]Fisher–Yates shuffle en.wikipedia.org/wiki/Fisher…
  2. [^]Will It Shuffle? bost.ocks.org/mike/shuffl…