Randomizza un elenco <T>


855

Qual è il modo migliore per randomizzare l'ordine di un elenco generico in C #? Ho un set finito di 75 numeri in un elenco a cui vorrei assegnare un ordine casuale, al fine di disegnarli per un'applicazione di tipo lotteria.


3
Esiste un problema aperto per l'integrazione di questa funzionalità con .NET: github.com/dotnet/corefx/issues/461
Natan

5
Potresti essere interessato a questo pacchetto NuGet , che contiene metodi di estensione per mescolare IList <T> e IEnumerable <T> usando l'algoritmo Fisher-Yates menzionato di seguito
ChaseMedallion

3
@Natan hanno chiuso il problema perché qualcuno "ha lavorato su molti progetti e sviluppato molte librerie e non ha mai avuto bisogno di un tale metodo" che mi ha fatto incazzare. Ora dobbiamo investigare noi stessi, cercare le migliori implementazioni, perdere tempo per reinventare semplicemente la ruota.
Vitalii Isaenko,

1
Sto vedendo questo giusto? Non una singola risposta funzionale valida dopo 10 anni? Forse abbiamo bisogno di un'altra ricompensa per una soluzione che affronti la quantità di entropia necessaria, per mescolare un elenco con 75 numeri $ log2 (75!) = 364 $ e come possiamo ottenerlo. Bisognerebbe ridimensionare anche un RNG crittograficamente sicuro con 256 bit di entropia almeno una volta durante un riordino del pescato.
Falco,

1
E se il solito programmatore non è in grado di risolvere questo problema, abbiamo sempre giocato allo stesso 0,01% dei possibili solitari per sempre?
Falco,

Risposte:


1137

Mescola qualsiasi (I)Listcon un metodo di estensione basato sul riordino Fisher-Yates :

private static Random rng = new Random();  

public static void Shuffle<T>(this IList<T> list)  
{  
    int n = list.Count;  
    while (n > 1) {  
        n--;  
        int k = rng.Next(n + 1);  
        T value = list[k];  
        list[k] = list[n];  
        list[n] = value;  
    }  
}

Uso:

List<Product> products = GetProducts();
products.Shuffle();

Il codice sopra usa il metodo System.Random molto criticato per selezionare i candidati swap. È veloce ma non così casuale come dovrebbe essere. Se hai bisogno di una migliore qualità di casualità nei tuoi shuffle, usa il generatore di numeri casuali in System.Security.Cryptography in questo modo:

using System.Security.Cryptography;
...
public static void Shuffle<T>(this IList<T> list)
{
    RNGCryptoServiceProvider provider = new RNGCryptoServiceProvider();
    int n = list.Count;
    while (n > 1)
    {
        byte[] box = new byte[1];
        do provider.GetBytes(box);
        while (!(box[0] < n * (Byte.MaxValue / n)));
        int k = (box[0] % n);
        n--;
        T value = list[k];
        list[k] = list[n];
        list[n] = value;
    }
}

Un semplice confronto è disponibile su questo blog (WayBack Machine).

Modifica: da quando ho scritto questa risposta un paio di anni fa, molte persone mi hanno commentato o scritto per sottolineare il grande difetto sciocco nel mio confronto. Hanno ovviamente ragione. Non c'è niente di sbagliato in System.Random se viene utilizzato nel modo in cui è stato progettato. Nel mio primo esempio sopra, ho un'istanza della variabile rng all'interno del metodo Shuffle, che chiede problemi se il metodo verrà chiamato ripetutamente. Di seguito è riportato un esempio completo e fisso basato su un commento davvero utile ricevuto oggi da @weston qui su SO.

Program.cs:

using System;
using System.Collections.Generic;
using System.Threading;

namespace SimpleLottery
{
  class Program
  {
    private static void Main(string[] args)
    {
      var numbers = new List<int>(Enumerable.Range(1, 75));
      numbers.Shuffle();
      Console.WriteLine("The winning numbers are: {0}", string.Join(",  ", numbers.GetRange(0, 5)));
    }
  }

  public static class ThreadSafeRandom
  {
      [ThreadStatic] private static Random Local;

      public static Random ThisThreadsRandom
      {
          get { return Local ?? (Local = new Random(unchecked(Environment.TickCount * 31 + Thread.CurrentThread.ManagedThreadId))); }
      }
  }

  static class MyExtensions
  {
    public static void Shuffle<T>(this IList<T> list)
    {
      int n = list.Count;
      while (n > 1)
      {
        n--;
        int k = ThreadSafeRandom.ThisThreadsRandom.Next(n + 1);
        T value = list[k];
        list[k] = list[n];
        list[n] = value;
      }
    }
  }
}

32
Cosa succede se list.Count è> Byte.MaxValue? Se n = 1000, quindi 255/1000 = 0, quindi il ciclo do sarà un ciclo infinito poiché la casella [0] <0 è sempre falsa.
AndrewS,

18
Vorrei sottolineare che il confronto è imperfetto. L'uso di <code> new Random () </code> in un ciclo è il problema, non la casualità di <code> Random </code> Spiegazione
Sven

9
È una buona idea passare un'istanza di Casuale al metodo Shuffle piuttosto che crearla all'interno come se si chiamasse Shuffle molte volte in rapida successione (ad esempio mescolando un sacco di brevi elenchi), gli elenchi saranno tutti mescolati nello stesso modo (es. il primo oggetto viene sempre spostato in posizione 3).
Mark Heath,

7
Basta fare Random rng = new Random();un per staticrisolvere il problema nel post di confronto. Poiché ogni chiamata successiva sarebbe seguita dall'ultimo risultato casuale delle chiamate precedenti.
Weston,

5
# 2, non è chiaro che la versione con il generatore Crypto funzioni perché l'intervallo massimo di un byte è 255, quindi qualsiasi elenco più grande di quello non verrà mischiato correttamente.
Mark Sowul

336

Se abbiamo solo bisogno di mescolare gli articoli in un ordine completamente casuale (solo per mescolare gli articoli in un elenco), preferisco questo codice semplice ma efficace che ordina gli articoli da guid ...

var shuffledcards = cards.OrderBy(a => Guid.NewGuid()).ToList();

40
I GUID devono essere univoci e non casuali. Parte di essa è basata sulla macchina e un'altra parte sul tempo e solo una piccola parte è casuale. blogs.msdn.com/b/oldnewthing/archive/2008/06/27/8659071.aspx
Despertar

99
Questa è una bella soluzione elegante. Se vuoi qualcosa di diverso da un guid per generare casualità, ordina semplicemente qualcos'altro. Ad esempio: var shuffledcards = cards.OrderBy(a => rng.Next()); compilr.com/grenade/sandbox/Program.cs
granata

20
Per favore no. Questo è sbagliato. "L'ordinamento casuale" NON è assolutamente casuale: si introduce un pregiudizio e, peggio ancora, si rischia di fare cicli infiniti
Vito De Tullio,

78
@VitoDeTullio: stai ricordando male. Rischi infiniti loop quando fornisci una funzione di confronto casuale ; è necessaria una funzione di confronto per produrre un ordine totale coerente . Una chiave casuale va bene. Questo suggerimento è sbagliato perché non è garantito che le guide siano casuali , non perché la tecnica di ordinamento per chiave casuale è errata.
Eric Lippert,

24
@Doug: NewGuidgarantisce solo che ti dà un GUID unico. Non fornisce garanzie sulla casualità. Se stai usando un GUID per uno scopo diverso dalla creazione di un valore univoco , lo stai facendo in modo sbagliato.
Eric Lippert,

120

Sono un po 'sorpreso da tutte le versioni goffe di questo semplice algoritmo qui. Fisher-Yates (o Knuth shuffle) è un po 'complicato ma molto compatto. Perché è complicato? Perché è necessario prestare attenzione al fatto che il generatore di numeri casuali r(a,b)restituisca un valore binclusivo o esclusivo. Ho anche modificato la descrizione di Wikipedia in modo che le persone non seguano ciecamente lo pseudocodice lì e creino bug difficili da rilevare. Per .Net, Random.Next(a,b)restituisce il numero esclusivo di bcosì senza ulteriori indugi, ecco come può essere implementato in C # /. Net:

public static void Shuffle<T>(this IList<T> list, Random rnd)
{
    for(var i=list.Count; i > 0; i--)
        list.Swap(0, rnd.Next(0, i));
}

public static void Swap<T>(this IList<T> list, int i, int j)
{
    var temp = list[i];
    list[i] = list[j];
    list[j] = temp;
}

Prova questo codice .


Non sarebbe meglio cambiare rnd (i, list.Count) in rnd (0, list.Count) in modo che qualsiasi carta possa essere scambiata?
Ciambelle

16
@Donuts - no. Se lo fai aggiungerai distorsione in shuffle.
Shital Shah,

2
Separando Swap <T> in un metodo separato, sembra che tu causi molte allocazioni T non necessarie per temp.
Clay,

2
Direi che LINQ potrebbe potenzialmente rallentare le prestazioni dello shuffle, e questo sarebbe un motivo per non usarlo, soprattutto data la relativa semplicità del codice.
Winglerw28,

7
Quando i = list.Count - 1, ovvero l'ultima iterazione, rnd.Next(i, list.Count)ti restituirò. È quindi necessario i < list.Count -1come condizione del ciclo. Bene, non 'ne hai bisogno', ma salva 1 iterazione;)
Pod

78

Metodo di estensione per IEnumerable:

public static IEnumerable<T> Randomize<T>(this IEnumerable<T> source)
{
    Random rnd = new Random();
    return source.OrderBy<T, int>((item) => rnd.Next());
}

3
Nota che questo non è thread-safe, anche se utilizzato in un elenco thread-safe
BlueRaja - Danny Pflughoeft

1
come diamo la lista <stringa> a questa funzione?
MonsterMMORPG

8
Esistono due problemi significativi con questo algoritmo: - OrderByutilizza una variante QuickSort per ordinare gli oggetti in base alle loro chiavi (apparentemente casuali). Le prestazioni di QuickSort sono O (N log N) ; al contrario, uno shuffle Fisher-Yates è O (N) . Per una raccolta di 75 elementi, questo potrebbe non essere un grosso problema, ma la differenza sarà pronunciata per le raccolte più grandi.
John Beyer,

10
... - Random.Next()può produrre una distribuzione ragionevolmente pseudo-casuale di valori, ma non garantisce che i valori siano univoci. La probabilità di chiavi duplicate aumenta (non linearmente) con N fino a quando non raggiunge la certezza quando N raggiunge 2 ^ 32 + 1. Il OrderByQuickSort è stabile ordinamento; quindi, se a più elementi viene assegnato lo stesso valore di indice pseudo-casuale, il loro ordine nella sequenza di output sarà lo stesso della sequenza di input; così, un "bias" viene introdotto nello "shuffle".
John Beyer,

27
@JohnBeyer: ci sono problemi molto, molto più grandi di quella fonte di parzialità. Ci sono solo quattro miliardi di possibili semi in Random, che è molto, molto inferiore al numero di possibili mescolamenti di un set di dimensioni moderate. È possibile generare solo una minima parte dei possibili shuffle. Questo pregiudizio sminuisce il pregiudizio a causa di collisioni accidentali.
Eric Lippert,

16

L'idea è ottenere un oggetto anonimo con oggetto e ordine casuale, quindi riordinare gli articoli in base a questo ordine e restituire valore:

var result = items.Select(x => new { value = x, order = rnd.Next() })
            .OrderBy(x => x.order).Select(x => x.value).ToList()

3
migliore soluzione di una fodera
vipin8169

1
Ti manca un punto e virgola alla fine
reggaeguitar

Se qualcuno non è sicuro di rnd aggiungilo prima del codice sopra Random rnd = new Random ();
Greg Trevellick,

10
    public static List<T> Randomize<T>(List<T> list)
    {
        List<T> randomizedList = new List<T>();
        Random rnd = new Random();
        while (list.Count > 0)
        {
            int index = rnd.Next(0, list.Count); //pick a random item from the master list
            randomizedList.Add(list[index]); //place it at the end of the randomized list
            list.RemoveAt(index);
        }
        return randomizedList;
    }


4
Non dovresti fare qualcosa del genere var listCopy = list.ToList()per evitare di eliminare tutti gli elementi dall'elenco in arrivo? Non vedo davvero perché vorresti mutare quelle liste per svuotare.
Chris Marisic,

9

EDIT Il RemoveAtè una debolezza nella mia versione precedente. Questa soluzione lo supera.

public static IEnumerable<T> Shuffle<T>(
        this IEnumerable<T> source,
        Random generator = null)
{
    if (generator == null)
    {
        generator = new Random();
    }

    var elements = source.ToArray();
    for (var i = elements.Length - 1; i >= 0; i--)
    {
        var swapIndex = generator.Next(i + 1);
        yield return elements[swapIndex];
        elements[swapIndex] = elements[i];
    }
}

Nota facoltativo Random generator, se l'implementazione del framework di base Randomnon è thread-safe o crittograficamente abbastanza forte per le tue esigenze, puoi iniettare l'implementazione nell'operazione.

In Randomquesta risposta è possibile trovare un'implementazione adatta per un'implementazione crittograficamente sicura thread-safe .


Ecco un'idea, estendi IList in un modo (si spera) efficiente.

public static IEnumerable<T> Shuffle<T>(this IList<T> list)
{
    var choices = Enumerable.Range(0, list.Count).ToList();
    var rng = new Random();
    for(int n = choices.Count; n > 1; n--)
    {
        int k = rng.Next(n);
        yield return list[choices[k]];
        choices.RemoveAt(k);
    }

    yield return list[choices[0]];
}


Vedi stackoverflow.com/questions/4412405/… . devi essere già consapevole.
nawfal,

@nawfal vedere la mia implementazione migliorata.
Jodrell,

1
hmm abbastanza giusto. È GetNexto Next?
nawfal,

4

Puoi farlo utilizzando questo semplice metodo di estensione

public static class IEnumerableExtensions
{

    public static IEnumerable<t> Randomize<t>(this IEnumerable<t> target)
    {
        Random r = new Random();

        return target.OrderBy(x=>(r.Next()));
    }        
}

e puoi usarlo nel modo seguente

// use this on any collection that implements IEnumerable!
// List, Array, HashSet, Collection, etc

List<string> myList = new List<string> { "hello", "random", "world", "foo", "bar", "bat", "baz" };

foreach (string s in myList.Randomize())
{
    Console.WriteLine(s);
}

3
Terrei l' Randomistanza di classe fuori dalla funzione come staticvariabile. Altrimenti potresti ottenere lo stesso seme di randomizzazione dal timer se chiamato in rapida successione.
Lemonseed,

Una nota interessante: se si crea rapidamente un'istanza della classe Random all'interno di un ciclo, ad esempio tra 0 ms e 200 ms l'uno dell'altro, si ha un'alta probabilità di ottenere lo stesso seme di randomizzazione, il che si traduce quindi in risultati ripetuti. Puoi comunque aggirare questo problema usando Random rand = new Random (Guid.NewGuid (). GetHashCode ()); Questo costringe effettivamente la randomizzazione a essere derivata da Guid.NewGuid ()
Baaleos,

4

Questo è il mio metodo preferito di shuffle quando è desiderabile non modificare l'originale. È una variante dell'algoritmo "inside-out" di Fisher – Yates che funziona su qualsiasi sequenza enumerabile (la lunghezza di sourcenon ha bisogno di essere conosciuta dall'inizio).

public static IList<T> NextList<T>(this Random r, IEnumerable<T> source)
{
  var list = new List<T>();
  foreach (var item in source)
  {
    var i = r.Next(list.Count + 1);
    if (i == list.Count)
    {
      list.Add(item);
    }
    else
    {
      var temp = list[i];
      list[i] = item;
      list.Add(temp);
    }
  }
  return list;
}

Questo algoritmo può anche essere implementato allocando un intervallo da 0alength - 1 ed esaurendo casualmente gli indici scambiando l'indice scelto casualmente con l'ultimo indice fino a quando tutti gli indici sono stati scelti esattamente una volta. Questo codice sopra realizza esattamente la stessa cosa ma senza l'allocazione aggiuntiva. Che è abbastanza pulito.

Per quanto riguarda la Randomclasse è un generatore di numeri per scopi generici (e se avessi organizzato una lotteria, prenderei in considerazione l'uso di qualcosa di diverso). Si basa anche su un valore seed basato sul tempo per impostazione predefinita. Una piccola soluzione al problema è seminare la Randomclasse con RNGCryptoServiceProvidero potresti usare il RNGCryptoServiceProvidermetodo in un metodo simile a questo (vedi sotto) per generare valori a virgola mobile doppi casuali scelti in modo uniforme ma eseguire una lotteria praticamente richiede la comprensione della casualità e della natura di la fonte di casualità.

var bytes = new byte[8];
_secureRng.GetBytes(bytes);
var v = BitConverter.ToUInt64(bytes, 0);
return (double)v / ((double)ulong.MaxValue + 1);

Il punto di generare un doppio casuale (esclusivamente tra 0 e 1) è usare per ridimensionare a una soluzione intera. Se hai bisogno di scegliere qualcosa da un elenco basato su un doppio casuale xche sarà sempre, 0 <= x && x < 1è semplice.

return list[(int)(x * list.Count)];

Godere!


4

Se non ti dispiace usarne due Lists, questo è probabilmente il modo più semplice per farlo, ma probabilmente non il più efficiente o imprevedibile:

List<int> xList = new List<int>() { 1, 2, 3, 4, 5 };
List<int> deck = new List<int>();

foreach (int xInt in xList)
    deck.Insert(random.Next(0, deck.Count + 1), xInt);

3

Se si dispone di un numero fisso (75), è possibile creare un array con 75 elementi, quindi enumerare l'elenco, spostando gli elementi in posizioni casuali nell'array. È possibile generare la mappatura del numero di elenco sull'indice dell'array utilizzando il riordino Fisher-Yates .


3

Di solito uso:

var list = new List<T> ();
fillList (list);
var randomizedList = new List<T> ();
var rnd = new Random ();
while (list.Count != 0)
{
    var index = rnd.Next (0, list.Count);
    randomizedList.Add (list [index]);
    list.RemoveAt (index);
}

list.RemoveAt è un'operazione O (n), che rende questa implementazione proibitivamente lenta.
George Polevoy,

1
    List<T> OriginalList = new List<T>();
    List<T> TempList = new List<T>();
    Random random = new Random();
    int length = OriginalList.Count;
    int TempIndex = 0;

    while (length > 0) {
        TempIndex = random.Next(0, length);  // get random value between 0 and original length
        TempList.Add(OriginalList[TempIndex]); // add to temp list
        OriginalList.RemoveAt(TempIndex); // remove from original list
        length = OriginalList.Count;  // get new list <T> length.
    }

    OriginalList = new List<T>();
    OriginalList = TempList; // copy all items from temp list to original list.

0

Ecco un efficiente Shuffler che restituisce una matrice di byte di valori mescolati. Non mescola mai più del necessario. Può essere riavviato da dove era stato precedentemente interrotto. La mia implementazione effettiva (non illustrata) è un componente MEF che consente un shuffler di sostituzione specificato dall'utente.

    public byte[] Shuffle(byte[] array, int start, int count)
    {
        int n = array.Length - start;
        byte[] shuffled = new byte[count];
        for(int i = 0; i < count; i++, start++)
        {
            int k = UniformRandomGenerator.Next(n--) + start;
            shuffled[i] = array[k];
            array[k] = array[start];
            array[start] = shuffled[i];
        }
        return shuffled;
    }

`


0

Ecco un modo thread-safe per farlo:

public static class EnumerableExtension
{
    private static Random globalRng = new Random();

    [ThreadStatic]
    private static Random _rng;

    private static Random rng 
    {
        get
        {
            if (_rng == null)
            {
                int seed;
                lock (globalRng)
                {
                    seed = globalRng.Next();
                }
                _rng = new Random(seed);
             }
             return _rng;
         }
    }

    public static IEnumerable<T> Shuffle<T>(this IEnumerable<T> items)
    {
        return items.OrderBy (i => rng.Next());
    }
}

0
 public Deck(IEnumerable<Card> initialCards) 
    {
    cards = new List<Card>(initialCards);
    public void Shuffle() 
     }
    {
        List<Card> NewCards = new List<Card>();
        while (cards.Count > 0) 
        {
            int CardToMove = random.Next(cards.Count);
            NewCards.Add(cards[CardToMove]);
            cards.RemoveAt(CardToMove);
        }
        cards = NewCards;
    }

public IEnumerable<string> GetCardNames() 

{
    string[] CardNames = new string[cards.Count];
    for (int i = 0; i < cards.Count; i++)
    CardNames[i] = cards[i].Name;
    return CardNames;
}

Deck deck1;
Deck deck2;
Random random = new Random();

public Form1() 
{

InitializeComponent();
ResetDeck(1);
ResetDeck(2);
RedrawDeck(1);
 RedrawDeck(2);

}



 private void ResetDeck(int deckNumber) 
    {
    if (deckNumber == 1) 
{
      int numberOfCards = random.Next(1, 11);
      deck1 = new Deck(new Card[] { });
      for (int i = 0; i < numberOfCards; i++)
           deck1.Add(new Card((Suits)random.Next(4),(Values)random.Next(1, 14)));
       deck1.Sort();
}


   else
    deck2 = new Deck();
 }

private void reset1_Click(object sender, EventArgs e) {
ResetDeck(1);
RedrawDeck(1);

}

private void shuffle1_Click(object sender, EventArgs e) 
{
    deck1.Shuffle();
    RedrawDeck(1);

}

private void moveToDeck1_Click(object sender, EventArgs e) 
{

    if (listBox2.SelectedIndex >= 0)
    if (deck2.Count > 0) {
    deck1.Add(deck2.Deal(listBox2.SelectedIndex));

}

    RedrawDeck(1);
    RedrawDeck(2);

}

2
Benvenuto in Stack Overflow! Ti preghiamo di considerare di aggiungere qualche spiegazione alla tua risposta, piuttosto che solo un enorme blocco di codice. Il nostro obiettivo qui è educare le persone in modo che comprendano la risposta e possano applicarla in altre situazioni. Se commenti il ​​tuo codice e aggiungi una spiegazione, renderai la tua risposta più utile non solo alla persona che ha posto la domanda questa volta, ma a chiunque in futuro potrebbe avere lo stesso problema.
starsplusplus,

4
Gran parte di questo codice è del tutto irrilevante per la domanda, e l'unica parte utile sostanzialmente ripete la risposta di Adam Tegen di quasi 6 anni fa.
TC,

0

Una semplice modifica della risposta accettata che restituisce un nuovo elenco invece di funzionare sul posto e accetta il più generale IEnumerable<T>come fanno molti altri metodi Linq.

private static Random rng = new Random();

/// <summary>
/// Returns a new list where the elements are randomly shuffled.
/// Based on the Fisher-Yates shuffle, which has O(n) complexity.
/// </summary>
public static IEnumerable<T> Shuffle<T>(this IEnumerable<T> list) {
    var source = list.ToList();
    int n = source.Count;
    var shuffled = new List<T>(n);
    shuffled.AddRange(source);
    while (n > 1) {
        n--;
        int k = rng.Next(n + 1);
        T value = shuffled[k];
        shuffled[k] = shuffled[n];
        shuffled[n] = value;
    }
    return shuffled;
}


-5

Vecchio post di sicuro, ma uso solo un GUID.

Items = Items.OrderBy(o => Guid.NewGuid().ToString()).ToList();

Un GUID è sempre unico e poiché viene rigenerato ogni volta che il risultato cambia ogni volta.


Compatto, ma hai un riferimento allo smistamento di nuovi Guidi consecutivi per essere casuali di alta qualità? Alcune versioni di quid / uuid hanno timestamp e altre parti non casuali.
Johan Lundberg,

8
Questa risposta è già stata data, e peggio ancora è progettata per unicità e non casualità.
Alex Angas,

-7

Un approccio molto semplice a questo tipo di problema consiste nell'utilizzare un numero di scambi di elementi casuali nell'elenco.

In pseudo-codice questo sarebbe simile a questo:

do 
    r1 = randomPositionInList()
    r2 = randomPositionInList()
    swap elements at index r1 and index r2 
for a certain number of times

1
Un problema con questo approccio è sapere quando fermarsi. Ha anche la tendenza a esagerare eventuali distorsioni nel generatore di numeri pseudo-casuali.
Mark Bessey,

3
Sì. Altamente inefficiente. Non c'è motivo di utilizzare un approccio come questo quando esistono approcci migliori e più veloci altrettanto semplici.
PeterAllenWebb,

1
non molto efficiente o efficace ... Eseguire N volte probabilmente lascerebbe molti elementi nella loro posizione originale.
NSjonas,
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.