Чи використовує Random та OrderBy хороший алгоритм перетасовки?


164

Я читав статтю про різні алгоритми перетасовки у програмі Coding Horror . Я бачив, що десь люди робили це, щоб перетасувати список:

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

Це хороший алгоритм переміщення? Як це працює саме? Це прийнятний спосіб зробити це?

Відповіді:


205

Мені подобається не такий спосіб перемішування, в основному на тій підставі, що це O (n log n) без поважних причин, коли легко здійснити O (n) переміщення. Код у питанні "працює", в основному даючи випадковий (сподіваюсь, унікальний!) Номер кожному елементу, а потім упорядковує елементи відповідно до цього числа.

Я вважаю за краще варіант Дурстенфілда від перемикання Фішера-Йейта, який міняє елементи.

Реалізація простого Shuffleметоду розширення в основному складається з виклику ToListабо ToArrayна вході, а потім з використанням існуючої реалізації Fisher-Yates. (Введіть параметр Randomяк параметр, щоб зробити життя загалом кращим.) Навколо існує маса реалізацій ... Я, мабуть, десь отримав відповідь.

Приємна річ у такому методі розширення полягає в тому, що тоді читачеві буде дуже зрозуміло, що ви насправді намагаєтесь зробити.

EDIT: Ось проста реалізація (перевірки помилок немає!):

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;
    }
}

EDIT: Коментарі до ефективності нижче нагадували мені, що ми можемо фактично повертати елементи під час переміщення їх:

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 тато

2
Розумний! (І я ненавиджу цей матеріал із 15 символів ...)
Свиш

@P Тато: А? Хочете допрацювати?
Свиш

1
Або ви можете замінити> 0 на> = 0 і не потрібно (хоча, додаткове потрапляння RNG плюс
зайве

4
Вартість запуску - O (N) як вартість джерела.ToArray ();
Дейв Хілліє

8

Починаючи з цієї цитати Скіта:

Мені подобається не такий спосіб перемішування, в основному на тій підставі, що це O (n log n) без поважних причин, коли легко здійснити O (n) переміщення. Код у питанні "працює", в основному даючи випадковий ( сподіваюсь, унікальний! ) Номер кожному елементу, а потім упорядковує елементи відповідно до цього числа.

Я продовжу трохи пояснюю причину, надіюсь, унікальної!

Тепер, від Inumerable.OrderBy :

Цей метод виконує стабільний сорт; тобто, якщо ключі двох елементів рівні, порядок елементів зберігається

Це дуже важливо! Що станеться, якщо два елементи "отримують" однакове випадкове число? Буває, що вони залишаються в тому ж порядку, в якому вони є в масиві. Тепер, яка можливість цього статися? Важко точно обчислити, але є проблема Дня народження, яка саме ця проблема.

Тепер це реально? Це правда?

Як завжди, коли сумніваєтесь, напишіть кілька рядків програми: http://pastebin.com/5CDnUxPG

Цей невеликий блок коду перетасовує масив з 3-х елементів певну кількість разів, використовуючи алгоритм Fisher-Yates, зроблений назад, алгоритм Fisher-Yates зроблений вперед (на сторінці вікі є два алгоритми псевдо-коду ... Вони виробляють еквівалент результати, але один робиться від першого до останнього елемента, а інший - від останнього до першого елемента), наївний неправильний алгоритм 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 (числових значень)), так що якщо максимальне значення дорівнює 2 ^ 31, я скажу, що ви повинні мати можливість сортувати масиви до 2 ^ 12/2 ^ 13 біт (4096-8192 елементів)


Добре викладено і прекрасно відображає проблему з оригінальним запитанням. Це слід злити з відповіддю Йона.
TheSoftwareJedi

6

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

Він працює, присвоївши кожному елементу ряду випадкове ціле число, а потім упорядкувавши послідовність за цими цілими числами.

Це цілком прийнятно для 99,9% застосунків (якщо тільки вам абсолютно не потрібно обробляти крайню справу вище). Крім того, заперечення скелета щодо його виконання є дійсним, тому, якщо ви перетасуєте довгий список, ви, можливо, не захочете його використовувати.


4

Це вже було багато разів. Шукайте Fisher-Yates на 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;
                }
        }
}

2
Ви не повинні використовувати Randomяк статичну змінну, як ця - Randomне є безпечною для потоків. Дивіться csharpindepth.com/Articles/Chapter12/Random.aspx
Джон Скіт

@Jon Skeet: впевнений, це законний аргумент. ОТО, ОП запитувала про алгоритм, який не було виправданим, тоді як це правильно (крім випадків використання багатопотокової перетасування карт).
hughdbrown

1
Це просто означає, що це "менш помилково", ніж підхід ОП. Це не означає, що це код, який слід використовувати, не розуміючи, що його не можна безпечно використовувати в багатопотоковому контексті ... це те, що ви не згадували. Є обгрунтоване сподівання, що статичні елементи можна безпечно використовувати з декількох потоків.
Джон Скіт

@Jon Skeet: Звичайно, я можу це змінити. Зроблено. Я схильний вважати, що повертаючись до запитання, відповів три з половиною роки тому і кажу: "Це неправильно, оскільки він не обробляє багатопотокове використання", коли ОП ніколи не запитувала про щось більше, ніж алгоритм є надмірним. Перегляньте мої відповіді протягом багатьох років. Часто я давав відповіді ОП, які виходили за рамки заявлених вимог. Мене за це критикують. Я б не очікував, що ОП отримають відповіді, які відповідають усім можливим напрямкам.
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];
    }
}

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


Це не O (n). Один лише RemoveA є O (n).
папараццо

Гм, здається, ти маєш рацію, моя погана! Я зніму цю частину.
SteeveDroz

1

Цей алгоритм переміщується, генеруючи нове випадкове значення для кожного значення у списку, а потім упорядковуючи список за тими випадковими значеннями. Подумайте про це як додавання нового стовпчика до таблиці пам'яті, а потім заповнення його GUID, а потім сортування за цим стовпцем. Мені це здається ефективним способом (особливо з лямбда-цукром!)


1

Трохи не пов’язаний між собою, але ось цікавий метод (який, хоч він і справді є надмірним, реально реалізований) для справді випадкового генерування рулонів кісток!

Dice-O-Matic

Причина, яку я публікую тут, полягає в тому, що він робить цікаві моменти щодо того, як його користувачі відреагували на ідею використання алгоритмів для переміщення над фактичними костями. Звичайно, в реальному світі таке рішення є лише для справді крайніх кінців спектру, де випадковість має такий великий вплив і, можливо, вплив впливає на гроші;).


1

Я б сказав, що багато відповідей на кшталт "Цей алгоритм переміщується, генеруючи нове випадкове значення для кожного значення в списку, а потім упорядкувати список за тими випадковими значеннями" може бути дуже помилковим!

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

Чи не може бути те, що IEnumerableSorter викликає функцію порівняння для кожного кроку алгоритму, наприклад, quicksort і кожен раз викликає функцію x => r.Next()для обох параметрів, не кешуючи їх!

У такому випадку ви можете дійсно зіпсувати алгоритм сортування і зробити його набагато гіршим, ніж очікування, на яких алгоритм будується. Звичайно, вона з часом стане стабільною і щось поверне.

Я можу перевірити це пізніше, додавши налагоджувальний вихід у нову функцію "Далі", щоб побачити, що відбувається. У Reflector я не міг одразу дізнатися, як це працює.


1
Це не так: внутрішнє переосмислення недійсних ComputeKeys (елементи TElement [], кількість int); Тип оголошення: System.Linq.EnumerableSorter <TElement, TKey> Assembly: System.Core, Version = 3.5.0.0 Ця функція створює спочатку масив із усіма ключами, які споживає пам'ять, перш ніж quicksort сортує їх
Крістіан

Це добре знати - все ж лише детальна інформація про впровадження, яка, можливо, може змінитися в наступних версіях!
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.)

після другого разу вони обидва використовують в один і той же час.

EDIT: TEST CODE на Intel Core i7 4@2.1GHz, ОЗУ 8 ГБ DDR3 @ 1600, HDD 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 ()) не гірші за визначений користувачем метод перетасовки в першому рішенні.

Відповідь: Вони суперечать.


1
Другий варіант невірний , тому його продуктивність не має значення . Це також досі не відповідає на питання про те, чи прийнятне, ефективне замовлення за випадковим числом чи як воно працює. Перше рішення також має проблеми з коректністю, але вони не так вже й великі.
Сервіс

Вибачте, я хотів би знати, що краще тип параметра Quicksort з Linq OrderBy? Мені потрібно перевірити продуктивність. Однак я думаю, що тип int просто має швидкість кращу, ніж рядок Guid, але це не так. Я зрозумів, чому рекомендує MSDN. Перше відредаговане рішення виконане так само, як OrderBy з екземпляром Random.
GMzo

Який сенс вимірювати продуктивність коду, який не вирішує проблему? Продуктивність - це лише врахування між двома рішеннями, які працюють обома . Якщо у вас є працююче рішення, то ви можете почати їх порівнювати.
Сервіс

У мене повинен бути час на тестування на більшій кількості даних, тоді, якщо це закінчено, я обіцяю ще раз. Припустимо: я думаю, що Linq OrderBy - це не гірше першого рішення. Думка: Це легко у використанні та розумінні.
GMzo

Це помітно менш ефективно, ніж дуже прості алгоритми прямого перетасування, але, знову ж таки, продуктивність не має значення . Вони не надійно переміщують дані, крім того, що вони менш ефективні.
Сервіс
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.