Как лучше всего рандомизировать порядок общего списка в C #? У меня есть конечный набор из 75 чисел в списке, которому я хотел бы назначить случайный порядок, чтобы нарисовать их для приложения типа лотереи.
Произвести случайный выбор списка ‹T›
Ответы (22)
Перемешайте любой (I)List
с помощью метода расширения, основанного на перемешивании Фишера-Йейтса:
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;
}
}
Использование:
List<Product> products = GetProducts();
products.Shuffle();
В приведенном выше коде для выбора кандидатов на замену используется очень критикуемый метод System.Random. Это быстро, но не так случайно, как должно быть. Если вам нужно лучшее качество случайности в перемешивании, используйте генератор случайных чисел в System.Security.Cryptography следующим образом:
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;
}
}
Доступно простое сравнение в этом блоге (WayBack Machine).
Изменить: с момента написания этого ответа пару лет назад многие люди комментировали или писали мне, чтобы указать на большой глупый недостаток в моем сравнении. Конечно, они правы. В System.Random нет ничего плохого, если он используется по назначению. В моем первом примере выше я создаю экземпляр переменной rng внутри метода Shuffle, который вызывает проблемы, если метод будет вызываться повторно. Ниже приведен фиксированный полный пример, основанный на действительно полезном комментарии, полученном сегодня от @weston здесь, на 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;
}
}
}
}
System.Random
не так плохо, как вы показываете. new Random()
- это с использованием зависящего от времени начального значения по умолчанию, поэтому в вашем тесте вообще нет рандомизации. По истинной причине см. boallen.com/random-numbers.html, но опять же, это немного Зависит от ОС, поэтому System.Random
может работать лучше или нет.
- person Wernight; 11.09.2012
Random rng = new Random();
a static
решит проблему в сравнительной публикации. Поскольку каждый последующий вызов будет следовать за предыдущими вызовами, последний случайный результат.
- person weston; 28.11.2012
ThreadSafeRandom
класс на основе ответа Марка Соулса, надеюсь, это круто!
- person weston; 10.05.2013
ThreadSafeRandom
в ASP.NET, я полагаю, вы бы сгенерировали много-много экземпляров Local
. Я бы даже не подумал, что они будут собирать мусор, пока AppDomain не сломается.
- person Chris Marisic; 17.09.2014
List<Person> a = LoadSomehow(); List<Person> b = a.ToList(); b.Shuffle<string>();
, в двух списках будут одинаковые экземпляры Person, но в разном порядке.
- person Eric J.; 16.07.2015
var shuffledList = originalList.Shuffle();
- person Dagrooms; 31.05.2016
unchecked
, ни подходящей альтернативы.
- person Marco Sulla; 24.06.2016
while
-цикла или ограничения в 255 элементов, вы можете предварительно рассчитать количество случайных байтов, необходимых для выполнения алгоритма до завершения (sizeof(int) * (list.Count - 1)
или long
при работе с необработанными массивами) и заполнить байтовый массив одним вызовом GetBytes()
. Затем оберните этот массив байтов в MemoryStream
с помощью конструктора упаковки (практически бесплатно) и прочтите из него случайные целые числа с помощью BinaryReader
. Не забывайте утилизировать одноразовые экземпляры! (Вы можете поддерживать более крупные списки, создав кольцевой буфер.)
- person Xharlie; 04.01.2017
1
из n
, а затем прибавить его обратно? Кроме того, операторы посткремента создают дополнительный экземпляр, поэтому с точки зрения производительности лучше использовать здесь оператор прекремента. При этом я бы изменил две строки на следующие: первая int k = ThreadSafeRandom.ThisThreadsRandom.Next(n);
вторая --n
. Но в любом случае я благодарен вам за предоставленный алгоритм.
- person qqqqqqq; 07.03.2020
Если нам нужно только перемешать элементы в полностью случайном порядке (просто чтобы смешать элементы в списке), я предпочитаю этот простой, но эффективный код, который упорядочивает элементы по руководству ...
var shuffledcards = cards.OrderBy(a => Guid.NewGuid()).ToList();
Как отмечали люди в комментариях, не гарантируется, что GUID будут случайными, поэтому вместо этого мы должны использовать настоящий генератор случайных чисел:
private static Random rng = new Random();
...
var shuffledcards = cards.OrderBy(a => rng.Next()).ToList();
var shuffledcards = cards.OrderBy(a => rng.Next());
compilr.com/grenade/sandbox/Program.cs.
- person grenade; 27.05.2013
NewGuid
гарантирует только то, что дает вам уникальный GUID. Это не дает никаких гарантий относительно случайности. Если вы используете GUID не для создания уникального значения, вы делаете это неправильно.
- person Eric Lippert; 14.09.2013
OrderBy(x => x.Id)
(2) Orderby(x => 5)
(3) OrderBy(x => rng.Next())
Ничто из этого не создает бесконечного цикла. значение, выбранное в OrderBy, не влияет на обработку элементов. Каждый элемент в списке получит свое значение ровно один раз. Обратите внимание, что вы были бы правы, если, например, OrderBy был пузырьковой сортировкой, при которой значения не кэшировались; но здесь дело обстоит не так.
- person Flater; 28.05.2018
Enumerable.Range(0,10).OrderBy(x => GetRandomNumberAndLogMessageToConsole());
Я полагаю, это имя достаточно наглядное.
- person Flater; 28.05.2018
Guid.NewGuid()
делает, как люди сказали, рисование из вызова взаимодействия с Windows ... если вы работаете в Windows. Однако все остальные ОС используют RNGCryptoServiceProvider
, который, скорее всего, действительно является производным от случайных байтов.
- person Jeff Putz; 18.10.2020
Random
. Добавьте настоящий ГПСЧ, и все в порядке.
- person Enigmativity; 12.07.2021
Меня немного удивили все неуклюжие версии этого простого алгоритма. Фишер-Йейтс (или перетасовка Кнута) немного сложен, но очень компактен. Почему это сложно? Потому что вам нужно обратить внимание на то, возвращает ли ваш генератор случайных чисел r(a,b)
значение, где b
является включительным или исключающим. Я также отредактировал описание в Википедии, чтобы люди не слепо следуйте псевдокоду и создавайте трудно обнаруживаемые ошибки. Для .Net Random.Next(a,b)
возвращает число без b
, поэтому, без лишних слов, вот как это можно реализовать в 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;
}
0
или list.Count-1
.
- person Oneiros; 03.01.2020
list.Swap(0, rnd.Next(0, i));
переключение на следующие исправляет это и делает его работающим, не -смещенная псевдослучайная функция: list.Swap(i-1, rnd.Next(0, i));
- person Garrison Becker; 10.08.2020
Метод расширения для IEnumerable:
public static IEnumerable<T> Randomize<T>(this IEnumerable<T> source)
{
Random rnd = new Random();
return source.OrderBy<T, int>((item) => rnd.Next());
}
OrderBy
использует вариант QuickSort для сортировки элементов по их (предположительно случайным) ключам. Производительность QuickSort составляет O (N log N); Напротив, тасование Фишера-Йетса - O (N). Для коллекции из 75 элементов это может не иметь большого значения, но разница станет заметной для больших коллекций.
- person John Beyer; 26.06.2013
Random.Next()
может давать достаточно псевдослучайное распределение значений, но не гарантирует, что значения будут уникальными. Вероятность дублирования ключей растет (нелинейно) с N до тех пор, пока не достигнет достоверности, когда N достигнет 2 ^ 32 + 1. OrderBy
QuickSort - это стабильная сортировка; таким образом, если нескольким элементам будет присвоено одно и то же значение псевдослучайного индекса, то их порядок в выходной последовательности будет таким же, как и во входной последовательности; таким образом, в тасование вносится смещение.
- person John Beyer; 26.06.2013
Идея состоит в том, чтобы получить анонимный объект с элементом и случайным порядком, а затем переупорядочить элементы в этом порядке и вернуть значение:
var result = items.Select(x => new { value = x, order = rnd.Next() })
.OrderBy(x => x.order).Select(x => x.value).ToList()
ИЗМЕНИТЬ RemoveAt
- слабое место в моей предыдущей версии. Это решение преодолевает это.
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];
}
}
Обратите внимание на необязательный Random generator
, если реализация базовой структуры Random
не является потокобезопасной или криптографически достаточно сильной для ваших нужд, вы можете внедрить свою реализацию в операцию.
Вот идея, как расширить IList (надеюсь) эффективным способом.
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]];
}
Это мой предпочтительный метод перемешивания, когда желательно не изменять оригинал. Это вариант Фишера – Йейтса "внутри- out "алгоритм, который работает с любой перечислимой последовательностью (длину source
не нужно знать с самого начала).
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;
}
Этот алгоритм также может быть реализован путем выделения диапазона от 0
до length - 1
и случайного исчерпания индексов путем замены случайно выбранного индекса последним индексом до тех пор, пока все индексы не будут выбраны ровно один раз. Этот код выше выполняет то же самое, но без дополнительного выделения. Что довольно аккуратно.
Что касается класса Random
, это генератор чисел общего назначения (и если бы я проводил лотерею, я бы подумал об использовании чего-то другого). По умолчанию он также полагается на начальное значение на основе времени. Небольшое облегчение проблемы состоит в том, чтобы засеять класс Random
классом RNGCryptoServiceProvider
, или вы можете использовать RNGCryptoServiceProvider
в методе, аналогичном этому (см. Ниже), для генерации равномерно выбранных случайных двойных значений с плавающей запятой, но для запуска лотереи в значительной степени требуется понимание случайности и природа источника случайности.
var bytes = new byte[8];
_secureRng.GetBytes(bytes);
var v = BitConverter.ToUInt64(bytes, 0);
return (double)v / ((double)ulong.MaxValue + 1);
Смысл генерации случайного числа double (исключительно от 0 до 1) заключается в масштабировании до целочисленного решения. Если вам нужно выбрать что-то из списка, основанного на случайном двойном x
, который всегда будет 0 <= x && x < 1
, это просто.
return list[(int)(x * list.Count)];
Наслаждаться!
Если вы не против использовать два Lists
, то это, вероятно, самый простой способ сделать это, но, вероятно, не самый эффективный или непредсказуемый:
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);
Вы можете добиться этого, используя этот простой метод расширения
public static class IEnumerableExtensions
{
public static IEnumerable<t> Randomize<t>(this IEnumerable<t> target)
{
Random r = new Random();
return target.OrderBy(x=>(r.Next()));
}
}
и вы можете использовать его, выполнив следующие
// 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);
}
Просто хотел предложить вариант с использованием IComparer<T>
и List.Sort()
:
public class RandomIntComparer : IComparer<int>
{
private readonly Random _random = new Random();
public int Compare(int x, int y)
{
return _random.Next(-1, 2);
}
}
Использование:
list.Sort(new RandomIntComparer());
Если у вас есть фиксированное число (75), вы можете создать массив из 75 элементов, а затем пронумеровать свой список, перемещая элементы в рандомизированные позиции в массиве. Вы можете сгенерировать отображение номера списка в индекс массива, используя Fisher-Yates перемешать.
Обычно я использую:
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);
}
Я нашел интересное решение в сети.
Предоставлено: https://improveandrepeat.com/2018/08/a-simple-way-to-shuffle-your-lists-in-c/
var shuffled = myList.OrderBy (x => Guid.NewGuid ()). ToList ();
Простая модификация принятого ответа, которая возвращает новый список вместо работы на месте и принимает более общий IEnumerable<T>
столько же другие методы 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;
}
Вот эффективный Shuffler, который возвращает байтовый массив перемешанных значений. Он никогда не перетасовывает больше, чем нужно. Его можно перезапустить с того места, где он был ранее остановлен. Моя фактическая реализация (не показана) - это компонент MEF, который позволяет использовать указанную пользователем замену.
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;
}
`
Вот поточно-ориентированный способ сделать это:
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());
}
}
Вот реализация тасования Фишера-Йейтса, которая позволяет указать количество возвращаемых элементов; следовательно, нет необходимости сначала сортировать всю коллекцию, прежде чем взять желаемое количество элементов.
Последовательность замены элементов обратная по умолчанию; и переходит от первого элемента к последнему, так что получение подмножества коллекции дает ту же (частичную) последовательность, что и перетасовка всей коллекции:
collection.TakeRandom(5).SequenceEqual(collection.Shuffle().Take(5)); // true
Этот алгоритм основан на (современной) версии Дарстенфельда Fisher-Yates shuffle в Википедии.
public static IList<T> TakeRandom<T>(this IEnumerable<T> collection, int count, Random random) => shuffle(collection, count, random);
public static IList<T> Shuffle<T>(this IEnumerable<T> collection, Random random) => shuffle(collection, null, random);
private static IList<T> shuffle<T>(IEnumerable<T> collection, int? take, Random random)
{
var a = collection.ToArray();
var n = a.Length;
if (take <= 0 || take > n) throw new ArgumentException("Invalid number of elements to return.");
var end = take ?? n;
for (int i = 0; i < end; i++)
{
var j = random.Next(i, n);
(a[i], a[j]) = (a[j], a[i]);
}
if (take.HasValue) return new ArraySegment<T>(a, 0, take.Value);
return a;
}
Ваш вопрос в том, как рандомизировать список. Это означает:
- Должны быть возможны все уникальные комбинации.
- Все уникальные комбинации должны происходить с одним и тем же распределением (AKA не является необъективным).
Большое количество ответов, опубликованных на этот вопрос, НЕ удовлетворяют двум вышеперечисленным требованиям к случайности.
Вот компактная, несмещенная псевдослучайная функция, соответствующая методу тасования Фишера-Йейтса.
public static void Shuffle<T>(this IList<T> list, Random rnd)
{
for (var i = list.Count-1; i > 0; i--)
{
var randomIndex = rnd.Next(i + 1); //maxValue (i + 1) is EXCLUSIVE
list.Swap(i, randomIndex);
}
}
public static void Swap<T>(this IList<T> list, int indexA, int indexB)
{
var temp = list[indexA];
list[indexA] = list[indexB];
list[indexB] = temp;
}
Можно использовать метод расширения Shuffle из пакета morelinq, он работает на IEnumerables
установочный пакет morelinq
using MoreLinq;
...
var randomized = list.Shuffle();
private List<GameObject> ShuffleList(List<GameObject> ActualList) {
List<GameObject> newList = ActualList;
List<GameObject> outList = new List<GameObject>();
int count = newList.Count;
while (newList.Count > 0) {
int rando = Random.Range(0, newList.Count);
outList.Add(newList[rando]);
newList.RemoveAt(rando);
}
return (outList);
}
использование :
List<GameObject> GetShuffle = ShuffleList(ActualList);
Старый пост точно, но я просто использую GUID.
Items = Items.OrderBy(o => Guid.NewGuid().ToString()).ToList();
Идентификатор GUID всегда уникален, и поскольку он восстанавливается каждый раз, когда результат изменяется каждый раз.
Очень простой подход к этой проблеме - использовать произвольное количество элементов в списке.
В псевдокоде это будет выглядеть так:
do
r1 = randomPositionInList()
r2 = randomPositionInList()
swap elements at index r1 and index r2
for a certain number of times