今天遇到一个用 JS 实现从数组中随机挑出几个不重复元素的需求,一般是采用以下的思路。
- 先将数组元素排列顺序打乱
- 在打乱后的数组中截取需要的数量的元素
而 JS 现有的 runtime 的底层并未提供打乱相关的 API,所以需要自己动手实现,写成代码大概是这样的。
var array = ["one", "two", "three", "four", "five"];
var n = 3;
function shuffle(a) {
// xxxx
}
shuffle(array);
var result = array.slice(0, n);
关键在于这里的 shuffle 函数如何实现。我最初想到的 shuffle 的思路是这样的,遍历一个数组,让每一个元素与数组中随机选择的一个元素交换:
function shuffle(array) {
var i = array.length, j, temp;
if (i == 0) return;
while ( --i ) {
j = Math.floor(Math.random() * (i + 1));
temp = array[i];
array[i] = array[j];
array[j] = temp;
}
}
搜索了一下,惊奇地发现一个神奇的姿势,这个姿势在爆栈网收获了好多个赞:
https://stackoverflow.com/questions/19269545/how-to-get-n-no-elements-randomly-from-an-array
具体是利用 runtime 给数组原生提供的 sort() 方法,sort() 方法提供了通过传入一个比较函数来决定排序的比较过程的机制。具体的思路是,在这个比较函数里面引入随机的因子,在排序算法运行到需要判断是否交换,把执行权交给这个判断函数的时候,这个函数通过随机地返回来搅乱排序算法,从而产生搅乱这个数组的效果。
// Shuffle array
const shuffled = array.sort(() => 0.5 - Math.random());
// Get sub-array of first n elements after shuffled
let selected = shuffled.slice(0, n);
简单说明几点,方便不熟悉 JS 的同学:
1. () => xxx 是 JS 的一种函数定义,类似 lambda 表达式
2. Math.random() 会返回 [0, 1) 之间的一个浮点数。
3. 当比较函数返回值小于0时,会把比较函数的第一个参数的元素排在第二个参数的元素前面;当比较函数返回值等于0时,维持现状;当比较函数返回值大于0时候,会把第二个参数排在第一个元素的前面。
整个过程可谓是一气呵成,完全满足了我现有的抽取元素的需求,或许可以说是 JS 环境中关于这个问题的一个十分优雅的表达了。
和@johnbanq 同学分享了这个骚操作,经过我们的讨论,banq 同学引出了一个新的疑问,这里通过扰乱排序比较过程的方式实现的打乱过程是否对该数组的所有元素公平?
一开始 banq 同学提到了排序算法里的 decision-tree model,根据这个模型,基于比较的排序的算法在每一个决定的步骤,若随机选择,往两边走都是 50% 的概率,所以决定的最终,各种情况的概率也是相等的,那么可以说这里是公平的?
关于这个模型,参考 《算法导论》 的第八章,这里截下原文以便查阅。
当然,我们两人的讨论到此也暂时告一段落,彼此都信誓旦旦地认同了这个骚操作。然后我去洗了个澡回来,准备睡觉前,再看 Stack Overflow,发现在这段代码下面的评论有人提出了异议:
It's nice, but is far from random. The first item has many more chances to get picked than the last one. See here why: stackoverflow.com/a/18650169/1325646 – pomber Mar 25 '18 at 18:53
所以这么操作是不公平的?突然有种瞬间被打脸的感觉哈哈哈。
然后顺藤摸瓜,爬到了这个回答:
How to randomize (shuffle) a JavaScript array? - Stack Overflow
其中有两个评论我感觉可能可以说明这个问题的存在:
Downvoting as this isn't really that random. I don't know why it has so many upvotes. Do not use this method. It looks pretty, but isn't completely correct. Here are results after 10,000 iterations on how many times each number in your array hits index [0] (I can give the other results too): 1 = 29.19%, 2 = 29.53%, 3 = 20.06%, 4 = 11.91%, 5 = 5.99%, 6 = 3.32% – radtad Nov 13 '13 at 18:35
The problem is that it's not deterministic, which will give wrong results (if 1 > 2 and 2 > 3, it should be given that 1 > 3, but this will not guarantee that. This will confuse the sort, and give the result commented by @radtad). – MatsLindh Sep 10 '14 at 14:07
结合我引用的第二个评论,我可以这么说?正是它保证不了前后的决策步骤的确定性,从而不满足之前我们提到的 decision-tree model?
其中也有人分享了一个在 2012 年的时候做的一个统计各种数组打乱算法产生的结果的分布,验证这个问题的网页,从直观的角度展示了这种 骚操作 为什么不好:
Will It Shuffle?
这里的具体细节我还不是很清晰,留下这个坑,欢迎在此留下你对这个问题的思考。