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.
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.
Risposte:
Mescola qualsiasi (I)List
con 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;
}
}
}
}
Random rng = new Random();
un per static
risolvere il problema nel post di confronto. Poiché ogni chiamata successiva sarebbe seguita dall'ultimo risultato casuale delle chiamate precedenti.
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();
var shuffledcards = cards.OrderBy(a => rng.Next());
compilr.com/grenade/sandbox/Program.cs
NewGuid
garantisce 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.
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 b
inclusivo 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 b
così 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;
}
i = list.Count - 1
, ovvero l'ultima iterazione, rnd.Next(i, list.Count)
ti restituirò. È quindi necessario i < list.Count -1
come condizione del ciclo. Bene, non 'ne hai bisogno', ma salva 1 iterazione;)
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());
}
OrderBy
utilizza 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.
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 OrderBy
QuickSort è 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".
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()
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;
}
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.
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 Random
non è thread-safe o crittograficamente abbastanza forte per le tue esigenze, puoi iniettare l'implementazione nell'operazione.
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]];
}
GetNext
o Next
?
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);
}
Random
istanza di classe fuori dalla funzione come static
variabile. Altrimenti potresti ottenere lo stesso seme di randomizzazione dal timer se chiamato in rapida successione.
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 source
non 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 0
alength - 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 Random
classe è 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 Random
classe con RNGCryptoServiceProvider
o potresti usare il RNGCryptoServiceProvider
metodo 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 x
che sarà sempre, 0 <= x && x < 1
è semplice.
return list[(int)(x * list.Count)];
Godere!
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);
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 .
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<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.
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;
}
`
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());
}
}
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);
}
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;
}
Ho trovato una soluzione interessante online.
Per gentile concessione: https://improveandrepeat.com/2018/08/a-simple-way-to-shuffle-your-lists-in-c/
var shuffled = myList.OrderBy (x => Guid.NewGuid ()). ToList ();
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.
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