【问题标题】:Sampling a random subset from an array从数组中采样一个随机子集
【发布时间】:2012-08-09 17:36:40
【问题描述】:

什么是随机抽样的干净方式,而不是从 javascript 中的数组中替换?所以假设有一个数组

x = [1,2,3,4,5,6,7,8,9,10,11,12,13,14,15]

我想随机抽取 5 个唯一值;即生成长度为 5 的随机子集。要生成一个随机样本,可以执行以下操作:

x[Math.floor(Math.random()*x.length)];

但如果多次这样做,则存在多次抓取同一个条目的风险。

【问题讨论】:

标签: javascript arrays random numerical-methods


【解决方案1】:

我建议使用Fisher-Yates shuffle 对数组的副本进行洗牌并取一个切片:

function getRandomSubarray(arr, size) {
    var shuffled = arr.slice(0), i = arr.length, temp, index;
    while (i--) {
        index = Math.floor((i + 1) * Math.random());
        temp = shuffled[index];
        shuffled[index] = shuffled[i];
        shuffled[i] = temp;
    }
    return shuffled.slice(0, size);
}

var x = [1,2,3,4,5,6,7,8,9,10,11,12,13,14,15];
var fiveRandomMembers = getRandomSubarray(x, 5);

请注意,这不是获取大数组的小随机子集的最有效方法,因为它会不必要地对整个数组进行洗牌。为了获得更好的性能,您可以改为进行部分随机播放:

function getRandomSubarray(arr, size) {
    var shuffled = arr.slice(0), i = arr.length, min = i - size, temp, index;
    while (i-- > min) {
        index = Math.floor((i + 1) * Math.random());
        temp = shuffled[index];
        shuffled[index] = shuffled[i];
        shuffled[i] = temp;
    }
    return shuffled.slice(min);
}

【讨论】:

  • 应该是 i* Math.random() 而不是 (i+1) * Math.random()。 Math.random() * (i+1) 可以在 Math.floor 之后返回 i。当 i==arr.length 时, arr[i] 会导致索引越界
  • @AaronJo:不,这是故意的。 i 在计算 index 时已经递减,因此在第一次迭代中 i + 1 等于第一个函数中的 arr.length,这是正确的。
  • 对于那些已经使用d3的人,从d3-array模块导入shuffle。它还使用了这个 Fisher-Yates shuffle。详情如下。
【解决方案2】:

聚会有点晚了,但这可以通过下划线的新 sample 方法解决(下划线 1.5.2 - 2013 年 9 月):

var x = [1,2,3,4,5,6,7,8,9,10,11,12,13,14,15];

var randomFiveNumbers = _.sample(x, 5);

【讨论】:

  • 这对我来说只产生 1 个元素,而不是 5 个。
  • 来自下划线的documentation:“从列表中产生一个随机样本。传递一个数字以从列表中返回 n 个随机元素。否则将返回一个随机项。” - 你传入第二个参数了吗?
  • lodash 有一个 _.sampleSize,其工作方式如上所述:lodash.com/docs/4.17.4#sampleSize
【解决方案3】:

在我看来,我认为没有必要洗牌整副牌。您只需要确保您的样本是随机的,而不是您的套牌。您可以做的是从前面选择size 数量,然后将采样数组中的每个位置与其中的另一个位置交换。所以,如果你允许替换,你会变得越来越混乱。

function getRandom(length) { return Math.floor(Math.random()*(length)); }

function getRandomSample(array, size) {
    var length = array.length;

    for(var i = size; i--;) {
        var index = getRandom(length);
        var temp = array[index];
        array[index] = array[i];
        array[i] = temp;
    }

    return array.slice(0, size);
}

本算法只有2*size步骤,如果包含slice方法,则选择随机样本。


更多随机

为了让样本更加随机,我们可以随机选择样本的起点。但是获取样品要贵一些。

function getRandomSample(array, size) {
    var length = array.length, start = getRandom(length);

    for(var i = size; i--;) {
        var index = (start + i)%length, rindex = getRandom(length);
        var temp = array[rindex];
        array[rindex] = array[index];
        array[index] = temp;
    }
    var end = start + size, sample = array.slice(start, end);
    if(end > length)
        sample = sample.concat(array.slice(0, end - length));
    return sample;
}

使这更加随机的原因是,当您总是对前面的项目进行洗牌时,如果采样数组很大而样本很小,您往往不会经常在样本中得到它们。如果数组不应该始终相同,这将不是问题。所以,这个方法所做的就是改变这个打乱区域开始的位置。


不可更换

为了不必复制采样数组并且不用担心替换,您可以执行以下操作,但它确实为您提供 3*size2*size

function getRandomSample(array, size) {
    var length = array.length, swaps = [], i = size, temp;

    while(i--) {
        var rindex = getRandom(length);
        temp = array[rindex];
        array[rindex] = array[i];
        array[i] = temp;
        swaps.push({ from: i, to: rindex });
    }

    var sample = array.slice(0, size);

    // Put everything back.
    i = size;
    while(i--) {
         var pop = swaps.pop();
         temp = array[pop.from];
         array[pop.from] = array[pop.to];
         array[pop.to] = temp;
    }

    return sample;
}

无替换,更多随机

将提供更多随机样本的算法应用于无替换函数:

function getRandomSample(array, size) {
    var length = array.length, start = getRandom(length),
        swaps = [], i = size, temp;

    while(i--) {
        var index = (start + i)%length, rindex = getRandom(length);
        temp = array[rindex];
        array[rindex] = array[index];
        array[index] = temp;
        swaps.push({ from: index, to: rindex });
    }

    var end = start + size, sample = array.slice(start, end);
    if(end > length)
        sample = sample.concat(array.slice(0, end - length));

    // Put everything back.
    i = size;
    while(i--) {
         var pop = swaps.pop();
         temp = array[pop.from];
         array[pop.from] = array[pop.to];
         array[pop.to] = temp;
    }

    return sample;
}

更快...

与所有这些帖子一样,它使用了 Fisher-Yates Shuffle。但是,我删除了复制数组的开销。

function getRandomSample(array, size) {
    var r, i = array.length, end = i - size, temp, swaps = getRandomSample.swaps;

    while (i-- > end) {
        r = getRandom(i + 1);
        temp = array[r];
        array[r] = array[i];
        array[i] = temp;
        swaps.push(i);
        swaps.push(r);
    }

    var sample = array.slice(end);

    while(size--) {
        i = swaps.pop();
        r = swaps.pop();
        temp = array[i];
        array[i] = array[r];
        array[r] = temp;
    }

    return sample;
}
getRandomSample.swaps = [];

【讨论】:

    【解决方案4】:

    或者...如果您使用 underscore.js...

    _und = require('underscore');
    
    ...
    
    function sample(a, n) {
        return _und.take(_und.shuffle(a), n);
    }
    

    足够简单。

    【讨论】:

      【解决方案5】:

      您可以通过这种方式获得5个元素的样本:

      var sample = [1,2,3,4,5,6,7,8,9,10,11,12,13,14,15]
      .map(a => [a,Math.random()])
      .sort((a,b) => {return a[1] < b[1] ? -1 : 1;})
      .slice(0,5)
      .map(a => a[0]);
      

      您可以将其定义为要在代码中使用的函数:

      var randomSample = function(arr,num){ return arr.map(a => [a,Math.random()]).sort((a,b) => {return a[1] < b[1] ? -1 : 1;}).slice(0,num).map(a => a[0]); }
      

      或者将其添加到 Array 对象本身:

          Array.prototype.sample = function(num){ return this.map(a => [a,Math.random()]).sort((a,b) => {return a[1] < b[1] ? -1 : 1;}).slice(0,num).map(a => a[0]); };
      

      如果您愿意,您可以将代码分开以获得 2 个功能(随机播放和采样):

          Array.prototype.shuffle = function(){ return this.map(a => [a,Math.random()]).sort((a,b) => {return a[1] < b[1] ? -1 : 1;}).map(a => a[0]); };
          Array.prototype.sample = function(num){ return this.shuffle().slice(0,num); };
      

      【讨论】:

        【解决方案6】:

        虽然我强烈支持使用 Fisher-Yates Shuffle,例如 suggested by Tim Down,但这里有一个非常简短的方法,可以根据要求获得数学上正确的随机子集,包括空集和给定集本身。

        注意解决方案取决于lodash / underscore

        Lodash v4

        const _ = require('loadsh')
        
        function subset(arr) {
            return _.sampleSize(arr, _.random(arr.length))
        }
        

        Lodash v3

        const _ = require('loadsh')
        
        function subset(arr) {
            return _.sample(arr, _.random(arr.length));
        }
        

        【讨论】:

        • 投反对票。这个答案不起作用。应该是_.sampleSize(arr, _.random(arr.length - 1))
        • @MananMehta 虽然你让作者知道你为什么投反对票肯定更好,所以感谢你这样做,下次你也考虑让作者有机会更新一个 5 岁的答案。 . 写这个的时候,Lodash V4 还不存在,这对于 V3 来说仍然是正确的。无论如何,我添加了一个 V4 答案。
        【解决方案7】:

        如果您使用的是 lodash,则 API 在 4.x 中有所更改:

        const oneItem = _.sample(arr);
        const nItems = _.sampleSize(arr, n);
        

        https://lodash.com/docs#sampleSize

        【讨论】:

          【解决方案8】:

          也许我遗漏了一些东西,但似乎有一个解决方案不需要洗牌的复杂性或潜在开销:

          function sample(array,size) {
            const results = [],
              sampled = {};
            while(results.length<size && results.length<array.length) {
              const index = Math.trunc(Math.random() * array.length);
              if(!sampled[index]) {
                results.push(array[index]);
                sampled[index] = true;
              }
            }
            return results;
          }
          

          【讨论】:

          • 我同意:如果size 很小,我想这是最快的解决方案。
          【解决方案9】:

          这是另一个基于 Fisher-Yates Shuffle 的实现。但是这个针对样本大小明显小于数组长度的情况进行了优化。此实现不会扫描整个数组,也不会分配与原始数组一样大的数组。它使用稀疏数组来减少内存分配。

          function getRandomSample(array, count) {
              var indices = [];
              var result = new Array(count);
              for (let i = 0; i < count; i++ ) {
                  let j = Math.floor(Math.random() * (array.length - i) + i);
                  result[i] = array[indices[j] === undefined ? j : indices[j]];
                  indices[j] = indices[i] === undefined ? i : indices[i];
              }
              return result;
          }
          

          【讨论】:

          • 我不知道它是如何工作的,但它确实 - 并且在 count undefineds。
          • @Downvoter,请告诉我你为什么不赞成这个答案,所以我可以改进它
          【解决方案10】:

          您可以在选择元素时从数组的副本中删除元素。性能可能并不理想,但可能满足您的需要:

          function getRandom(arr, size) {
            var copy = arr.slice(0), rand = [];
            for (var i = 0; i < size && i < copy.length; i++) {
              var index = Math.floor(Math.random() * copy.length);
              rand.push(copy.splice(index, 1)[0]);
            }
            return rand;
          }
          

          【讨论】:

            【解决方案11】:

            很多这些答案都在谈论克隆、改组、切片原始数组。我很好奇为什么从熵/分布的角度来看这会有所帮助。

            我不是专家,但我确实使用索引编写了一个示例函数以避免任何数组突变——尽管它确实添加到了 Set 中。我也不知道随机分布如何,但代码很简单,我认为在这里有必要回答。

            function sample(array, size = 1) {
              const { floor, random } = Math;
              let sampleSet = new Set();
              for (let i = 0; i < size; i++) {
                let index;
                do { index = floor(random() * array.length); }
                while (sampleSet.has(index));
                sampleSet.add(index);
              }
              return [...sampleSet].map(i => array[i]);
            }
            
            const words = [
              'confused', 'astonishing', 'mint', 'engine', 'team', 'cowardly', 'cooperative',
              'repair', 'unwritten', 'detailed', 'fortunate', 'value', 'dogs', 'air', 'found',
              'crooked', 'useless', 'treatment', 'surprise', 'hill', 'finger', 'pet',
              'adjustment', 'alleged', 'income'
            ];
            
            console.log(sample(words, 4));

            【讨论】:

              【解决方案12】:

              对于非常大的数组,使用索引而不是数组的成员更有效。

              这是我在此页面上没有找到我喜欢的任何内容后最终得到的。

              /**
               * Get a random subset of an array
               * @param {Array} arr - Array to take a smaple of.
               * @param {Number} sample_size - Size of sample to pull.
               * @param {Boolean} return_indexes - If true, return indexes rather than members
               * @returns {Array|Boolean} - An array containing random a subset of the members or indexes.
               */
              function getArraySample(arr, sample_size, return_indexes = false) {
                  if(sample_size > arr.length) return false;
                  const sample_idxs = [];
                  const randomIndex = () => Math.floor(Math.random() * arr.length);
                  while(sample_size > sample_idxs.length){
                      let idx = randomIndex();
                      while(sample_idxs.includes(idx)) idx = randomIndex();
                      sample_idxs.push(idx);
                  }
                  sample_idxs.sort((a, b) => a > b ? 1 : -1);
                  if(return_indexes) return sample_idxs;
                  return sample_idxs.map(i => arr[i]);
              }
              

              【讨论】:

                【解决方案13】:

                我的方法是创建一个getRandomIndexes 方法,您可以使用该方法创建一个索引数组,该数组将从主数组中提取。在这种情况下,我添加了一个简单的逻辑来避免示例中的相同索引。这就是它的工作原理

                const getRandomIndexes = (length, size) => {
                  const indexes = [];
                  const created = {};
                
                  while (indexes.length < size) {
                    const random = Math.floor(Math.random() * length);
                    if (!created[random]) {
                      indexes.push(random);
                      created[random] = true;
                    }
                  }
                  return indexes;
                };
                

                此函数独立于您拥有的任何内容,将为您提供一个索引数组,您可以使用这些索引从长度为 length 的数组中提取值,因此可以通过以下方式进行采样

                const myArray = ['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j']
                
                getRandomIndexes(myArray.length, 3).map(i => myArray[i])
                

                每次调用该方法时,您都会得到一个不同的myArray 样本。在这一点上,这个解决方案很酷,但可以更好地采样不同的尺寸。如果你想这样做,你可以使用

                getRandomIndexes(myArray.length, Math.ceil(Math.random() * 6)).map(i => myArray[i])
                

                每次调用时都会为您提供 1-6 的不同样本量。

                我希望这会有所帮助:D

                【讨论】:

                  【解决方案14】:

                  D3-arrayshuffle 使用 Fisher-Yeates shuffle 算法对数组进行随机重新排序。它是一个变异函数——意味着原始数组在原地重新排序,这有利于性能。

                  D3 用于浏览器 - 与 node 一起使用更复杂。

                  https://github.com/d3/d3-array#shuffle

                  npm install d3-array
                  
                  

                      //import {shuffle} from "d3-array" 
                      
                      let x = [1,2,3,4,5,6,7,8,9,10,11,12,13,14,15];
                  
                      d3.shuffle(x)
                  
                      console.log(x) // it is shuffled
                  &lt;script src="https://cdnjs.cloudflare.com/ajax/libs/d3/5.0.0/d3.min.js"&gt;&lt;/script&gt;

                  如果你不想改变原始数组

                      let x = [1,2,3,4,5,6,7,8,9,10,11,12,13,14,15];
                  
                      let shuffled_x = d3.shuffle(x.slice()) //calling slice with no parameters returns a copy of the original array
                  
                      console.log(x) // not shuffled
                      console.log(shuffled_x) 
                  &lt;script src="https://cdnjs.cloudflare.com/ajax/libs/d3/5.0.0/d3.min.js"&gt;&lt;/script&gt;

                  【讨论】:

                    【解决方案15】:

                    Underscore.js 大约是 70kb。如果您不需要所有额外的废话,rando.js 只有大约 2kb(小 97%),它的工作原理如下:

                    console.log(randoSequence([8, 6, 7, 5, 3, 0, 9]).slice(-5));
                    &lt;script src="https://randojs.com/2.0.0.js"&gt;&lt;/script&gt;

                    您可以看到默认情况下它会跟踪原始索引,以防两个值相同但您仍然关心选择了哪一个。如果你不需要这些,你可以添加一个地图,像这样:

                    console.log(randoSequence([8, 6, 7, 5, 3, 0, 9]).slice(-5).map((i) =&gt; i.value));
                    &lt;script src="https://randojs.com/2.0.0.js"&gt;&lt;/script&gt;

                    【讨论】:

                      猜你喜欢
                      • 1970-01-01
                      • 2017-06-01
                      • 2019-10-02
                      • 1970-01-01
                      • 1970-01-01
                      • 1970-01-01
                      • 1970-01-01
                      • 1970-01-01
                      • 1970-01-01
                      相关资源
                      最近更新 更多