Является ли использование Random и OrderBy хорошим алгоритмом перемешивания?

164

Я прочитал статью о различных алгоритмах случайного воспроизведения в Coding Horror . Я видел, что где-то люди сделали это, чтобы перетасовать список:

var r = new Random();
var shuffled = ordered.OrderBy(x => r.Next());

Это хороший алгоритм тасования? Как это работает? Это приемлемый способ сделать это?

Svish
источник

Ответы:

205

Мне не нравится способ тасования, в основном на том основании, что это 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 которой более подробно рассматриваются эти проблемы и предлагаются решения.

Джон Скит
источник
5
Что ж, реализации для небольших, но важных вещей, подобных этой, я бы сказал, всегда приятно найти здесь, на StackOverflow. Так что да, пожалуйста, если хотите =)
Свиш
9
Джон - ваше объяснение Фишера-Йейтса эквивалентно приведенной в вопросе реализации (наивная версия). Дюрстенфельд / Кнут добиваются O (n) не по назначению, а по выбору из убывающего набора и замены. Таким образом, выбранное случайное число может повторяться, и алгоритм принимает только O (n).
tvanfosson
8
Возможно, вам надоело слышать от меня об этом, но в моих модульных тестах я столкнулся с небольшой проблемой, о которой вы, возможно, захотите знать. С ElementAt есть странность, которая заставляет его каждый раз вызывать расширение, что дает ненадежные результаты. В своих тестах я проверяю результат перед проверкой, чтобы избежать этого.
tvanfosson
3
@tvanfosson: совсем не болеет :) Но да, абоненты должны знать, что это лениво оценивается.
Джон Скит
4
Немного поздно, но, пожалуйста, обратите внимание, что 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?)
Powerlord
70

Это основано на ответе Джона Скита .

В этом ответе массив перемешивается, а затем возвращается с помощью yield. Конечным результатом является то, что массив хранится в памяти на время foreach, а также объекты, необходимые для итерации, и все же затраты все в начале - выход в основном пустой цикл.

Этот алгоритм часто используется в играх, где выбираются первые три элемента, а остальные понадобятся только позже, если вообще будут. Мое предложение к yieldномерам, как только они меняются местами. Это уменьшит стоимость запуска при сохранении стоимости итерации на уровне O (1) (в основном 5 операций на итерацию). Общая стоимость останется прежней, но сама перетасовка будет быстрее. В случаях, когда это называется, поскольку collection.Shuffle().ToArray()это теоретически не имеет значения, но в вышеупомянутых случаях использования это ускорит запуск. Кроме того, это сделает алгоритм полезным для случаев, когда вам нужно всего несколько уникальных предметов. Например, если вам нужно вытащить три карты из колоды из 52, вы можете коллировать, deck.Shuffle().Take(3)и произойдет только три обмена (хотя сначала нужно скопировать весь массив).

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);
        yield return elements[swapIndex];
        elements[swapIndex] = elements[i];
        // we don't actually perform the swap, we can forget about the
        // swapped element because we already returned it.
    }

    // there is one item remaining that was not returned - we return it now
    yield return elements[0]; 
}
конфигуратор
источник
Ой! Это, вероятно, не вернет все элементы в источнике. Вы не можете полагаться на случайное число, уникальное для N итераций.
P Daddy
2
Умная! (И я ненавижу эти 15 персонажей ...)
Свиш
@P папа: а? Хотите разработать?
Свиш
1
Или вы могли бы заменить> 0 на> = 0 и не должны (хотя, дополнительное попадание ГСЧ плюс избыточное назначение)
FryGuy
4
Начальная стоимость O (N) как стоимость source.ToArray ();
Дэйв Хиллиер
8

Начиная с этой цитаты Скита:

Мне не нравится способ тасования, в основном на том основании, что это O (n log n) без веской причины, когда легко реализовать O (n) тасование. Код в вопросе «работает», в основном давая случайное ( мы надеемся, уникальное! ) Число каждому элементу, затем упорядочивая элементы в соответствии с этим числом.

Я продолжу немного объяснять причину уникальной надежды!

Теперь из 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 является

32-разрядное целое число со знаком, большее или равное 0 и меньшее, чем MaxValue.

так что это эквивалентно

OrderBy(x => r.Next(int.MaxValue))

Чтобы проверить, существует ли эта проблема, мы могли бы увеличить массив (что-то очень медленное) или просто уменьшить максимальное значение генератора случайных чисел ( 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 элементов)

Ксанатос
источник
Хорошо сформулировано и отлично отображает проблему с оригинальным вопросом. Это должно быть объединено с ответом Джона.
TheSoftwareJedi
6

Это, вероятно, нормально для большинства целей и почти всегда генерирует действительно случайное распределение (кроме случаев, когда Random.Next () создает два одинаковых случайных целых числа).

Он работает, присваивая каждому элементу ряда случайное целое число, затем упорядочивая последовательность по этим целым числам.

Это полностью приемлемо для 99,9% приложений (если только вам абсолютно не нужно обрабатывать крайний случай выше). Кроме того, возражение Skeet относительно времени выполнения является действительным, поэтому, если вы перетасовываете длинный список, вы, возможно, не захотите его использовать.

ripper234
источник
4

Это возникало много раз раньше. Поиск Фишера-Йейтса в StackOverflow.

Вот пример кода C #, который я написал для этого алгоритма. Вы можете параметризировать его по другому типу, если хотите.

static public class FisherYates
{
        //      Based on Java code from wikipedia:
        //      http://en.wikipedia.org/wiki/Fisher-Yates_shuffle
        static public void Shuffle(int[] deck)
        {
                Random r = new Random();
                for (int n = deck.Length - 1; n > 0; --n)
                {
                        int k = r.Next(n+1);
                        int temp = deck[n];
                        deck[n] = deck[k];
                        deck[k] = temp;
                }
        }
}
hughdbrown
источник
2
Вы не должны использовать Randomв качестве статической переменной, подобной этой, - Randomне является поточно-ориентированной. См. Csharpindepth.com/Articles/Chapter12/Random.aspx
Джон Скит
@ Джон Скит: конечно, это законный аргумент. OTOH, OP спрашивал об алгоритме, который был совершенно неправильным, тогда как это правильно (кроме случая использования многопоточного тасования карт).
hughdbrown
1
Это просто означает, что это «менее неправильно», чем подход ОП. Это не означает, что это код, который следует использовать, не понимая, что его нельзя безопасно использовать в многопоточном контексте ... это то, что вы не упомянули. Разумно ожидать, что статические члены могут безопасно использоваться из нескольких потоков.
Джон Скит
@ Джон Скит: Конечно, я могу изменить это. Готово. Я склонен думать, что возвращаясь к вопросу, на который ответили три с половиной года назад, и говоря: «Это неправильно, потому что он не обрабатывает многопоточный сценарий использования», когда OP никогда не спрашивал ни о чем, кроме алгоритма, чрезмерного. Просмотрите мои ответы за эти годы. Часто я давал ответы ОП, которые выходили за рамки заявленных требований. Меня критиковали за это. Я не ожидал бы, что ОП получат ответы, подходящие для всех возможных применений.
hughdbrown
Я только посетил этот ответ, потому что кто-то еще указал мне на него в чате. Несмотря на то , что ОП не конкретно упомянуть многопоточность, я думаю , что это, безусловно , стоит упомянуть , когда статический метод не поточно-, как это необычно и делает коды непригодные для многих ситуаций без изменений. Ваш новый код является поточно-ориентированным, но все же не идеален, так как если вы вызываете его из нескольких потоков «примерно» в одно и то же время, чтобы перетасовать две коллекции одного размера, тасования будут эквивалентны. Как правило, Randomэто боль в использовании, как отмечено в моей статье.
Джон Скит
3

Похоже, хороший алгоритм тасования, если вы не слишком беспокоитесь о производительности. Единственная проблема, на которую я хотел бы обратить внимание, заключается в том, что ее поведение не контролируется, поэтому вам может быть трудно ее протестировать.

Одним из возможных вариантов является наличие начального числа, которое передается в качестве параметра в генератор случайных чисел (или генератор случайных чисел в качестве параметра), так что вы можете иметь больше контроля и тестировать его проще.

Самуэль Каррихо
источник
3

Я нашел ответ Джона Скита вполне удовлетворительным, но робо-сканер моего клиента сообщит о любом случае Randomкак о недостатке безопасности. Так что я обменял это на System.Security.Cryptography.RNGCryptoServiceProvider. В качестве бонуса, это исправляет ту проблему безопасности потока, которая была упомянута. С другой стороны, RNGCryptoServiceProviderбыло измерено в 300 раз медленнее, чем при использовании Random.

Использование:

using (var rng = new RNGCryptoServiceProvider())
{
    var data = new byte[4];
    yourCollection = yourCollection.Shuffle(rng, data);
}

Метод:

/// <summary>
/// Shuffles the elements of a sequence randomly.
/// </summary>
/// <param name="source">A sequence of values to shuffle.</param>
/// <param name="rng">An instance of a random number generator.</param>
/// <param name="data">A placeholder to generate random bytes into.</param>
/// <returns>A sequence whose elements are shuffled randomly.</returns>
public static IEnumerable<T> Shuffle<T>(this IEnumerable<T> source, RNGCryptoServiceProvider rng, byte[] data)
{
    var elements = source.ToArray();
    for (int i = elements.Length - 1; i >= 0; i--)
    {
        rng.GetBytes(data);
        var swapIndex = BitConverter.ToUInt32(data, 0) % (i + 1);
        yield return elements[swapIndex];
        elements[swapIndex] = elements[i];
    }
}
frattaro
источник
3

Ищете алгоритм? Вы можете использовать мой ShuffleListкласс:

class ShuffleList<T> : List<T>
{
    public void Shuffle()
    {
        Random random = new Random();
        for (int count = Count; count > 0; count--)
        {
            int i = random.Next(count);
            Add(this[i]);
            RemoveAt(i);
        }
    }
}

Затем используйте это так:

ShuffleList<int> list = new ShuffleList<int>();
// Add elements to your list.
list.Shuffle();

Как это работает?

Давайте первоначальный отсортированный список из 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

SteeveDroz
источник
Это не O (n). RemoveAt - это O (n).
Папараццо
Хм, похоже ты прав, мой плохой! Я удалю эту часть.
SteeveDroz
1

Этот алгоритм перемешивает, генерируя новое случайное значение для каждого значения в списке, затем упорядочивая список по этим случайным значениям. Думайте об этом как о добавлении нового столбца в таблицу в памяти, затем заполнении его GUID, а затем сортировке по этому столбцу. Похоже, эффективный способ для меня (особенно с лямбда-сахаром!)

Дейв Сверски
источник
1

Немного не связанный, но вот интересный метод (который, хотя он действительно избыточен, ДЕЙСТВИТЕЛЬНО был реализован) для действительно случайного генерирования бросков костей!

Кубик-O-Matic

Причина, по которой я публикую это здесь, заключается в том, что он делает несколько интересных замечаний о том, как его пользователи отреагировали на идею использования алгоритмов для перетасовки поверх реальных костей. Конечно, в реальном мире такое решение предназначено только для действительно экстремальных концов спектра, где случайность оказывает такое большое влияние и, возможно, воздействие влияет на деньги;).

Irfy
источник
1

Я бы сказал, что многие ответы здесь, такие как «Этот алгоритм тасует, генерируя новое случайное значение для каждого значения в списке, а затем упорядочивая список по этим случайным значениям», могут быть очень неправильными!

Я думаю, что это НЕ присваивает случайное значение каждому элементу исходной коллекции. Вместо этого мог бы быть алгоритм сортировки, работающий как Quicksort, который вызвал бы функцию сравнения приблизительно n log n раз. Какой-то алгоритм действительно ожидает, что эта функция сравнения будет стабильной и всегда будет возвращать один и тот же результат!

Не может быть так, что IEnumerableSorter вызывает функцию сравнения для каждого шага алгоритма, например, быстрой сортировки, и каждый раз вызывает функцию x => r.Next()для обоих параметров, не кэшируя их!

В этом случае вы можете действительно испортить алгоритм сортировки и сделать его намного хуже ожиданий, на которых основан алгоритм. Конечно, со временем он станет стабильным и что-то вернет.

Я мог бы проверить это позже, поместив выходные данные отладки в новую функцию «Next», чтобы посмотреть, что произойдет. В Reflector я не смог сразу узнать, как это работает.

Кристиан
источник
1
Это не так: внутреннее переопределение void ComputeKeys (TElement [] elements, int count); Объявление типа: System.Linq.EnumerableSorter <TElement, TKey> Сборка: System.Core, Version = 3.5.0.0 Эта функция сначала создает массив со всеми ключами, которые потребляют память, а затем сортирует их быстрой сортировкой
Christian
Это хорошо знать - все еще только деталь реализации, которая может измениться в будущих версиях!
Blorgbeard выходит
-5

Время запуска для выполнения кода с очисткой всех потоков и кэшированием каждого нового теста,

Первый неудачный код. Он работает на LINQPad. Если вы будете следовать, чтобы проверить этот код.

Stopwatch st = new Stopwatch();
st.Start();
var r = new Random();
List<string[]> list = new List<string[]>();
list.Add(new String[] {"1","X"});
list.Add(new String[] {"2","A"});
list.Add(new String[] {"3","B"});
list.Add(new String[] {"4","C"});
list.Add(new String[] {"5","D"});
list.Add(new String[] {"6","E"});

//list.OrderBy (l => r.Next()).Dump();
list.OrderBy (l => Guid.NewGuid()).Dump();
st.Stop();
Console.WriteLine(st.Elapsed.TotalMilliseconds);

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]

using System;
using System.Runtime;
using System.Diagnostics;
using System.IO;
using System.Collections.Generic;
using System.Collections;
using System.Linq;
using System.Threading;

namespace Algorithm
{
    class Program
    {
        public static void Main(string[] args)
        {
            try {
                int i = 0;
                int limit = 10;
                var result = GetTestRandomSort(limit);
                foreach (var element in result) {
                    Console.WriteLine();
                    Console.WriteLine("time {0}: {1} ms", ++i, element);
                }
            } catch (Exception e) {
                Console.WriteLine(e.Message);
            } finally {
                Console.Write("Press any key to continue . . . ");
                Console.ReadKey(true);
            }
        }

        public static IEnumerable<double> GetTestRandomSort(int limit)
        {
            for (int i = 0; i < 5; i++) {
                string path = null, temp = null;
                Stopwatch st = null;
                StreamReader sr = null;
                int? count = null;
                List<string> list = null;
                Random r = null;

                GC.Collect();
                GC.WaitForPendingFinalizers();
                Thread.Sleep(5000);

                st = Stopwatch.StartNew();
                #region Import Input Data
                path = Environment.CurrentDirectory + "\\data";
                list = new List<string>();
                sr = new StreamReader(path);
                count = 0;
                while (count < limit && (temp = sr.ReadLine()) != null) {
//                  Console.WriteLine(temp);
                    list.Add(temp);
                    count++;
                }
                sr.Close();
                #endregion

//              Console.WriteLine("--------------Random--------------");
//              #region Sort by Random with OrderBy(random.Next())
//              r = new Random();
//              list = list.OrderBy(l => r.Next()).ToList();
//              #endregion

//              #region Sort by Random with OrderBy(Guid)
//              list = list.OrderBy(l => Guid.NewGuid()).ToList();
//              #endregion

//              #region Sort by Random with Parallel and OrderBy(random.Next())
//              r = new Random();
//              list = list.AsParallel().OrderBy(l => r.Next()).ToList();
//              #endregion

//              #region Sort by Random with Parallel OrderBy(Guid)
//              list = list.AsParallel().OrderBy(l => Guid.NewGuid()).ToList();
//              #endregion

//              #region Sort by Random with User-Defined Shuffle Method
//              r = new Random();
//              list = list.Shuffle(r).ToList();
//              #endregion

//              #region Sort by Random with Parallel User-Defined Shuffle Method
//              r = new Random();
//              list = list.AsParallel().Shuffle(r).ToList();
//              #endregion

                // Result
//              
                st.Stop();
                yield return st.Elapsed.TotalMilliseconds;
                foreach (var element in list) {
                Console.WriteLine(element);
            }
            }

        }
    }
}

Описание результата: 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.

Ответ: это противоречие.

GMzo
источник
1
Второй вариант не верен , и поэтому его производительность не имеет значения . Это также все еще не отвечает на вопрос о том, является ли упорядочение по случайному числу приемлемым, эффективным или как оно работает. У первого решения также есть проблемы с корректностью, но они не так важны.
Обслуживание
Извините, я хотел бы знать, что является лучшим параметром Quicksort из Linq OrderBy? Мне нужно проверить производительность. Тем не менее, я думаю, что тип int имеет скорость лучше, чем строка Guid, но это не так. Я понял, почему MSDN рекомендуется. Первое решение, отредактированное по производительности, такое же, как и OrderBy с экземпляром Random.
GMzo
Какой смысл измерять производительность кода, который не решает проблему? Производительность - это всего лишь соображение, которое можно сделать между двумя решениями, которые работают . Когда у вас есть рабочие решения, тогда вы можете начать их сравнивать.
Обслуживание
У меня должно быть время, чтобы проверить больше данных, тогда, если это будет закончено, я обещаю опубликовать снова. Предположим: я думаю, что Linq OrderBy не хуже первого решения. Мнение: это легко использовать и понять.
GMzo
Он заметно менее эффективен, чем простые простые алгоритмы тасования, но, опять же, производительность не имеет значения . Они не надежно перетасовывают данные, а также менее производительны.
Servy