Мне не нравится способ тасования, в основном на том основании, что это O (n log n) без веской причины, когда легко реализовать O (n) тасование. Код в вопросе «работает», в основном давая случайное (мы надеемся, уникальное!) Число каждому элементу, затем упорядочивая элементы в соответствии с этим числом.
Я предпочитаю Дюрстенфилдский вариант перетасовки Фишера-Йейтса, который меняет элементы.
Реализация простого Shuffle
метода расширения в основном состоит из вызова ToList
или ToArray
ввода, а затем с использованием существующей реализации Фишера-Йейтса. (Передайте в Random
качестве параметра, чтобы сделать жизнь в целом более приятной.) Существует множество реализаций вокруг ... У меня, вероятно, где-то есть ответ.
Хорошая вещь в таком методе расширения заключается в том, что читателю будет очень ясно, что вы на самом деле пытаетесь сделать.
РЕДАКТИРОВАТЬ: Вот простая реализация (без проверки ошибок!):
public static IEnumerable<T> Shuffle<T>(this IEnumerable<T> source, Random rng)
{
T[] elements = source.ToArray();
// Note i > 0 to avoid final pointless iteration
for (int i = elements.Length-1; i > 0; i--)
{
// Swap element "i" with a random earlier element it (or itself)
int swapIndex = rng.Next(i + 1);
T tmp = elements[i];
elements[i] = elements[swapIndex];
elements[swapIndex] = tmp;
}
// Lazily yield (avoiding aliasing issues etc)
foreach (T element in elements)
{
yield return element;
}
}
РЕДАКТИРОВАТЬ: Комментарии к производительности ниже напомнили мне, что мы можем на самом деле вернуть элементы, как мы их перемешаем:
public static IEnumerable<T> Shuffle<T>(this IEnumerable<T> source, Random rng)
{
T[] elements = source.ToArray();
for (int i = elements.Length - 1; i >= 0; i--)
{
// Swap element "i" with a random earlier element it (or itself)
// ... except we don't really need to swap it fully, as we can
// return it immediately, and afterwards it's irrelevant.
int swapIndex = rng.Next(i + 1);
yield return elements[swapIndex];
elements[swapIndex] = elements[i];
}
}
Теперь это будет выполнять столько работы, сколько нужно.
Обратите внимание, что в обоих случаях вы должны быть осторожны с экземпляром, который Random
вы используете как:
- Создание двух экземпляров
Random
примерно в одно и то же время приведет к одинаковой последовательности случайных чисел (при одинаковом использовании)
Random
не потокобезопасен.
У меня есть статья, вRandom
которой более подробно рассматриваются эти проблемы и предлагаются решения.
source.ToArray();
вы должны иметьusing System.Linq;
в том же файле. Если вы этого не сделаете, вы получите эту ошибку:'System.Collections.Generic.IEnumerable<T>' does not contain a definition for 'ToArray' and no extension method 'ToArray' accepting a first argument of type 'System.Collections.Generic.IEnumerable<T>' could be found (are you missing a using directive or an assembly reference?)
Это основано на ответе Джона Скита .
В этом ответе массив перемешивается, а затем возвращается с помощью
yield
. Конечным результатом является то, что массив хранится в памяти на время foreach, а также объекты, необходимые для итерации, и все же затраты все в начале - выход в основном пустой цикл.Этот алгоритм часто используется в играх, где выбираются первые три элемента, а остальные понадобятся только позже, если вообще будут. Мое предложение к
yield
номерам, как только они меняются местами. Это уменьшит стоимость запуска при сохранении стоимости итерации на уровне O (1) (в основном 5 операций на итерацию). Общая стоимость останется прежней, но сама перетасовка будет быстрее. В случаях, когда это называется, посколькуcollection.Shuffle().ToArray()
это теоретически не имеет значения, но в вышеупомянутых случаях использования это ускорит запуск. Кроме того, это сделает алгоритм полезным для случаев, когда вам нужно всего несколько уникальных предметов. Например, если вам нужно вытащить три карты из колоды из 52, вы можете коллировать,deck.Shuffle().Take(3)
и произойдет только три обмена (хотя сначала нужно скопировать весь массив).источник
Начиная с этой цитаты Скита:
Я продолжу немного объяснять причину уникальной надежды!
Теперь из Enumerable.OrderBy :
Это очень важно! Что произойдет, если два элемента «получат» одно и то же случайное число? Бывает, что они остаются в том же порядке, что и в массиве. Теперь, какова вероятность того, что это произойдет? Трудно точно рассчитать, но есть проблема дня рождения, которая именно эта проблема.
Теперь это реально? Это правда?
Как всегда, если сомневаетесь, напишите несколько строчек программы: http://pastebin.com/5CDnUxPG
Этот небольшой блок кода перемешивает массив из 3 элементов определенное количество раз, используя алгоритм Фишера-Йейтса, выполненный в обратном направлении, алгоритм Фишера-Йейтса, выполненный в прямом направлении (на вики- странице есть два алгоритма псевдокода ... Они производят эквивалентные результаты, но один выполняется от первого до последнего элемента, а другой выполняется от последнего до первого элемента), наивный неправильный алгоритм http://blog.codinghorror.com/the-danger-of-naivete/ и используя
.OrderBy(x => r.Next())
и.OrderBy(x => r.Next(someValue))
.Теперь Random.Next является
так что это эквивалентно
Чтобы проверить, существует ли эта проблема, мы могли бы увеличить массив (что-то очень медленное) или просто уменьшить максимальное значение генератора случайных чисел (
int.MaxValue
это не «специальное» число ... Это просто очень большое число). В конце концов, если алгоритм не смещен из-за стабильностиOrderBy
, любой диапазон значений должен давать тот же результат.Затем программа проверяет некоторые значения в диапазоне 1 ... 4096. Глядя на результат, совершенно ясно, что для низких значений (<128) алгоритм очень смещен (4-8%). С 3 значениями вам нужно как минимум
r.Next(1024)
. Если вы сделаете массив больше (4 или 5), то даже этогоr.Next(1024)
будет недостаточно. Я не специалист по тасованию и математике, но я думаю, что для каждого дополнительного бита длины массива вам нужно 2 дополнительных бита максимального значения (потому что парадокс дня рождения связан с sqrt (numvalues)), поэтому что если максимальное значение равно 2 ^ 31, я скажу, что вы должны иметь возможность сортировать массивы до 2 ^ 12/2 ^ 13 бит (4096-8192 элементов)источник
Это, вероятно, нормально для большинства целей и почти всегда генерирует действительно случайное распределение (кроме случаев, когда Random.Next () создает два одинаковых случайных целых числа).
Он работает, присваивая каждому элементу ряда случайное целое число, затем упорядочивая последовательность по этим целым числам.
Это полностью приемлемо для 99,9% приложений (если только вам абсолютно не нужно обрабатывать крайний случай выше). Кроме того, возражение Skeet относительно времени выполнения является действительным, поэтому, если вы перетасовываете длинный список, вы, возможно, не захотите его использовать.
источник
Это возникало много раз раньше. Поиск Фишера-Йейтса в StackOverflow.
Вот пример кода C #, который я написал для этого алгоритма. Вы можете параметризировать его по другому типу, если хотите.
источник
Random
в качестве статической переменной, подобной этой, -Random
не является поточно-ориентированной. См. Csharpindepth.com/Articles/Chapter12/Random.aspxRandom
это боль в использовании, как отмечено в моей статье.Похоже, хороший алгоритм тасования, если вы не слишком беспокоитесь о производительности. Единственная проблема, на которую я хотел бы обратить внимание, заключается в том, что ее поведение не контролируется, поэтому вам может быть трудно ее протестировать.
Одним из возможных вариантов является наличие начального числа, которое передается в качестве параметра в генератор случайных чисел (или генератор случайных чисел в качестве параметра), так что вы можете иметь больше контроля и тестировать его проще.
источник
Я нашел ответ Джона Скита вполне удовлетворительным, но робо-сканер моего клиента сообщит о любом случае
Random
как о недостатке безопасности. Так что я обменял это наSystem.Security.Cryptography.RNGCryptoServiceProvider
. В качестве бонуса, это исправляет ту проблему безопасности потока, которая была упомянута. С другой стороны,RNGCryptoServiceProvider
было измерено в 300 раз медленнее, чем при использованииRandom
.Использование:
Метод:
источник
Ищете алгоритм? Вы можете использовать мой
ShuffleList
класс:Затем используйте это так:
Как это работает?
Давайте первоначальный отсортированный список из 5 первых чисел:
{ 0, 1, 2, 3, 4 }
.Метод начинается с подсчета количества элементов и вызывает его
count
. Затем, сcount
уменьшением на каждом шаге, он берет случайное число между0
иcount
и перемещает его в конец списка.В следующем пошаговом примере элементы, которые можно переместить, выделены курсивом , выделенный элемент выделен жирным шрифтом :
0 1 2 3 4
0 1 2 3 4
0 1 2 4 3
0 1 2 4 3
1 2 4 3 0
1 2 4 3 0
1 2 3 0 4
1 2 3 0 4
2 3 0 4 1
2 3 0 4 1
3 0 4 1 2
источник
Этот алгоритм перемешивает, генерируя новое случайное значение для каждого значения в списке, затем упорядочивая список по этим случайным значениям. Думайте об этом как о добавлении нового столбца в таблицу в памяти, затем заполнении его GUID, а затем сортировке по этому столбцу. Похоже, эффективный способ для меня (особенно с лямбда-сахаром!)
источник
Немного не связанный, но вот интересный метод (который, хотя он действительно избыточен, ДЕЙСТВИТЕЛЬНО был реализован) для действительно случайного генерирования бросков костей!
Кубик-O-Matic
Причина, по которой я публикую это здесь, заключается в том, что он делает несколько интересных замечаний о том, как его пользователи отреагировали на идею использования алгоритмов для перетасовки поверх реальных костей. Конечно, в реальном мире такое решение предназначено только для действительно экстремальных концов спектра, где случайность оказывает такое большое влияние и, возможно, воздействие влияет на деньги;).
источник
Я бы сказал, что многие ответы здесь, такие как «Этот алгоритм тасует, генерируя новое случайное значение для каждого значения в списке, а затем упорядочивая список по этим случайным значениям», могут быть очень неправильными!
Я думаю, что это НЕ присваивает случайное значение каждому элементу исходной коллекции. Вместо этого мог бы быть алгоритм сортировки, работающий как Quicksort, который вызвал бы функцию сравнения приблизительно n log n раз. Какой-то алгоритм действительно ожидает, что эта функция сравнения будет стабильной и всегда будет возвращать один и тот же результат!
Не может быть так, что IEnumerableSorter вызывает функцию сравнения для каждого шага алгоритма, например, быстрой сортировки, и каждый раз вызывает функцию
x => r.Next()
для обоих параметров, не кэшируя их!В этом случае вы можете действительно испортить алгоритм сортировки и сделать его намного хуже ожиданий, на которых основан алгоритм. Конечно, со временем он станет стабильным и что-то вернет.
Я мог бы проверить это позже, поместив выходные данные отладки в новую функцию «Next», чтобы посмотреть, что произойдет. В Reflector я не смог сразу узнать, как это работает.
источник
Время запуска для выполнения кода с очисткой всех потоков и кэшированием каждого нового теста,
Первый неудачный код. Он работает на LINQPad. Если вы будете следовать, чтобы проверить этот код.
list.OrderBy (x => r.Next ()) использует 38,6528 мс
list.OrderBy (x => Guid.NewGuid ()) использует 36,7634 мс (рекомендуется из MSDN.)
после второго раза оба они используют в одно и то же время.
РЕДАКТИРОВАТЬ: ТЕСТОВЫЙ КОД на Intel Core i7 4 @ 2.1 ГГц, оперативная память 8 ГБ, DDR3 @ 1600, жесткий диск SATA 5200 об / мин с [Data: www.dropbox.com/s/pbtmh5s9lw285kp/data]
Описание результата: https://www.dropbox.com/s/9dw9wl259dfs04g/ResultDescription.PNG
Статистика результатов: https://www.dropbox.com/s/ewq5ybtsvesme4d/ResultStat.PNG
Вывод:
Предположим, что LINQ OrderBy (r.Next ()) и OrderBy (Guid.NewGuid ()) не хуже, чем определяемый пользователем метод Shuffle в First Solution.
Ответ: это противоречие.
источник