如何从数组中获取随机元素的数量?

196

我正在研究如何在JavaScript中随机访问数组元素。我发现了许多相关链接,比如: 从JavaScript数组获取随机项

var item = items[Math.floor(Math.random()*items.length)];

但是在这种情况下,我们只能从数组中选择一个项目。如果我们想要多个元素,该怎么办?如何从数组中获取多个元素?


7
只需要多次执行它吗? - Bergi
4
从这个声明中我们可以做到这一点吗?循环生成了重复项。 - Shyam Dixit
1
从那个确切的语句中,你无法获得多个元素。 - Sébastien
2
啊,你本应该说出你不想要重复的数字。那就看看 Unique random numbers in O(1)? 和我的回答:Generate unique number within range (0 - X), keeping a history to prevent duplicates - Bergi
打乱数组并获取前N个,见https://dev59.com/IXE95IYBdhLWcg3wHqMV。 - georg
1
我创建了一个 JsPerf 来测试这里的一些解决方案。@Bergi 的解决方案似乎是最好的,而我的解决方案在需要从数组中获取多个元素时效果更好。http://jsperf.com/k-random-elements-from-array - Tibos
26个回答

342

只有两行:

// 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);

演示:


45
非常好!当然,也可以用一行代码实现:let random = array.sort(() => .5 - Math.random()).slice(0,n) - unitario
2
天才!优雅、简短、简单、快速,使用内置功能。 - Vlad
67
不错,但远非随机。第一项被选中的机会比最后一项要多得多。请参阅以下链接了解原因:https://dev59.com/IXE95IYBdhLWcg3wHqMV#18650169 - pomber
7
太棒了!如果您想保持数组不变,只需像这样更改第一行:const shuffled = [...array].sort(() => 0.5 - Math.random()); - Yair Levy
7
虽然不错,但对于大阵列而言CPU的负荷较高;如果您只需要挑选几个元素,为什么要对整个数组进行排序? - João Pimentel Ferreira
显示剩余2条评论

210
尝试使用这个非破坏性(且快速)的函数:
function getRandom(arr, n) {
    var result = new Array(n),
        len = arr.length,
        taken = new Array(len);
    if (n > len)
        throw new RangeError("getRandom: more elements taken than available");
    while (n--) {
        var x = Math.floor(Math.random() * len);
        result[n] = arr[x in taken ? taken[x] : x];
        taken[x] = --len in taken ? taken[len] : len;
    }
    return result;
}

44
老兄,我只是想说我花了大约十分钟欣赏这个算法的美。 - Prajeeth Emanuel
@Derek朕会功夫 哦,聪明,这对于从大范围中获取小样本确实更有效。特别是使用ES6的Set(它在'13年还不可用:-/)。 - Bergi
@AlexWhite 感谢您的反馈,我简直无法相信这个漏洞竟然在多年间一直被忽略。已经修复了。不过您应该发表评论而不是建议编辑。 - Bergi
2
@cbdev420 是的,这只是一个(部分)费舍尔-耶茨洗牌。 - Bergi
1
jsPerf链接目前似乎已经失效。 - KeshavDulal
显示剩余8条评论

35

这里有一个简短的独特解决方案

 array.sort(() => Math.random() - Math.random()).slice(0, n)

3
虽然它能够正常运作,但对于更大的数组可能会很慢。 - philk
5
这并不能让你获得平均分配。请参阅https://www.robweir.com/blog/2010/02/microsoft-random-browser-ballot.html。 - JasonWoof

25

Lodash_.sample_.sampleSize方法可从集合中获取一个或N个不重复键的随机元素,最多取到集合的大小。

_.sample([1, 2, 3, 4]);
// => 2

_.sampleSize([1, 2, 3], 2);
// => [3, 1]
 
_.sampleSize([1, 2, 3], 3);
// => [2, 3, 1]

什么是_?它不是标准的Javascript对象。 - vanowm
4
这句话的意思是“通常会使用'_'别名导入lodash库。” - Rubek Joshi

15

获取 5 个随机元素,而不改变原始数组:

const n = 5;
const sample = items
  .map(x => ({ x, r: Math.random() }))
  .sort((a, b) => a.r - b.r)
  .map(a => a.x)
  .slice(0, n);

(请勿用于大列表)


我们能否对这个如何工作进行更好的解释? - Qasim
@Qasim,该算法接受一个items数组(第2行),并创建一组成对的数组:原始项和一个随机数(第3行)。然后按照随机数对数组进行排序(第4行)。接着,它再次创建一个简单的项列表,只使用原始项(因此跳过随机数,第5行)。最后,它选择(随机排序的)项目数组的前n个项目(第6行)。为了更好地理解,请阅读诸如mapsort之类的函数的文档,例如https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/map。 - Jochem Schulenklopper

14

将 Python 标准库中的 .sample 迁移:

function sample(population, k){
    /*
        Chooses k unique random elements from a population sequence or set.

        Returns a new list containing elements from the population while
        leaving the original population unchanged.  The resulting list is
        in selection order so that all sub-slices will also be valid random
        samples.  This allows raffle winners (the sample) to be partitioned
        into grand prize and second place winners (the subslices).

        Members of the population need not be hashable or unique.  If the
        population contains repeats, then each occurrence is a possible
        selection in the sample.

        To choose a sample in a range of integers, use range as an argument.
        This is especially fast and space efficient for sampling from a
        large population:   sample(range(10000000), 60)

        Sampling without replacement entails tracking either potential
        selections (the pool) in a list or previous selections in a set.

        When the number of selections is small compared to the
        population, then tracking selections is efficient, requiring
        only a small set and an occasional reselection.  For
        a larger number of selections, the pool tracking method is
        preferred since the list takes less space than the
        set and it doesn't suffer from frequent reselections.
    */

    if(!Array.isArray(population))
        throw new TypeError("Population must be an array.");
    var n = population.length;
    if(k < 0 || k > n)
        throw new RangeError("Sample larger than population or is negative");

    var result = new Array(k);
    var setsize = 21;   // size of a small set minus size of an empty list

    if(k > 5)
        setsize += Math.pow(4, Math.ceil(Math.log(k * 3) / Math.log(4)))

    if(n <= setsize){
        // An n-length list is smaller than a k-length set
        var pool = population.slice();
        for(var i = 0; i < k; i++){          // invariant:  non-selected at [0,n-i)
            var j = Math.random() * (n - i) | 0;
            result[i] = pool[j];
            pool[j] = pool[n - i - 1];       // move non-selected item into vacancy
        }
    }else{
        var selected = new Set();
        for(var i = 0; i < k; i++){
            var j = Math.random() * n | 0;
            while(selected.has(j)){
                j = Math.random() * n | 0;
            }
            selected.add(j);
            result[i] = population[j];
        }
    }

    return result;
}

实现源自Lib/random.py

注:

  • setsize 基于 Python 特性进行设置以提高效率。虽然它没有为 JavaScript 进行调整,但算法仍将按预期运行。
  • 此页面中描述的其他答案由于误用 Array.prototype.sort 而不符合 ECMAScript 规范,但此算法保证在有限时间内终止。
  • 对于不支持 Set 的旧浏览器,可以使用 Array 替换集合,并将 .has(j) 替换为 .indexOf(j) > -1

与接受的答案相比的性能:


我在下面发布了一个优化版本的代码。还更正了你帖子中第二个算法中错误的随机参数。我想知道有多少人在生产中使用之前的有偏版本,希望没有什么关键问题。 - user

13
创建一个能够执行此操作的函数:
var getMeRandomElements = function(sourceArray, neededElements) {
    var result = [];
    for (var i = 0; i < neededElements; i++) {
        result.push(sourceArray[Math.floor(Math.random()*sourceArray.length)]);
    }
    return result;
}

您还应检查源数组中是否有足够的元素可供返回。如果要返回唯一元素,则应从源数组中删除已选择的元素。


好答案!看看我的答案,复制了你的代码并添加了“仅唯一元素”功能。 - evilReiko
1
此函数可以多次返回sourceArray中的同一元素。 - Sampo

12
如果您想在循环中随机获取数组中的元素而且不重复,可以使用splice从数组中删除已选择的元素:

var items = [1, 2, 3, 4, 5];
var newItems = [];

for (var i = 0; i < 3; i++) {
  var idx = Math.floor(Math.random() * items.length);
  newItems.push(items[idx]);
  items.splice(idx, 1);
}

console.log(newItems);


1
在语句 items.splice(idx,1) 中,为什么要使用数字 '1'?splice 是什么意思? - Shyam Dixit
2
根据MDN文档1deleteCount,表示要删除的旧数组元素数量。(顺便说一下,我将最后两行代码简化为newItems.push(items.splice(idx, 1)[0]))。Shyam Dixit - Kurt Peek

8

ES6语法

const pickRandom = (arr,count) => {
  let _arr = [...arr];
  return[...Array(count)].map( ()=> _arr.splice(Math.floor(Math.random() * _arr.length), 1)[0] ); 
}

简洁明了! - jeffbRTC

5

我简直不敢相信没有人提到过这种方法,非常干净、直截了当。

const getRnd = (a, n) => new Array(n).fill(null).map(() => a[Math.floor(Math.random() * a.length)]);

4
你没有确保两个项目不会重复。 - Valery
这个OP并没有要求那个。 - DedaDev
是的,他是。阅读评论和编辑清楚地说明结果中不应该有重复项。 - Lumnezia

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接