Як отримати кількість випадкових елементів з масиву?


102

Я працюю над тим, як отримати випадковий доступ до елементів із масиву в javascript. Я знайшов багато посилань щодо цього. Подобається: Отримати випадковий елемент із масиву JavaScript

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

Але в цьому ми можемо вибрати лише один елемент із масиву. Якщо ми хочемо більше одного елемента, то як ми можемо цього досягти? Як ми можемо отримати більше одного елемента з масиву?


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

2
З цього твердження ми можемо це зробити ?? Цикл генерації дублікатів.
Shyam Dixit

1
З цього точного твердження ви не можете отримати більше одного елемента.
Себастьян,

1
Ах, вам слід було сказати, що ви не хочете дублікатів. Потім перевірте Унікальні випадкові числа в O (1)? і моя відповідь на " Створити унікальне число в межах діапазону (0 - X), зберігаючи історію для запобігання дублікатів"
Бергі,

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

Відповіді:


96

Спробуйте цю неруйнівну (і швидку ) функцію:

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

9
Привіт, чоловіче, я просто хотів сказати, що витратив близько десяти хвилин, оцінюючи красу цього алгоритму.
Prajeeth Emanuel

Я б запропонував використовувати реалізацію Python, якщо ви йдете на швидкість: jsperf.com/pick-random-elements-from-an-array
Дерек 朕 會 功夫

@Derek 朕 會 功夫 Ах, розумно, це справді працює набагато краще для малих зразків із великих діапазонів. Особливо з використанням ES6 Set(який був недоступний у '13: - /)
Бергі

@AlexWhite Дякую за відгук, я не можу повірити, що ця помилка роками ухилялася від усіх. Виправлено. Вам слід було опублікувати коментар, а не пропонувати редагування.
Бергі,

2
@ Cbdev420 Так, це просто (часткові) Fisher-Yates перетасувати
Берги

186

Всього два рядки:

// 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
Геній! Елегантний, короткий і простий, швидкий, використовуючи вбудовану функціональність.
Влад

34
Це приємно, але далеко не випадково. Перший предмет має набагато більше шансів бути обраним, ніж останній. Дивіться тут чому: stackoverflow.com/a/18650169/1325646
pomber

Це не зберігає роду оригінального масиву
almathie

2
Дивовижний! якщо ви хочете зберегти масив цілим, ви можете просто змінити перший рядок таким чином: const shuffled = [... array] .sort (() => 0.5 - Math.random ());
Яїр Леві,


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кілька разів.
Сампо

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
Чому в елементах statement.splice (idx, 1) ви використовуєте цей '1'? зрощення ??
Shyam Dixit

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%, то існують випадки використання, коли це рішення є найшвидшим. Інакше я визнаю, що це марно :).
Тібос

Я мав на увазі, що перемішування 9 елементів швидше, ніж перетасування 10 елементів. До речі, я впевнений, що ОП не хоче знищити його масив вводу ...
Бергі,

Не думаю, що я розумію, як перемішування 9 елементів допомагає у вирішенні цієї проблеми. Я усвідомлюю, що якщо вам потрібно більше половини масиву, ви можете просто вирізати випадкові елементи, поки не залишитеся з кількістю бажаних, а потім перемішайте, щоб отримати випадкове замовлення. Чи є щось, що я пропустив? PS: Виправлено знищення масиву, дякую.
Тібос

Це не має нічого спільного з "половиною". Вам просто потрібно виконати стільки роботи, скільки елементів, які ви хочете повернути, вам не потрібно обробляти весь масив у будь-який момент. Ваш поточний код має складність 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, він відповідно переключиться на перший або другий алгоритм. У цьому випадку перший працює вдвічі швидше, ніж другий у 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));


Я усвідомлюю, що функція не генерує всіх можливих випадкових масивів із вихідного масиву. В іншому світі результат не такий випадковий, як мав би ... якась ідея вдосконалення?
MAMY Sébastien

де _shuffleфункція?
vanowm

вибачте, я відредагував допис
MAMY Sébastien

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! Розміщуючи відповіді, важливо згадати, як працює ваш код та / або як він вирішує проблему ОП :)
Джоель

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.