it-swarm.com.ru

Выборка случайного подмножества из массива

Как правильно выбрать случайную выборку без замены из массива в 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)];

Но если это сделать несколько раз, существует риск получения одной и той же записи несколько раз.

18
Jeroen

Я предлагаю перетасовать копию массива с помощью 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);
}
38
Tim Down

Немного опоздал на вечеринку, но это можно было бы решить с помощью нового образца метода подчеркивания (подчеркивание 1.5.2 - сентябрь 2013):

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

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

Или ... если вы используете underscore.js ...

_und = require('underscore');

...

function sample(a, n) {
    return _und.take(_und.shuffle(a), n);
}

Достаточно просто.

6
ntalbs

Вы можете удалить элементы из копии массива по мере их выбора. Производительность, вероятно, не идеальна, но она может подойти для того, что вам нужно:

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;
}
2
mamapitufo

Если вы используете lodash, API изменился в 4.x:

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

https://lodash.com/docs#sampleSize

2
chovy

В то время как я решительно поддерживаю использование Shuffle Фишера-Йейтса, как предложенный Тимом Дауном , вот очень короткий метод для получения случайного подмножества по запросу, математически корректного, включая пустой набор и сам набор.

Примечание решение зависит от lodash / underscore :

function subset(arr) {
    return _.sample(arr, _.random(arr.length));
}
2
Selfish

На мой взгляд, я не считаю необходимым перетасовывать всю колоду. Вам просто нужно убедиться, что ваш сэмпл случайный, а не колода. Что вы можете сделать, это выбрать значение 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*size против 2*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;
}

Быстрее...

Как и во всех этих постах, здесь используется случай Фишера-Йейтса. Но я удалил накладные расходы на копирование массива.

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 = [];
2
tkellehe

Вы можете получить образец из 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 функции (Shuffle и Sample):

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); };
1
Luis Marin

Вот еще одна реализация, основанная на Fisher-Yater 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;
}
1
Jesús López

Возможно, я что-то упускаю, но, похоже, есть решение, которое не требует сложности или потенциальных издержек при перемешивании:

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;
}
0
AnyWhichWay