Come ottenere un numero di elementi casuali da un array?


103

Sto lavorando su "come accedere agli elementi in modo casuale da un array in javascript". Ho trovato molti link a riguardo. Come: Ottieni un elemento casuale dall'array JavaScript

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

Ma in questo, possiamo scegliere solo un elemento dall'array. Se vogliamo più di un elemento, come possiamo ottenerlo? Come possiamo ottenere più di un elemento da un array?


5
Basta eseguirlo più volte?
Bergi

2
Da questa affermazione possiamo farlo ?? Loop che genera duplicati.
Shyam Dixit

1
Da quell'esatta affermazione non puoi ottenere più di un elemento.
Sébastien

1
Ah, avresti dovuto dire che non vuoi duplicati. Quindi controlla Numeri casuali unici in O (1)? e la mia risposta a Genera numero univoco nell'intervallo (0 - X), mantenendo una cronologia per evitare duplicati
Bergi

1
Ho creato un JsPerf per testare alcune delle soluzioni qui. @ Bergi sembra essere il migliore in generale, mentre il mio funziona meglio se hai bisogno di molti elementi dall'array. jsperf.com/k-random-elements-from-array
Tibos

Risposte:


97

Prova questa funzione non distruttiva (e veloce ):

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
Ehi amico, volevo solo dire che ho trascorso circa dieci minuti ad apprezzare la bellezza di questo algoritmo.
Prajeeth Emanuel

Suggerirei di utilizzare l'implementazione di Python se stai andando per la velocità: jsperf.com/pick-random-elements-from-an-array
Derek 朕 會 功夫

@Derek 朕 會 功夫 Ah, intelligente, funziona molto meglio per piccoli campioni da grandi intervalli. Soprattutto con l'utilizzo di un ES6 Set(che non era disponibile nel '13: - /)
Bergi

@AlexWhite Grazie per il feedback, non posso credere che questo bug sia sfuggito a tutti per anni. Fisso. Tuttavia, dovresti aver pubblicato un commento, non suggerito una modifica.
Bergi

2
@ cbdev420 Sì, è solo un (parziale) shuffle fisher-yates
Bergi

187

Solo due righe:

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

DEMO :


22
Molto bella! Una nave anche ovviamente possibile:let random = array.sort(() => .5 - Math.random()).slice(0,n)
unitario

1
Genio! Elegante, breve e semplice, veloce, utilizzando funzionalità integrate.
Vlad

35
È bello, ma è tutt'altro che casuale. Il primo oggetto ha molte più possibilità di essere scelto rispetto all'ultimo. Vedi qui per questo: stackoverflow.com/a/18650169/1325646
pomber

Questo non preserva il tipo di matrice originale
almathie

3
Sorprendente! se vuoi mantenere intatto l'array puoi semplicemente modificare la prima riga in questo modo: const shuffled = [... array] .sort (() => 0.5 - Math.random ());
Yair Levy


12

Porting .sampledalla libreria standard 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;
}

Implementazione portata da Lib / random.py .

Appunti:

  • setsizeè impostato in base alle caratteristiche in Python per l'efficienza. Sebbene non sia stato modificato per JavaScript, l'algoritmo continuerà a funzionare come previsto.
  • Alcune altre risposte descritte in questa pagina non sono sicure secondo la specifica ECMAScript a causa dell'uso improprio di Array.prototype.sort. Questo algoritmo tuttavia è garantito per terminare in un tempo finito.
  • Per i browser meno recenti che non hanno Setimplementato, il set può essere sostituito con un Arraye .has(j)sostituito con .indexOf(j) > -1.

Prestazioni contro la risposta accettata:


Ho pubblicato una versione ottimizzata di questo codice di seguito. Corretto anche il parametro casuale sbagliato nel secondo algo nel tuo post. Mi chiedo quante persone stiano usando la precedente versione distorta in produzione, spero che nulla di critico.
utente

11

Ottenere 5 elementi casuali senza modificare l'array originale:

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

(Non usarlo per elenchi grandi)


Potremmo avere una spiegazione migliore di come funziona?
Qasim

10

creare una funzione che faccia questo:

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

dovresti anche controllare se sourceArray ha abbastanza elementi da restituire. e se desideri restituire elementi univoci, devi rimuovere l'elemento selezionato da sourceArray.


Buona risposta! Dai un'occhiata alla mia risposta, copiato il tuo codice e aggiunto la funzionalità "solo elementi unici".
evilReiko

1
Questa funzione può restituire lo stesso elemento di sourceArraypiù volte.
Sampo

6

Sintassi ES6

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

4

Se desideri ottenere in modo casuale elementi dall'array in un ciclo senza ripetizioni, puoi rimuovere l'elemento selezionato dall'array con 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
Nell'istruzione items.splice (idx, 1) perché usi questo '1'? giunzione ??
Shyam Dixit

2
Shyam Dixit , secondo la documentazione MDN la 1è il deleteCountche indica il numero di vecchi elementi dell'array da rimuovere. (Per inciso, ho ridotto le ultime due righe a newItems.push(items.splice(idx, 1)[0])).
Kurt Peek

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

Ecco una versione ben scritta. Non fallisce. Restituisce un array mescolato se la dimensione del campione è maggiore della lunghezza dell'array originale.

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/ ) _.samplee _.sampleSize.

Ottiene uno o n elementi casuali in chiavi univoche dalla raccolta fino alla dimensione della raccolta.

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

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

Che cos'è _? Non è un oggetto Javascript standard.
vanowm

Ciao @vanowm, sono lodash lodash.com
nodejh

1

EDIT : questa soluzione è più lenta di altre presentate qui (che uniscono l'array di origine) se si desidera ottenere solo pochi elementi. La velocità di questa soluzione dipende solo dal numero di elementi nell'array originale, mentre la velocità della soluzione di splicing dipende dal numero di elementi richiesti nell'array di output.

Se vuoi elementi casuali non ripetitivi, puoi mescolare il tuo array quindi ottenerne solo il numero che vuoi:

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

DEMO: http://jsbin.com/UHUHuqi/1/edit

Funzione Shuffle presa da qui: https://stackoverflow.com/a/6274398/1669279


Dipende dalla percentuale di elementi casuali richiesti dall'array. Se vuoi 9 elementi casuali da un array di 10 elementi, sarà sicuramente più veloce mescolare che estrarre 9 elementi casuali uno dopo l'altro. Se la percentuale per cui è utile è inferiore al 50%, ci sono casi d'uso in cui questa soluzione è la più veloce. Altrimenti ammetto che è inutile :).
Tibos

Volevo dire che mescolare 9 elementi è più veloce che mescolare 10 elementi. Comunque sono fiducioso che l'OP non voglia distruggere il suo array di input ...
Bergi

Non credo di capire come mescolare 9 elementi aiuti in questo problema. Sono consapevole del fatto che se vuoi più della metà dell'array puoi semplicemente tagliare gli elementi casuali fino a quando non rimani con quanti ne vuoi, quindi mescolare per ottenere un ordine casuale. C'è qualcosa che mi sono perso? PS: Risolto il problema con la distruzione dell'array, grazie.
Tibos

Non ha a che fare con "metà di". Hai solo bisogno di fare tanto lavoro quanto gli elementi che vuoi recuperare, non hai bisogno di trattare l'intero array in nessun momento. Il tuo codice attuale ha una complessità di O(n+k)(n elementi nell'array, ne vuoi k) mentre O(k)sarebbe possibile (e ottimale).
Bergi

1
OK, il tuo codice ha più cose simili O(2n)che potrebbero essere ridotte O(n+k)se cambiassi il ciclo in while (counter-- > len-k)e togliessi gli ultimi (invece del primo) kelementi da esso. In effetti splice(i, 1)non ce l'ha O(1), ma una O(k)soluzione è ancora possibile (vedi la mia risposta). La complessità dello spazio, tuttavia, rimane O(n+k)purtroppo, ma potrebbe O(2k)dipendere dall'implementazione dell'array sparse.
Bergi

1

Avevo bisogno di una funzione per risolvere questo tipo di problema, quindi la condivido qui.

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

Nota: se n = arr.lengthquindi fondamentalmente stai mescolando l'array arre randomItemsrestituisci quell'array mescolato.

Demo


1

In questa risposta, voglio condividere con te il test che devo conoscere il metodo migliore che offre uguali possibilità per tutti gli elementi di avere un sottoarray casuale.

Metodo 01

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

utilizzando questo metodo, alcuni elementi hanno maggiori possibilità di confronto con altri.

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)

Metodo 2

Utilizzando questo metodo, gli elementi hanno la stessa probabilità:

 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)

La risposta corretta è pubblicata nel seguente link: https://stackoverflow.com/a/46545530/3811640


1

Ecco una versione ottimizzata del codice portato da Python da @Derek, con l'aggiunta dell'opzione distruttiva (sul posto) che lo rende l'algoritmo più veloce possibile se puoi seguirlo. In caso contrario, crea una copia completa o, per un numero limitato di elementi richiesti da un array di grandi dimensioni, passa a un algoritmo basato sulla selezione.

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

Rispetto all'implementazione di Derek, il primo algoritmo è molto più veloce in Firefox mentre è un po 'più lento in Chrome, anche se ora ha l'opzione distruttiva, la più performante. Il secondo algoritmo è semplicemente del 5-15% più veloce. Cerco di non fornire numeri concreti poiché variano a seconda di k e ne probabilmente non significheranno nulla in futuro con le nuove versioni del browser.

L'euristica che fa la scelta tra gli algoritmi proviene dal codice Python. L'ho lasciato così com'è, anche se a volte seleziona quello più lento. Dovrebbe essere ottimizzato per JS, ma è un'attività complessa poiché le prestazioni dei casi d'angolo dipendono dal browser e dalla loro versione. Ad esempio, quando si tenta di selezionare 20 su 1000 o 1050, si passerà di conseguenza al primo o al secondo algoritmo. In questo caso, il primo è 2 volte più veloce del secondo in Chrome 80 ma 3 volte più lento in Firefox 74.


Si è verificato un errore log(k*3, 4)poiché JS non ha l' baseargomento. Dovrebbe esserelog(k*3)/log(4)
sfondato il

Inoltre, vedo uno svantaggio nella parte in cui riutilizzi poolcome file result. Dato che lo troncate poolnon può più essere usato come fonte per il campionamento e la prossima volta che lo usate sampledovrete ricreare pooldi nuovo da qualche fonte. L'implementazione di Derek rimescola solo il pool, quindi può essere perfettamente riutilizzato per il campionamento senza ricreare. E credo che questo sia il caso d'uso più frequente.
sfigurato il

1


Stile di programmazione funzionale non distruttivo 2020 , lavorando in un contesto immutabile.

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


Mi rendo conto che la funzione non genera tutti i possibili array casuali da un array sorgente. In un altro mondo, il risultato non è così casuale come dovrebbe ... qualche idea di miglioramento?
MAMY Sébastien

dov'è la _shufflefunzione?
vanowm

0

Estrae gli elementi casuali da srcArray uno per uno finché ne è abbastanza o non ci sono più elementi in srcArray rimasti per l'estrazione. Veloce e affidabile.

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


Benvenuto in SO! Quando pubblichi le risposte, è importante menzionare come funziona il tuo codice e / o come risolve il problema di OP :)
Joel

0

2019

Questo è lo stesso della risposta di Laurynas Mališauskas , solo che gli elementi sono unici (nessun duplicato).

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

Ora per rispondere alla domanda originale "Come ottenere più elementi casuali da jQuery", ecco qui:

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

Ecco una funzione che utilizzo che ti consente di campionare facilmente un array con o senza sostituzione:

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

Usarlo è semplice:

Senza sostituzione (comportamento predefinito)

randomSample([1, 2, 3], 2) può tornare [2, 1]

Con sostituzione

randomSample([1, 2, 3, 4, 5, 6], 4) può tornare [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

Non posso credere che nessuno abbia menzionato questo metodo, abbastanza pulito e diretto.

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

Non ti stai assicurando che due elementi non vengano ripetuti.
Valery

-2

Ecco la risposta più corretta e ti darà elementi casuali + unici.

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

C'è qualcosa di strano nella randomizzazione in questo: ho avuto lo stesso risultato con 6 tentativi su 9 (con un n di 8 e una dimensione dell'array di 148). Potresti pensare di passare a un metodo Fisher-Yates ; è quello che ho fatto e ora funziona molto meglio.
asetniop

Questo richiede un tempo quadratico perché esegue un cattivo controllo di unicità e non ha la stessa possibilità di selezionare ogni elemento perché ordina con un confronto casuale.
Ry-

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.