Как получить количество случайных элементов из массива?


103

Я работаю над тем, «как получить доступ к элементам случайным образом из массива в javascript». Я нашел много ссылок по этому поводу. Нравится: получить случайный элемент из массива JavaScript

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

Но в этом случае мы можем выбрать только один элемент из массива. Если нам нужно более одного элемента, как мы можем этого добиться? Как мы можем получить более одного элемента из массива?


5
Просто выполнить несколько раз?
Берги

2
Из этого утверждения мы можем это сделать ?? Цикл генерации дубликатов.
Шьям Диксит

1
Из этого точного утверждения вы не можете получить более одного элемента.
Себастьен


1
Я сделал JsPerf, чтобы протестировать здесь некоторые решения. @ Bergi's в целом кажется лучшим, а мой лучше работает, если вам нужно много элементов из массива. jsperf.com/k-random-elements-from-array
Tibos

Ответы:


97

Попробуйте эту неразрушающую (и быструю ) функцию:

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

10
Привет, парень, я просто хотел сказать, что потратил около десяти минут, оценивая красоту этого алгоритма.
Prajeeth Emanuel 08

Я бы посоветовал использовать реализацию Python, если вы стремитесь
Derek

@Derek 朕 會 功夫 Ах, умно, это действительно лучше работает с небольшими выборками из больших диапазонов. Особенно с использованием ES6 Set(который не был доступен в 13: - /)
Берги

@AlexWhite Спасибо за отзыв, я не могу поверить, что от этой ошибки все ускользали годами. Исправлена. Вы должны были опубликовать комментарий, а не предлагать правку.
Берги

2
@ cbdev420 Да, это просто (частичная) перетасовка Фишера-Ятса
Берги

189

Всего две строчки:

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

ДЕМО :


22
Очень хорошо! Один лайнер также, конечно, возможен:let random = array.sort(() => .5 - Math.random()).slice(0,n)
unitario

1
Гений! Элегантно, коротко и просто, быстро, используя встроенную функциональность.
Влад

35
Это приятно, но далеко не случайно. У первого предмета намного больше шансов быть выбранным, чем у последнего. Посмотрите, почему: stackoverflow.com/a/18650169/1325646
pomber

Это не сохраняет вид исходного массива
almathie

3
Удивительный! если вы хотите сохранить массив нетронутым, вы можете просто изменить первую строку следующим образом: const shuffled = [... array] .sort (() => 0.5 - Math.random ());
Яир Леви

16

Здесь есть однострочное уникальное решение

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

2
пока он работает, он также может быть медленным для больших массивов.
Филк

Это не дает вам равномерного распределения. См. Robweir.com/blog/2010/02/microsoft-random-browser-ballot.html
JasonWoof,

12

Портирование .sample из стандартной библиотеки Python:

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, 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, алгоритм по-прежнему будет работать, как ожидалось.
  • Некоторые другие ответы, описанные на этой странице, небезопасны в соответствии со спецификацией ECMAScript из-за неправильного использования Array.prototype.sort . Однако этот алгоритм гарантированно завершится через конечное время.
  • Для старых браузеров, которые еще не Setреализованы, набор можно заменить на Arrayи .has(j)заменить на.indexOf(j) > -1 .

Производительность против принятого ответа:


Я разместил оптимизированную версию этого кода ниже. Также исправлен неправильный случайный параметр во втором алгоритме в вашем посте. Интересно, сколько людей используют предыдущую предвзятую версию в производстве, надеюсь, ничего критического.
пользователь

11

Получение 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);

(Не используйте это для больших списков)


Можем ли мы лучше объяснить, как это работает?
Касим

10

создать функцию, которая делает это:

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

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


Хороший ответ! Взгляните на мой ответ, скопируйте ваш код и добавив функциональность «только уникальные элементы».
evilReiko

1
Эта функция может возвращать один и тот же элемент sourceArrayнесколько раз.
Sampo

6

Синтаксис ES6

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

4

Если вы хотите случайным образом получать элементы из массива в цикле без повторений, вы можете удалить выбранный элемент из массива с помощью 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»? сращивание ??
Шьям Диксит

2
Шиам Диксит , согласно документации MDN1 является deleteCountуказание количества старых элементов массива , чтобы удалить. (Кстати, последние две строчки я сократил до newItems.push(items.splice(idx, 1)[0])).
Курт Пик

2
Array.prototype.getnkill = function() {
    var a = Math.floor(Math.random()*this.length);
    var dead = this[a];
    this.splice(a,1);
    return dead;
}

//.getnkill() removes element in the array 
//so if you like you can keep a copy of the array first:

//var original= items.slice(0); 


var item = items.getnkill();

var anotheritem = items.getnkill();

2

Вот хорошо напечатанная версия. Это не подводит. Возвращает перемешанный массив, если размер выборки превышает длину исходного массива.

function sampleArr<T>(arr: T[], size: number): T[] {
  const setOfIndexes = new Set<number>();
  while (setOfIndexes.size < size && setOfIndexes.size < arr.length) {
    setOfIndexes.add(randomIntFromInterval(0, arr.length - 1));
  }
  return Array.from(setOfIndexes.values()).map(i => arr[i]);
}

const randomIntFromInterval = (min: number, max: number): number =>
  Math.floor(Math.random() * (max - min + 1) + min);

2

lodash ( https://lodash.com/ ) _.sampleи_.sampleSize .

Получает один или n случайных элементов с уникальными ключами из коллекции до размера коллекции.

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

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

Что есть _? Это не стандартный объект Javascript.
vanowm

Привет @vanowm, это lodash lodash.com
nodejh

1

РЕДАКТИРОВАТЬ : это решение работает медленнее, чем другие, представленные здесь (которые объединяют исходный массив), если вы хотите получить только несколько элементов. Скорость этого решения зависит только от количества элементов в исходном массиве, в то время как скорость решения для склейки зависит от количества элементов, требуемых в выходном массиве.

Если вам нужны не повторяющиеся случайные элементы, вы можете перетасовать свой массив, а затем получить столько, сколько хотите:

function shuffle(array) {
    var counter = array.length, temp, index;

    // While there are elements in the array
    while (counter--) {
        // Pick a random index
        index = (Math.random() * counter) | 0;

        // And swap the last element with it
        temp = array[counter];
        array[counter] = array[index];
        array[index] = temp;
    }

    return array;
}

var arr = [0,1,2,3,4,5,7,8,9];

var randoms = shuffle(arr.slice(0)); // array is cloned so it won't be destroyed
randoms.length = 4; // get 4 random elements

ДЕМО: http://jsbin.com/UHUHuqi/1/edit

Функция перемешивания взята отсюда: https://stackoverflow.com/a/6274398/1669279


Это зависит от процента случайных элементов, требуемых из массива. Если вам нужно 9 случайных элементов из массива из 10 элементов, перетасовка наверняка будет быстрее, чем извлечение 9 случайных элементов один за другим. Если процент, для которого это полезно, составляет менее 50%, тогда бывают случаи использования, когда это решение является самым быстрым. Иначе признаю, что это бесполезно :).
Tibos

Я имел в виду, что перемешивание 9 элементов происходит быстрее, чем перемешивание 10 элементов. Между прочим, я уверен, что ОП не хочет уничтожать свой входной массив…
Берги

Не думаю, что понимаю, как перетасовка 9 элементов помогает в этой проблеме. Я знаю, что если вам нужно больше половины массива, вы можете просто вырезать случайные элементы, пока не останется то, сколько хотите, а затем перемешайте, чтобы получить случайный порядок. Что я пропустил? PS: Исправлено разрушение массива, спасибо.
Tibos

Он не имеет ничего общего с «половиной». Вам просто нужно выполнить столько же работы, сколько элементов, которые вы хотите вернуть, вам не нужно обрабатывать весь массив в любой момент. Ваш текущий код имеет сложность O(n+k)(n элементов в массиве, вы хотите k из них), хотя O(k)это возможно (и оптимально).
Берги

1
Хорошо, в вашем коде есть больше того, O(2n)что можно было бы сократить, O(n+k)если бы вы изменили цикл while (counter-- > len-k)и извлекли из него последние (вместо первых) kэлементы. На самом деле splice(i, 1)нет O(1), но O(k)решение все еще возможно (см. Мой ответ). Однако сложность пространства, O(n+k)к сожалению, остается неизменной, но может O(2k)зависеть от реализации разреженного массива.
Берги

1

Мне нужна была функция для решения этой проблемы, поэтому я поделюсь ею здесь.

    const getRandomItem = function(arr) {
        return arr[Math.floor(Math.random() * arr.length)];
    }

    // original array
    let arr = [4, 3, 1, 6, 9, 8, 5];

    // number of random elements to get from arr
    let n = 4;

    let count = 0;
    // new array to push random item in
    let randomItems = []
    do {
        let item = getRandomItem(arr);
        randomItems.push(item);
        // update the original array and remove the recently pushed item
        arr.splice(arr.indexOf(item), 1);
        count++;
    } while(count < n);

    console.log(randomItems);
    console.log(arr);

Примечание: если n = arr.lengthтогда в основном вы перетасовываете массив arrи randomItemsвозвращает этот перемешанный массив.

Демо


1

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

Метод 01

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

используя этот метод, некоторые элементы имеют более высокие шансы по сравнению с другими.

calculateProbability = function(number=0 ,iterations=10000,arraySize=100) { 
let occ = 0 
for (let index = 0; index < iterations; index++) {
   const myArray= Array.from(Array(arraySize).keys()) //=> [0, 1, 2, 3, 4, ... arraySize]
   
  /** Wrong Method */
    const arr = myArray.sort(function() {
     return val= .5 - Math.random();
      });
     
  if(arr[0]===number) {
    occ ++
    }

    
}

console.log("Probability of ",number, " = ",occ*100 /iterations,"%")

}

calculateProbability(0)
calculateProbability(0)
calculateProbability(0)
calculateProbability(50)
calculateProbability(50)
calculateProbability(50)
calculateProbability(25)
calculateProbability(25)
calculateProbability(25)

Способ 2

Используя этот метод, элементы имеют одинаковую вероятность:

 const arr = myArray
      .map((a) => ({sort: Math.random(), value: a}))
      .sort((a, b) => a.sort - b.sort)
      .map((a) => a.value)

calculateProbability = function(number=0 ,iterations=10000,arraySize=100) { 
let occ = 0 
for (let index = 0; index < iterations; index++) {
   const myArray= Array.from(Array(arraySize).keys()) //=> [0, 1, 2, 3, 4, ... arraySize]
   

  /** Correct Method */
  const arr = myArray
  .map((a) => ({sort: Math.random(), value: a}))
  .sort((a, b) => a.sort - b.sort)
  .map((a) => a.value)
    
  if(arr[0]===number) {
    occ ++
    }

    
}

console.log("Probability of ",number, " = ",occ*100 /iterations,"%")

}

calculateProbability(0)
calculateProbability(0)
calculateProbability(0)
calculateProbability(50)
calculateProbability(50)
calculateProbability(50)
calculateProbability(25)
calculateProbability(25)
calculateProbability(25)

Правильный ответ размещен по следующей ссылке: https://stackoverflow.com/a/46545530/3811640


1

Вот оптимизированная версия кода, перенесенная из Python @Derek, с добавленной деструктивной (на месте) опцией, которая делает его самым быстрым из возможных алгоритмов, если вы можете с ним пойти. В противном случае он либо делает полную копию, либо, для небольшого количества элементов, запрошенных из большого массива, переключается на алгоритм, основанный на выборе.

// Chooses k unique random elements from pool.
function sample(pool, k, destructive) {
    var n = pool.length;

    if (k < 0 || k > n)
        throw new RangeError("Sample larger than population or is negative");

    if (destructive || n <= (k <= 5 ? 21 : 21 + Math.pow(4, Math.ceil(Math.log(k*3, 4))))) {
        if (!destructive)
            pool = Array.prototype.slice.call(pool);
        for (var i = 0; i < k; i++) { // invariant: non-selected at [i,n)
            var j = i + Math.random() * (n - i) | 0;
            var x = pool[i];
            pool[i] = pool[j];
            pool[j] = x;
        }
        pool.length = k; // truncate
        return pool;
    } else {
        var selected = new Set();
        while (selected.add(Math.random() * n | 0).size < k) {}
        return Array.prototype.map.call(selected, i => population[i]);
    }
}

По сравнению с реализацией Дерека, первый алгоритм намного быстрее в Firefox, но немного медленнее в Chrome, хотя теперь у него есть деструктивный вариант - самый производительный. Второй алгоритм просто на 5-15% быстрее. Я стараюсь не называть конкретных цифр, поскольку они различаются в зависимости от k и n и, вероятно, ничего не значат в будущем с новыми версиями браузеров.

Эвристика, которая делает выбор между алгоритмами, исходит из кода Python. Я оставил его как есть, хотя иногда выбирает более медленный. Он должен быть оптимизирован для JS, но это сложная задача, поскольку производительность угловых случаев зависит от браузера и их версии. Например, когда вы пытаетесь выбрать 20 из 1000 или 1050, он переключится на первый или второй алгоритм соответственно. В этом случае первый работает в 2 раза быстрее, чем второй в Chrome 80, но в 3 раза медленнее в Firefox 74.


Ошибка, log(k*3, 4)поскольку в JS нет baseаргумента. Должно бытьlog(k*3)/log(4)
дискредитирован

Кроме того, я вижу обратную сторону в том, что вы повторно используете poolв качестве result. Поскольку вы усекаете, poolего нельзя больше использовать в качестве источника для выборки, и в следующий раз, когда sampleвы будете использовать его, вам придется снова воссоздать poolиз какого-то источника. Реализация Дерека только перемешивает пул, так что его можно идеально повторно использовать для выборки без повторного создания. И я считаю, что это наиболее частый вариант использования.
дискредитирован

1

Неразрушающий
функциональный стиль программирования 2020 , работающий в неизменном контексте.

const _randomslice = (ar, size) => {
  let new_ar = [...ar];
  new_ar.splice(Math.floor(Math.random()*ar.length),1);
  return ar.length <= (size+1) ? new_ar : _randomslice(new_ar, size);
}


console.log(_randomslice([1,2,3,4,5],2));


Я понимаю, что функция не генерирует весь возможный случайный массив из исходного массива. В другом мире результат не такой случайный, как следовало бы ... есть идеи улучшения?
МАМИ Себастьян,

где _shuffleфункция?
vanowm

0

Он извлекает случайные элементы из srcArray один за другим, пока этого достаточно или в srcArray больше не осталось элементов для извлечения. Быстро и надежно.

function getNRandomValuesFromArray(srcArr, n) {
    // making copy to do not affect original srcArray
    srcArr = srcArr.slice();
    resultArr = [];
    // while srcArray isn't empty AND we didn't enough random elements
    while (srcArr.length && resultArr.length < n) {
        // remove one element from random position and add this element to the result array
        resultArr = resultArr.concat( // merge arrays
            srcArr.splice( // extract one random element
                Math.floor(Math.random() * srcArr.length),
                1
            )
        );
    }

    return resultArr;
}


Добро пожаловать в SO! При отправке ответов важно упомянуть, как работает ваш код и / или как он решает проблему OP :)
Джоэл

0

2019 г.

Это то же самое, что и ответ Лауринаса Малишаускаса , только элементы уникальны (без дубликатов).

var getMeRandomElements = function(sourceArray, neededElements) {
    var result = [];
    for (var i = 0; i < neededElements; i++) {
    var index = Math.floor(Math.random() * sourceArray.length);
        result.push(sourceArray[index]);
        sourceArray.splice(index, 1);
    }
    return result;
}

Теперь, чтобы ответить на исходный вопрос «Как получить несколько случайных элементов с помощью jQuery», вот что:

var getMeRandomElements = function(sourceArray, neededElements) {
    var result = [];
    for (var i = 0; i < neededElements; i++) {
    var index = Math.floor(Math.random() * sourceArray.length);
        result.push(sourceArray[index]);
        sourceArray.splice(index, 1);
    }
    return result;
}

var $set = $('.someClass');// <<<<< change this please

var allIndexes = [];
for(var i = 0; i < $set.length; ++i) {
    allIndexes.push(i);
}

var totalRandom = 4;// <<<<< change this please
var randomIndexes = getMeRandomElements(allIndexes, totalRandom);

var $randomElements = null;
for(var i = 0; i < randomIndexes.length; ++i) {
    var randomIndex = randomIndexes[i];
    if($randomElements === null) {
        $randomElements = $set.eq(randomIndex);
    } else {
        $randomElements.add($set.eq(randomIndex));
    }
}

// $randomElements is ready
$randomElements.css('backgroundColor', 'red');

0

Вот функция, которую я использую, которая позволяет вам легко выбирать массив с заменой или без нее:

  // Returns a random sample (either with or without replacement) from an array
  const randomSample = (arr, k, withReplacement = false) => {
    let sample;
    if (withReplacement === true) {  // sample with replacement
      sample = Array.from({length: k}, () => arr[Math.floor(Math.random() *  arr.length)]);
    } else { // sample without replacement
      if (k > arr.length) {
        throw new RangeError('Sample size must be less than or equal to array length         when sampling without replacement.')
      }
      sample = arr.map(a => [a, Math.random()]).sort((a, b) => {
        return a[1] < b[1] ? -1 : 1;}).slice(0, k).map(a => a[0]); 
      };
    return sample;
  };

Использовать его просто:

Без замены (поведение по умолчанию)

randomSample([1, 2, 3], 2) может вернуться [2, 1]

С заменой

randomSample([1, 2, 3, 4, 5, 6], 4) может вернуться [2, 3, 3, 2]


0
var getRandomElements = function(sourceArray, requiredLength) {
    var result = [];
    while(result.length<requiredLength){
        random = Math.floor(Math.random()*sourceArray.length);
        if(result.indexOf(sourceArray[random])==-1){
            result.push(sourceArray[random]);
        }
    }
    return result;
}

0

Не могу поверить, что никто не упомянул этот метод, довольно чистый и простой.

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

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

-2

Вот самый правильный ответ, и он даст вам случайные + уникальные элементы.

function randomize(array, n)
{
    var final = [];
    array = array.filter(function(elem, index, self) {
        return index == self.indexOf(elem);
    }).sort(function() { return 0.5 - Math.random() });

    var len = array.length,
    n = n > len ? len : n;

    for(var i = 0; i < n; i ++)
    {
        final[i] = array[i];
    }

    return final;
}

// randomize([1,2,3,4,5,3,2], 4);
// Result: [1, 2, 3, 5] // Something like this

Что-то странное происходит с рандомизацией в этом - у меня был тот же результат, показанный в 6 из 9 попыток (с n равным 8 и размером массива 148). Вы можете подумать о переходе на метод Фишера-Йейтса ; это то, что я сделал, и теперь работает намного лучше.
asetniop

Это занимает квадратичное время, потому что это плохая проверка уникальности и не имеет равных шансов выбрать каждый элемент, потому что он сортируется с помощью случайного сравнения.
Ry-

Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.