Quel est le meilleur moyen de rendre aléatoire l'ordre d'une liste générique en C #? J'ai une série finie de 75 numéros dans une liste que je voudrais assigner à un ordre aléatoire, afin de les dessiner pour une application de type loterie.
Mélangez les (I)List
avec une méthode d'extension basée sur le shuffle de 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;
}
}
Usage:
List<Product> products = GetProducts();
products.Shuffle();
Le code ci-dessus utilise la méthode très critiquée System.Random pour sélectionner des candidats au swap. C'est rapide mais pas aussi aléatoire que cela devrait être. Si vous avez besoin d’une meilleure qualité d’aléatoire dans vos mélanges, utilisez le générateur de nombres aléatoires dans System.Security.Cryptography comme suit:
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;
}
}
Une comparaison simple est disponible sur ce blog (WayBack Machine).
Edit: Depuis que cette réponse a été écrite il y a quelques années, beaucoup de gens m'ont commenté ou écrit pour souligner le gros défaut ridicule de ma comparaison. Ils ont bien sûr raison. Il n'y a rien de mal à System.Random s'il est utilisé comme prévu. Dans mon premier exemple ci-dessus, j'instancie la variable rng à l'intérieur de la méthode Shuffle, ce qui pose problème si la méthode doit être appelée à plusieurs reprises. Vous trouverez ci-dessous un exemple complet et basé sur un commentaire très utile reçu aujourd'hui de @weston ici à 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;
}
}
}
}
Si nous avons seulement besoin de mélanger les éléments dans un ordre complètement aléatoire (juste pour mélanger les éléments dans une liste), je préfère ce code simple mais efficace qui ordonne les éléments par ...
var shuffledcards = cards.OrderBy(a => Guid.NewGuid()).ToList();
Je suis un peu surpris par toutes les versions maladroites de cet algorithme simple ici. Fisher-Yates (ou Knuth Shuffle) est un peu délicat mais très compact. Si vous allez sur Wikipedia, vous verrez une version de cet algorithme qui a une boucle inversée et beaucoup de personnes ne semblent pas vraiment comprendre pourquoi c'est inversé. La raison principale est que cette version de l'algorithme suppose que le générateur de nombres aléatoires Random(n)
à votre disposition possède les deux propriétés suivantes:
Cependant, le générateur de nombres aléatoires .Net ne vérifie pas la propriété n ° 2. La Random.Next(n)
renvoie à la place le nombre compris entre 0 et n-1. Si vous essayez d'utiliser for-loop en sens inverse, vous devrez appeler Random.Next(n+1)
qui ajoute une opération supplémentaire.
Cependant, le générateur de nombres aléatoires .Net a une autre fonction intéressante Random.Next(a,b)
qui renvoie a en b-1 inclus. Cela s’adapte parfaitement à la mise en œuvre de cet algorithme qui a une boucle for normale. Donc, sans plus tarder, voici la mise en œuvre correcte, efficace et compacte:
public static void Shuffle<T>(this IList<T> list, Random rnd)
{
for(var i=0; i < list.Count - 1; i++)
list.Swap(i, rnd.Next(i, list.Count));
}
public static void Swap<T>(this IList<T> list, int i, int j)
{
var temp = list[i];
list[i] = list[j];
list[j] = temp;
}
Méthode d'extension pour IEnumerable:
public static IEnumerable<T> Randomize<T>(this IEnumerable<T> source)
{
Random rnd = new Random();
return source.OrderBy<T, int>((item) => rnd.Next());
}
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;
}
EDIT Le RemoveAt
est une faiblesse de ma version précédente. Cette solution surmonte cela.
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];
}
}
Notez le Random generator
facultatif. Si l'implémentation de structure de base de Random
n'est pas adaptée aux threads ou suffisamment forte sur le plan cryptographique pour vos besoins, vous pouvez injecter votre implémentation dans l'opération.
Voici une idée, prolongez IList de manière (espérons-le) efficace.
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]]; }
Vous pouvez y parvenir en utilisant cette méthode d'extension simple
public static class IEnumerableExtensions
{
public static IEnumerable<t> Randomize<t>(this IEnumerable<t> target)
{
Random r = new Random();
return target.OrderBy(x=>(r.Next()));
}
}
et vous pouvez l'utiliser en procédant comme suit
// 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);
}
L'idée est d'obtenir un objet anonyme avec un élément et un ordre aléatoire, puis de réorganiser les éléments en fonction de cet ordre et de la valeur renvoyée:
var result = items.Select(x => new { value = x, order = rnd.Next() })
.OrderBy(x => x.order).Select(x => x.value).ToList()
C’est ma méthode préférée de mélange lorsqu’il est souhaitable de ne pas modifier l’original. C'est une variante de l'algorithme algorithme "inside-out" de Fisher – Yates qui fonctionne sur toutes les séquences énumérables (la longueur de source
n'a pas besoin d'être connue dès le début).
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;
}
Cet algorithme peut également être implémenté en allouant une plage allant de 0
à length - 1
et en épuisant les index de manière aléatoire en échangeant l'index choisi de manière aléatoire avec le dernier index jusqu'à ce que tous les index aient été choisis exactement une fois. Ce code ci-dessus accomplit exactement la même chose mais sans l’allocation supplémentaire. Ce qui est plutôt chouette.
En ce qui concerne la classe Random
name__, il s’agit d’un générateur de nombres à usage général (et si j’exécutais une loterie, j’envisagerais d’utiliser quelque chose de différent). Il repose également sur une valeur de départ basée sur le temps par défaut. Une petite solution au problème consiste à ensemencer la classe Random
avec le RNGCryptoServiceProvider
ou à utiliser le RNGCryptoServiceProvider
dans une méthode semblable à celle-ci (voir ci-dessous) pour générer une valeur aléatoire à virgule flottante double choisie de manière uniforme, mais le fonctionnement d'une loterie requiert une grande précision. la nature de la source aléatoire.
var bytes = new byte[8];
_secureRng.GetBytes(bytes);
var v = BitConverter.ToUInt64(bytes, 0);
return (double)v / ((double)ulong.MaxValue + 1);
Le point de générer un double aléatoire (entre 0 et 1 exclusivement) est d'utiliser l'échelle pour une solution entière. Si vous devez choisir quelque chose dans une liste basée sur un double aléatoire x
qui sera toujours 0 <= x && x < 1
est simple.
return list[(int)(x * list.Count)];
Prendre plaisir!
Si vous avez un nombre fixe (75), vous pouvez créer un tableau avec 75 éléments, puis énumérer votre liste, en déplaçant les éléments vers des positions aléatoires dans le tableau. Vous pouvez générer le mappage du numéro de liste en index de tableau à l’aide de Fisher-Yates shuffle .
J'utilise habituellement:
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);
}
Si cela ne vous dérange pas d'utiliser deux Lists
, c'est probablement la façon la plus simple de le faire, mais probablement pas la plus efficace ou la plus imprévisible:
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);
Vous pouvez le faire en triant et la comparaison se fera en utilisant aléatoire
var Rand = new Random();
var list = new List<T>();
list.Sort((a,b)=>Rand.Next(-1,2));
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);
}
Voici un Shuffler efficace qui renvoie un tableau d'octets de valeurs mélangées. Il ne mélange jamais plus que nécessaire. Il peut être redémarré à l'endroit où il s'était arrêté auparavant. Mon implémentation réelle (non montrée) est un composant MEF qui permet à un utilisateur spécifié de remplacer le mélangeur.
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;
}
`
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.
Une simple modification de réponse acceptée qui renvoie une nouvelle liste au lieu de fonctionner sur place et accepte le plus général IEnumerable<T>
comme le font beaucoup d'autres méthodes 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;
}
Voici un moyen sûr de faire cela:
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());
}
}