È corretto utilizzare il metodo JavaScript Array.sort () per la riproduzione casuale?


126

Stavo aiutando qualcuno con il suo codice JavaScript e i miei occhi sono stati catturati da una sezione simile a quella:

function randOrd(){
  return (Math.round(Math.random())-0.5);
}
coords.sort(randOrd);
alert(coords);

Il mio primo pensiero è stato: ehi, non può proprio funzionare! Ma poi ho fatto alcuni esperimenti e ho scoperto che in effetti almeno sembra fornire risultati ben randomizzati.

Poi ho fatto qualche ricerca sul web e quasi in cima ho trovato un articolo da cui questo codice è stato copiato in modo più intenso. Sembrava un sito e un autore abbastanza rispettabili ...

Ma il mio istinto mi dice che questo deve essere sbagliato. Soprattutto perché l'algoritmo di ordinamento non è specificato dallo standard ECMA. Penso che diversi algoritmi di ordinamento si tradurranno in diversi mescolamenti non uniformi. Alcuni algoritmi di ordinamento potrebbero anche eseguire un ciclo infinito ...

Ma tu cosa ne pensi

E come un'altra domanda ... come potrei andare ora e misurare quanto casuali sono i risultati di questa tecnica di mescolamento?

aggiornamento: ho fatto alcune misurazioni e pubblicato i risultati di seguito come una delle risposte.


solo per notare che è inutile arrotondare il risultato solo il segno conta
bormat

2
" Ho scoperto che sembra fornire risultati piacevolmente randomizzati. " - DAVVERO ???
Bergi,

Risposte:


109

Non è mai stato il mio modo preferito di mescolare, in parte perché è specifico dell'implementazione come dici tu. In particolare, mi sembra di ricordare che l'ordinamento standard delle librerie da Java o .NET (non sono sicuro di quale) possa spesso rilevare se si finisce con un confronto incoerente tra alcuni elementi (ad esempio prima si afferma A < Be B < C, ma poi C < A).

Finisce anche come uno shuffle più complesso (in termini di tempi di esecuzione) di quanto sia realmente necessario.

Preferisco l'algoritmo shuffle che suddivide effettivamente la raccolta in "shuffle" (all'inizio della raccolta, inizialmente vuota) e "non mescolata" (il resto della collezione). Ad ogni passo dell'algoritmo, scegli un elemento casuale non mischiato (che potrebbe essere il primo) e scambialo con il primo elemento non mescolato - quindi trattalo come mescolato (cioè sposta mentalmente la partizione per includerlo).

Questo è O (n) e richiede solo chiamate n-1 al generatore di numeri casuali, il che è carino. Produce anche un autentico shuffle: ogni elemento ha una possibilità n / n di finire in ogni spazio, indipendentemente dalla sua posizione originale (assumendo un RNG ragionevole). La versione ordinata si avvicina a una distribuzione uniforme (supponendo che il generatore di numeri casuali non scelga lo stesso valore due volte, il che è altamente improbabile se restituisce doppi casuali) ma trovo più facile ragionare sulla versione shuffle :)

Questo approccio è chiamato shuffle Fisher-Yates .

Considero la migliore pratica codificare questo shuffle una volta e riutilizzarlo ovunque sia necessario mescolare gli oggetti. Quindi non è necessario preoccuparsi delle implementazioni di ordinamento in termini di affidabilità o complessità. Sono solo alcune righe di codice (che non tenterò in JavaScript!)

L' articolo di Wikipedia sul mescolamento (e in particolare la sezione sugli algoritmi shuffle) parla dell'ordinamento di una proiezione casuale - vale la pena leggere la sezione sulle cattive implementazioni del mescolamento in generale, quindi sai cosa evitare.


5
Raymond Chen approfondisce l'importanza che le funzioni di confronto degli ordinamenti seguano le regole: blogs.msdn.com/oldnewthing/archive/2009/05/08/9595334.aspx
Jason Kresowaty,

1
se il mio ragionamento è corretto, la versione ordinata non produce uno shuffle "genuino"!
Christoph,

@Christoph: A pensarci bene, anche Fisher-Yates darà una distribuzione "perfetta" solo se rand (x) avrà la garanzia di essere esattamente al di sopra della sua portata. Dato che di solito ci sono 2 ^ x stati possibili per l'RNG per alcuni x, non penso che sarà esattamente anche per rand (3).
Jon Skeet,

@Jon: ma Fisher-Yates creerà 2^xstati per ciascun indice di array, cioè ci saranno 2 ^ (xn) stati totali, che dovrebbero essere un po 'più grandi di 2 ^ c - vedi la mia risposta modificata per i dettagli
Christoph

@Christoph: forse non mi sono spiegato bene. Supponiamo di avere solo 3 elementi. Scegli il primo elemento in modo casuale, tra tutti 3. Per ottenere una distribuzione completamente uniforme , dovresti essere in grado di scegliere un numero casuale nell'intervallo [0,3) in modo totalmente uniforme - e se il PRNG ha 2 ^ n possibili stati, non puoi farlo - una o due delle possibilità avranno una probabilità leggermente maggiore di verificarsi.
Jon Skeet,

118

Dopo che Jon ha già trattato la teoria , ecco un'implementazione:

function shuffle(array) {
    var tmp, current, top = array.length;

    if(top) while(--top) {
        current = Math.floor(Math.random() * (top + 1));
        tmp = array[current];
        array[current] = array[top];
        array[top] = tmp;
    }

    return array;
}

L'algoritmo è O(n), mentre dovrebbe essere l'ordinamento O(n log n). A seconda del sovraccarico dell'esecuzione del codice JS rispetto alla sort()funzione nativa , ciò potrebbe comportare una notevole differenza nelle prestazioni che dovrebbe aumentare con le dimensioni dell'array.


Nei commenti alla risposta di bobobobo , ho affermato che l'algoritmo in questione potrebbe non produrre probabilità distribuite uniformemente (a seconda dell'implementazione di sort()).

La mia argomentazione segue queste linee: un algoritmo di ordinamento richiede un certo numero cdi confronti, ad esempio c = n(n-1)/2per Bubblesort. La nostra funzione di confronto casuale rende ugualmente probabile l'esito di ciascun confronto, ovvero ci sono risultati 2^c altrettanto probabili . Ora, ogni risultato deve corrispondere a una delle n!permutazioni delle voci dell'array, il che rende impossibile una distribuzione uniforme nel caso generale. (Questa è una semplificazione, poiché il numero effettivo di confronti necessari dipende dall'array di input, ma l'affermazione dovrebbe comunque essere valida.)

Come ha sottolineato Jon, questo da solo non è un motivo per preferire Fisher-Yates all'utilizzo sort(), poiché il generatore di numeri casuali mapperà anche un numero finito di valori pseudo-casuali alle n!permutazioni. Ma i risultati di Fisher-Yates dovrebbero essere ancora migliori:

Math.random()produce un numero pseudo-casuale nell'intervallo [0;1[. Poiché JS utilizza valori a virgola mobile a precisione doppia, ciò corrisponde a 2^xpossibili valori in cui 52 ≤ x ≤ 63(sono troppo pigro per trovare il numero effettivo). Una distribuzione di probabilità generata usando Math.random()smetterà di comportarsi bene se il numero di eventi atomici è dello stesso ordine di grandezza.

Quando si utilizza Fisher-Yates, il parametro rilevante è la dimensione dell'array, che non dovrebbe mai avvicinarsi a 2^52causa di limitazioni pratiche.

Quando si ordina con una funzione di confronto casuale, la funzione in pratica si preoccupa solo se il valore di ritorno è positivo o negativo, quindi questo non sarà mai un problema. Ma ce n'è una simile: poiché la funzione di confronto è ben educata, i 2^crisultati possibili sono, come affermato, ugualmente probabili. Se c ~ n log npoi 2^c ~ n^(a·n)dove a = const, il che rende quanto meno possibile che 2^cabbia la stessa grandezza di (o anche meno di) n!e che porti quindi a una distribuzione irregolare, anche se l'algoritmo di ordinamento dove mappare uniformemente le permuteioni. Se questo ha qualche impatto pratico è al di là di me.

Il vero problema è che gli algoritmi di ordinamento non sono garantiti per mappare uniformemente sulle permutazioni. È facile vedere che Mergesort fa come è simmetrico, ma il ragionamento su qualcosa come Bubblesort o, soprattutto, Quicksort o Heapsort, non lo è.


La linea di fondo: fintanto che sort()usa Mergesort, dovresti essere ragionevolmente sicuro, tranne nei casi d'angolo (almeno spero che 2^c ≤ n!sia un caso d'angolo), in caso contrario, tutte le scommesse sono disattivate.


Grazie per l'implementazione. È incredibilmente veloce! Soprattutto rispetto a quella lenta merda che ho scritto da solo nel frattempo.
Rene Saarsoo,

1
Se stai usando la libreria underscore.js, ecco come estenderla con il metodo shuffle Fisher-Yates sopra: github.com/ryantenney/underscore/commit/…
Steve

Grazie mille per questo, la combinazione della tua e della risposta di Johns mi ha aiutato a risolvere un problema che io e un collega abbiamo trascorso quasi 4 ore insieme! Inizialmente avevamo un metodo simile all'OP, ma abbiamo scoperto che la randomizzazione era molto traballante, quindi abbiamo preso il tuo metodo e lo abbiamo leggermente modificato per lavorare con un po 'di jquery per creare un elenco di immagini (per un cursore) per ottenere un po' randomizzazione fantastica.
Ciao mondo,

16

Ho fatto alcune misurazioni di quanto casuali siano i risultati di questo ordinamento casuale ...

La mia tecnica era di prendere un piccolo array [1,2,3,4] e crearne tutte (4! = 24) permutazioni. Quindi applicherei la funzione di mescolamento all'array un gran numero di volte e conterrei quante volte viene generata ogni permutazione. Un buon algoritmo di mescolamento distribuirà i risultati in modo abbastanza uniforme su tutte le permutazioni, mentre un cattivo non creerebbe quel risultato uniforme.

Usando il codice qui sotto ho testato in Firefox, Opera, Chrome, IE6 / 7/8.

Sorprendentemente per me, l'ordinamento casuale e il vero shuffle hanno entrambi creato distribuzioni ugualmente uniformi. Quindi sembra che (come molti hanno suggerito) i browser principali stanno usando l'ordinamento di tipo merge. Questo ovviamente non significa che non ci possa essere un browser là fuori, il che fa diversamente, ma direi che significa che questo metodo di ordinamento casuale è abbastanza affidabile da usare nella pratica.

EDIT: questo test non ha davvero misurato correttamente la casualità o la mancanza della stessa. Vedi l'altra risposta che ho pubblicato.

Ma per quanto riguarda le prestazioni, la funzione shuffle offerta da Cristoph è stata un chiaro vincitore. Anche per piccoli array a quattro elementi, il vero shuffle si è comportato circa il doppio della velocità rispetto all'ordinamento casuale!

// La funzione shuffle pubblicata da Cristoph.
var shuffle = function (array) {
    var tmp, current, top = array.length;

    if (top) while (- top) {
        current = Math.floor (Math.random () * (top + 1));
        tmp = array [corrente];
        array [current] = array [top];
        array [top] = tmp;
    }

    matrice di ritorno;
};

// la funzione di ordinamento casuale
var rnd = function () {
  return Math.round (Math.random ()) - 0,5;
};
var randSort = function (A) {
  ritorno A.sort (rnd);
};

var permutations = function (A) {
  if (A.length == 1) {
    ritorno [A];
  }
  altro {
    var perms = [];
    per (var i = 0; i <A.length; i ++) {
      var x = A.slice (i, i + 1);
      var xs = A.slice (0, i) .concat (A.slice (i + 1));
      var subperms = permutazioni (xs);
      per (var j = 0; j <subperms.length; j ++) {
        perms.push (x.concat (subperms [j]));
      }
    }
    permessi di ritorno;
  }
};

var test = function (A, iterations, func) {
  // permutazioni di init
  var stats = {};
  var perms = permutazioni (A);
  per (var i in perms) {
    stats ["" + perms [i]] = 0;
  }

  // mescola molte volte e raccogli statistiche
  var start = new Date ();
  per (var i = 0; i <iterazioni; i ++) {
    var shuffled = func (A);
    Statistiche [ ""] + già mescolato ++;
  }
  var end = new Date ();

  // risultato del formato
  var arr = [];
  per (var i in stats) {
    arr.push (i + "" + stats [i]);
  }
  return arr.join ("\ n") + "\ n \ nTempo impiegato:" + ((end - start) / 1000) + "secondi.";
};

alert ("ordinamento casuale:" + test ([1,2,3,4], 100000, randSort));
alert ("shuffle:" + test ([1,2,3,4], 100000, shuffle));

11

È interessante notare che Microsoft ha usato la stessa tecnica nella pagina pick-random-browser.

Hanno usato una funzione di confronto leggermente diversa:

function RandomSort(a,b) {
    return (0.5 - Math.random());
}

Mi sembra quasi lo stesso, ma si è rivelato non così casuale ...

Così ho realizzato di nuovo alcuni test con la stessa metodologia utilizzata nell'articolo collegato e, in effetti, ho scoperto che il metodo di ordinamento casuale ha prodotto risultati imperfetti. Nuovo codice di test qui:

function shuffle(arr) {
  arr.sort(function(a,b) {
    return (0.5 - Math.random());
  });
}

function shuffle2(arr) {
  arr.sort(function(a,b) {
    return (Math.round(Math.random())-0.5);
  });
}

function shuffle3(array) {
  var tmp, current, top = array.length;

  if(top) while(--top) {
    current = Math.floor(Math.random() * (top + 1));
    tmp = array[current];
    array[current] = array[top];
    array[top] = tmp;
  }

  return array;
}

var counts = [
  [0,0,0,0,0],
  [0,0,0,0,0],
  [0,0,0,0,0],
  [0,0,0,0,0],
  [0,0,0,0,0]
];

var arr;
for (var i=0; i<100000; i++) {
  arr = [0,1,2,3,4];
  shuffle3(arr);
  arr.forEach(function(x, i){ counts[x][i]++;});
}

alert(counts.map(function(a){return a.join(", ");}).join("\n"));

Non vedo perché debba essere 0,5 - Math.random (), perché non solo Math.random ()?
Alexander Mills,

1
@AlexanderMills: sort()si suppone che la funzione di confronto passata a restituisca un numero maggiore di, minore di o uguale a zero a seconda del confronto di ae b. ( developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/… )
LarsH

@LarsH sì, questo ha senso
Alexander Mills,

9

Ho inserito una semplice pagina di prova sul mio sito Web che mostra la propensione del tuo browser attuale rispetto ad altri browser popolari utilizzando diversi metodi per la riproduzione casuale. Mostra il terribile pregiudizio del solo utilizzo Math.random()-0.5, un altro shuffle 'casuale' che non è distorto e il metodo Fisher-Yates sopra menzionato.

Puoi vedere che su alcuni browser c'è una probabilità del 50% che alcuni elementi non cambieranno posto durante lo "shuffle"!

Nota: è possibile velocizzare leggermente l'implementazione del Fisher-Yates di @Christoph per Safari modificando il codice in:

function shuffle(array) {
  for (var tmp, cur, top=array.length; top--;){
    cur = (Math.random() * (top + 1)) << 0;
    tmp = array[cur]; array[cur] = array[top]; array[top] = tmp;
  }
  return array;
}

Risultati del test: http://jsperf.com/optimized-fisher-yates


5

Penso che vada bene per i casi in cui non sei esigente riguardo alla distribuzione e vuoi che il codice sorgente sia piccolo.

In JavaScript (dove la sorgente viene trasmessa costantemente), small fa la differenza nei costi della larghezza di banda.


2
Il fatto è che sei quasi sempre più esigente riguardo alla distribuzione di quanto pensi di essere, e per "piccolo codice", c'è sempre arr = arr.map(function(n){return [Math.random(),n]}).sort().map(function(n){return n[1]});, che ha il vantaggio di non essere troppo terribilmente più lungo e effettivamente distribuito correttamente. Esistono anche varianti shuffle Knuth / FY molto compresse.
Daniel Martin,

@DanielMartin Quella copertina dovrebbe essere una risposta. Inoltre, per evitare errori di parsing, due punti e virgola devono essere aggiunti in modo che assomiglia a questo: arr = arr.map(function(n){return [Math.random(),n];}).sort().map(function(n){return n[1];});.
Giacomo1968,

2

È un trucco, certamente. In pratica, non è probabile un algoritmo a ciclo continuo. Se stai ordinando gli oggetti, puoi passare in rassegna l'array coords e fare qualcosa del tipo:

for (var i = 0; i < coords.length; i++)
    coords[i].sortValue = Math.random();

coords.sort(useSortValue)

function useSortValue(a, b)
{
  return a.sortValue - b.sortValue;
}

(e poi ripercorrili di nuovo per rimuovere sortValue)

Ancora un trucco però. Se vuoi farlo bene, devi farlo nel modo più duro :)


2

Sono passati quattro anni, ma vorrei sottolineare che il metodo di confronto casuale non sarà distribuito correttamente, indipendentemente dall'algoritmo di ordinamento che usi.

Prova:

  1. Per una serie di nelementi, ci sono esattamente n!permutazioni (cioè possibili mescolamenti).
  2. Ogni confronto durante uno shuffle è una scelta tra due serie di permutazioni. Per un comparatore casuale, esiste una possibilità 1/2 di scegliere ciascun set.
  3. Pertanto, per ogni permutazione p, la possibilità di finire con la permutazione p è una frazione con denominatore 2 ^ k (per alcuni k), poiché è una somma di tali frazioni (ad esempio 1/8 + 1/16 = 3/16 ).
  4. Per n = 3, ci sono sei permutazioni ugualmente probabili. La possibilità di ogni permutazione, quindi, è 1/6. 1/6 non può essere espresso come una frazione con una potenza di 2 come denominatore.
  5. Pertanto, l'ordinamento del lancio della moneta non si tradurrà mai in un'equa distribuzione di shuffles.

Le uniche dimensioni che potrebbero essere correttamente distribuite sono n = 0,1,2.


Come esercizio, prova a disegnare l'albero decisionale di diversi algoritmi di ordinamento per n = 3.


C'è una lacuna nella dimostrazione: se un algoritmo di ordinamento dipende dalla coerenza del comparatore e ha un tempo di esecuzione illimitato con un comparatore incoerente, può avere una somma infinita di probabilità, che può aggiungere fino a 1/6 anche se ogni denominatore nella somma è un potere di 2. Prova a trovarne uno.

Inoltre, se un comparatore ha una probabilità fissa di dare una risposta (ad esempio (Math.random() < P)*2 - 1, per costante P), la dimostrazione sopra vale. Se invece il comparatore cambia le sue probabilità in base alle risposte precedenti, potrebbe essere possibile generare risultati equi. Trovare un simile comparatore per un determinato algoritmo di ordinamento potrebbe essere un documento di ricerca.


1

Se stai usando D3 c'è una funzione shuffle integrata (usando Fisher-Yates):

var days = ['Lundi','Mardi','Mercredi','Jeudi','Vendredi','Samedi','Dimanche'];
d3.shuffle(days);

E qui Mike sta entrando nei dettagli al riguardo:

http://bost.ocks.org/mike/shuffle/


0

Ecco un approccio che utilizza un singolo array:

La logica di base è:

  • A partire da una matrice di n elementi
  • Rimuovere un elemento casuale dall'array e inserirlo nell'array
  • Rimuovere un elemento casuale dai primi n - 1 elementi dell'array e spingerlo sull'array
  • Rimuovere un elemento casuale dai primi n - 2 elementi dell'array e spingerlo sull'array
  • ...
  • Rimuovere il primo elemento dell'array e spingerlo sull'array
  • Codice:

    for(i=a.length;i--;) a.push(a.splice(Math.floor(Math.random() * (i + 1)),1)[0]);

    L'implementazione ha un rischio elevato di lasciare intatto un numero significativo di elementi. Saranno spostati nell'intero array dalla quantità di elementi inferiori che sono stati spinti in cima. C'è un modello disegnato in quel mescolamento che lo rende inaffidabile.
    Kir Kanos,

    @KirKanos, non sono sicuro di aver capito il tuo commento. La soluzione che propongo è O (n). Sicuramente "toccherà" ogni elemento. Ecco un violino da dimostrare.
    ic3b3rg

    0

    Puoi usare la Array.sort()funzione per mescolare un array - Sì.

    I risultati sono abbastanza casuali - No.

    Considera il seguente frammento di codice:

    var array = ["a", "b", "c", "d", "e"];
    var stats = {};
    array.forEach(function(v) {
      stats[v] = Array(array.length).fill(0);
    });
    //stats = {
    //    a: [0, 0, 0, ...]
    //    b: [0, 0, 0, ...]
    //    c: [0, 0, 0, ...]
    //    ...
    //    ...
    //}
    var i, clone;
    for (i = 0; i < 100; i++) {
      clone = array.slice(0);
      clone.sort(function() {
        return Math.random() - 0.5;
      });
      clone.forEach(function(v, i) {
        stats[v][i]++;
      });
    }
    
    Object.keys(stats).forEach(function(v, i) {
      console.log(v + ": [" + stats[v].join(", ") + "]");
    })

    Uscita campione:

    a [29, 38, 20,  6,  7]
    b [29, 33, 22, 11,  5]
    c [17, 14, 32, 17, 20]
    d [16,  9, 17, 35, 23]
    e [ 9,  6,  9, 31, 45]

    Idealmente, i conteggi dovrebbero essere distribuiti uniformemente (per l'esempio sopra, tutti i conteggi dovrebbero essere circa 20). Ma non lo sono. Apparentemente, la distribuzione dipende da quale algoritmo di ordinamento è implementato dal browser e da come itera gli elementi dell'array per l'ordinamento.

    Ulteriori informazioni sono fornite in questo articolo:
    Array.sort () non deve essere usato per mescolare un array


    -3

    Non c'è niente di sbagliato in questo.

    La funzione che passi a .sort () di solito assomiglia a qualcosa

    funzione sortingFunc (primo, secondo)
    {
      // esempio:
      ritorno primo - secondo;
    }
    

    Il tuo lavoro in sortingFunc è di restituire:

    • un numero negativo se prima va prima del secondo
    • un numero positivo se il primo dovrebbe andare dopo il secondo
    • e 0 se sono completamente uguali

    La suddetta funzione di ordinamento mette le cose in ordine.

    Se restituisci -s e + in modo casuale come quello che hai, otterrai un ordine casuale.

    Come in MySQL:

    SELEZIONA * dalla tabella ORDER BY rand ()
    

    5
    c'è qualcosa di sbagliato in questo approccio: in base alla algoritmo di ordinamento in uso dal implementazione JS, le probabilità non saranno equamente distribuiti!
    Christoph,

    È qualcosa di cui praticamente ci preoccupiamo?
    bobobobo,

    4
    @bobobobo: a seconda dell'applicazione, sì, a volte lo facciamo; inoltre, un corretto funzionamento shuffle()deve essere scritto solo una volta, quindi non è davvero un problema: inserisci lo snippet nel tuo caveau di codice e scoprilo ogni volta che ne hai bisogno
    Christoph,
    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.