Випадково ранжируйте список <T>


852

Який найкращий спосіб рандомізувати порядок загального списку в C #? У мене в списку є обмежений набір 75 номерів, якому я хотів би призначити випадкове замовлення, щоб намалювати їх для заявки на тип лотереї.


3
Існує відкрите питання щодо інтеграції цієї функціональності до .NET: github.com/dotnet/corefx/isissue/461
Натан

5
Вас може зацікавити цей пакет NuGet , який містить методи розширення для переміщення IList <T> і IEnumerable <T> за допомогою алгоритму Fisher-Yates, згаданого нижче
ChaseMedallion

3
@Natan вони закрили це питання, тому що хтось "працював над багатьма проектами і розробив багато бібліотек і ніколи не мав потреби в такому методі", який мене розлютив. Тепер нам належить досліджувати себе, шукати найкращі втілення, витрачати час на просто винахід колеса.
Віталій Ісаєнко

1
Я бачу це правильно? Не було єдиної дійсної функціональної відповіді через 10 років? Можливо, нам потрібна ще одна винагорода за рішення, яке вирішує кількість необхідної ентропії, щоб перетасувати список із 75 цифрами $ log2 (75!) = 364 $ і як ми можемо це отримати. Потрібно було б перезавантажити навіть криптографічно захищений RNG з 256 бітами ентропії хоча б один раз під час переміщення рибалок.
Falco

1
І якщо звичайний кодер не може вирішити цю проблему, чи всі ми завжди граємо ті ж 0,01% можливих пасьянсів?
Falco

Відповіді:


1135

Перемішайте будь-який (I)Listіз методом розширення на основі перемішування 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;  
    }  
}

Використання:

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

31
Що робити, якщо list.Count -> Byte.MaxValue? Якщо n = 1000, то 255/1000 = 0, то цикл do буде нескінченним циклом, оскільки поле [0] <0 завжди помилкове.
AndrewS

18
Я хотів би зазначити, що порівняння є хибним. Використання <code> new Random () </code> у циклі - це проблема, а не випадковість <code> Random </code> Пояснення
Sven,

9
Добра ідея передавати екземпляр Random методу Shuffle, а не створювати його всередині, як ніби ви викликаєте Shuffle багато разів швидкою послідовністю (наприклад, перетасовування безлічі коротких списків), всі списки змішуватимуться в одному і тому ж. спосіб (наприклад, перший елемент завжди переміщується в положення 3).
Марк Хіт

7
Просто зробити б вирішити цю проблему в пості порівняння. Оскільки кожен наступний дзвінок випливає з попереднього випадкового результату попередніх дзвінків. Random rng = new Random();static
weston

5
# 2, незрозуміло, що версія з генератором Crypto працює, оскільки максимальний діапазон байтів становить 255, тому будь-який список, більший за цей, не буде перетасуватись правильно.
Марк Совул

336

Якщо нам потрібно лише переміщувати елементи в абсолютно випадковому порядку (просто для змішування елементів у списку), я віддаю перевагу цьому простому, але ефективного коду, який замовляє елементи за допомогою інструкцій ...

var shuffledcards = cards.OrderBy(a => Guid.NewGuid()).ToList();

40
GUID повинні бути унікальними, а не випадковими. Частина його є машинною, а інша - часовою, а лише невелика частина - випадковою. blogs.msdn.com/b/oldnewthing/archive/2008/06/27/8659071.aspx
Деспертар

99
Це приємне елегантне рішення. Якщо ви хочете щось інше, ніж орієнтир для створення випадковості, просто замовляйте щось інше. Напр .: var shuffledcards = cards.OrderBy(a => rng.Next()); compilr.com/grenade/sandbox/Program.cs
граната

20
Будь ласка, ні. Це неправильно. "замовлення випадковим чином" - це зовсім НЕ перетасування: ви вводите упередження, і, що ще гірше, ви ризикуєте піти в нескінченні петлі
Vito De Tullio

77
@VitoDeTullio: Ви неправильно пам’ятаєте. Ви ризикуєте нескінченними циклами, коли надаєте функцію випадкового порівняння ; функція порівняння необхідна для створення послідовного загального порядку . Випадковий ключ - це добре. Ця пропозиція є помилковою, оскільки не гарантується, що вказівники є випадковими , а не тому, що техніка сортування за випадковим ключем неправильна.
Ерік Ліпперт

24
@Doug: NewGuidгарантує лише те, що він дає вам унікальний GUID. Це не дає гарантій щодо випадковості. Якщо ви використовуєте GUID для цілей, відмінних від створення унікального значення, ви робите це неправильно.
Ерік Ліпперт

120

Мене трохи здивують усі незграбні версії цього простого алгоритму. Фішер-Йейтс (або Кнут-шуфф) трохи складний, але дуже компактний. Чому це хитро? Тому що вам потрібно звернути увагу на те, чи 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;
}

Спробуйте цей код .


Чи не було б краще змінити rnd (i, list.Count) на rnd (0, list.Count), щоб будь-яку карту можна було поміняти?
Пончики

15
@Donuts - ні. Якщо ви це зробите, ви додасте упередженість у переміщення.
Shital Shah

2
Виділяючи Swap <T> на окремий метод, схоже, ви викликаєте багато непотрібних виділень T для temp.
Глина

2
Я б стверджував, що LINQ може потенційно уповільнити продуктивність переміщення вниз, і це було б причиною не використовувати його, особливо зважаючи на відносну простоту коду.
winglerw28

7
Коли i = list.Count - 1, тобто остання ітерація, rnd.Next(i, list.Count)поверне вас я назад. Тому вам потрібна i < list.Count -1умова циклу. Ну, ви не «потреба» це, але це економить 1 ітерація;)
стручок

78

Метод розширення для IE Численні:

public static IEnumerable<T> Randomize<T>(this IEnumerable<T> source)
{
    Random rnd = new Random();
    return source.OrderBy<T, int>((item) => rnd.Next());
}

3
Зверніть увагу , що це не поточно-, навіть якщо вони використовуються на поточно-список
BlueRaja - Денні Pflughoeft

1
як ми надаємо список <string> цій функції?
MonsterMMORPG

8
У цього алгоритму є дві значні проблеми: - OrderByвикористовує варіант QuickSort для сортування елементів за їх (нібито випадковими) клавішами. Продуктивність QuickSort становить O (N log N) ; На відміну від цього, переміщення Фішера-Йейта є O (N) . Для колекції з 75 елементів це може не бути великою справою, але різниця стане яскраво вираженою для великих колекцій.
Джон Бейер

10
... - Random.Next()може спричинити розумно псевдовипадковий розподіл значень, але це не гарантує, що значення будуть унікальними. Ймовірність повторюваних ключів зростає (нелінійно) з N, поки не досягне визначеності, коли N досягне 2 ^ 32 + 1. OrderByQuickSort є стабільним родом; таким чином, якщо трапляється присвоєння декількох елементів однакового псевдовипадкового значення індексу, їх порядок у вихідній послідовності буде таким самим, як у послідовності введення; таким чином, упередження вводиться в "перетасування".
Джон Бейер

27
@JohnBeyer: Є набагато більші проблеми, ніж це джерело упередженості. У Random є лише чотири мільярди можливих насінин, що набагато менше, ніж кількість можливих перетасовок набору середнього розміру. Лише крихітна частка можливих перебоїв може генеруватися. Це упередження карликове зміщення через випадкові зіткнення.
Ерік Ліпперт

14

Ідея - отримати анонімний об’єкт з предметом і випадковим порядком, а потім переупорядкувати елементи за цим порядком і повернути значення:

var result = items.Select(x => new { value = x, order = rnd.Next() })
            .OrderBy(x => x.order).Select(x => x.value).ToList()

2
найкраще одне
лайнерне

1
Ви пропускаєте крапку з комою на кінці
фію

Якщо хтось не знає про rnd, додайте це до коду вище Random rnd = new Random ();
Грег Тревелік

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


4
Хіба ви не повинні зробити щось на кшталт, var listCopy = list.ToList()щоб уникнути спливання всіх елементів із списку вхідних? Я не розумію, чому ви хочете вимкнути ці списки на порожні.
Кріс Марісіч

9

EDITRemoveAt слабкість в моїй попередній версії. Це рішення долає це.

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

Відповідна реалізація для безпечної для потоків криптографічно сильної 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]];
}


Дивіться stackoverflow.com/questions/4412405/… . ви повинні вже знати.
nawfal

@nawfal див. мою покращену реалізацію.
Джодрелл

1
хм досить справедливо Це GetNextчи Next?
nawfal

4

Ви можете досягти цього, використовуючи цей простий метод розширення

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

3
Я б утримував Randomекземпляр класу поза функцією як staticзмінної. В іншому випадку ви можете отримати одне насіння рандомізації з таймера, якщо викликати його швидкою послідовністю.
Лимонний насіння

Цікава примітка - якщо ви інтенсифікуєте клас Random швидко в циклі, скажімо, від 0 мс до 200 мс для іншого, то у вас є дуже високий шанс отримати те саме насіння рандомізації - що призводить до повторення результатів. Однак можна обійти це за допомогою Random rand = new Random (Guid.NewGuid (). GetHashCode ()); Це ефективно змушує рандомізацію виводити з Guid.NewGuid ()
Baaleos

4

Це мій кращий метод переміщення, коли бажано не змінювати оригінал. Це варіант алгоритму Фішера-Йейта "всередину", який працює на будь-якій численній послідовності (довжина 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);

Сенс генерації випадкового подвійного (виключно від 0 до 1) полягає у використанні масштабу до цілого рішення. Якщо вам потрібно щось вибрати зі списку на основі випадкового подвійного, xякий завжди буде, 0 <= x && x < 1- це прямо вперед.

return list[(int)(x * list.Count)];

Насолоджуйтесь!


4

Якщо ви не проти використовувати два 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);

3

Якщо у вас є фіксоване число (75), ви можете створити масив із 75 елементами, а потім перерахувати свій список, перемістивши елементи до рандомізованих позицій у масиві. Ви можете створити відображення номера списку для індексу масиву за допомогою перемикання Fisher-Yates .


3

Я зазвичай використовую:

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

list.RemoveAt - це операція O (n), яка робить цю реалізацію надзвичайно повільною.
Георгій Полевой

1
    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.

0

Ось ефективний 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;
    }

`


0

Ось безпечний для цього спосіб:

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

0
 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);

}

2
Ласкаво просимо до переповнення стека! Будь ласка, подумайте, щоб додати відповідь у відповідь, а не просто величезний блок коду. Наша мета тут - навчити людей так, щоб вони розуміли відповідь і могли застосовувати її в інших ситуаціях. Якщо ви прокоментуєте свій код і додасте пояснення, ви зробите свою відповідь кориснішою не лише людині, яка цього разу поставила це питання, але й будь-кому в майбутньому, у кого може виникнути така ж проблема.
starsplusplus

4
Більшість цього кодексу абсолютно не стосується питання, і єдина корисна частина, в основному, повторює відповідь Адама Тегена майже 6 років тому.
ТК

0

Проста модифікація прийнятої відповіді, яка повертає новий список замість того, щоб працювати на місці, і приймає більш загальний, 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;
}


-5

Старий пост напевно, але я просто використовую GUID.

Items = Items.OrderBy(o => Guid.NewGuid().ToString()).ToList();

GUID завжди унікальний, і оскільки він регенерується щоразу, коли результат змінюється кожного разу.


Компактно, але чи є у вас посилання на сортування послідовних новихGuids, щоб вони були високоякісними випадковими? Деякі версії quid / uuid мають позначки часу та інші випадкові частини.
Йохан Лундберг

8
Ця відповідь вже дана, і що гірше, вона розрахована на унікальність, а не випадковість.
Алекс Ангас

-7

Дуже простий підхід до подібної проблеми полягає у використанні в списку декількох випадкових елементів заміни.

У псевдо-коді це виглядатиме так:

do 
    r1 = randomPositionInList()
    r2 = randomPositionInList()
    swap elements at index r1 and index r2 
for a certain number of times

1
Одна з проблем такого підходу - це знати, коли зупинитись. Він також має тенденцію до перебільшення будь-яких упереджень у генераторі псевдовипадкових чисел.
Марк Бессі

3
Так. Високоефективний. Немає підстав використовувати такий підхід, коли існують кращі, більш швидкі підходи, які є такими ж простими.
PeterAllenWebb

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