Рандомизировать список <T>

695

Каков наилучший способ рандомизировать порядок общего списка в С#? У меня есть конечный набор из 75 номеров в списке, который я хотел бы присвоить случайному порядку, чтобы нарисовать их для приложения типа лотереи.

  • 1
    Существует открытая проблема для интеграции этой функции в .NET: github.com/dotnet/corefx/issues/461
  • 3
    Возможно, вас заинтересует этот пакет NuGet , который содержит методы расширения для тасования IList <T> и IEnumerable <T> с использованием алгоритма Фишера-Йейтса, упомянутого ниже.
Показать ещё 3 комментария
Теги:
generic-list

19 ответов

938
Лучший ответ

Перетасуйте любой (I)List с помощью метода расширения, основанного на Fisher-Yates shuffle:

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;
      }
    }
  }
}
  • 0
    Ницца! Я люблю методы расширения: D
  • 2
    Обратите внимание, что производительность этого значительно ниже для LinkedLists vs ArrayLists. Всегда обязательно отправляйте ему индексированный массив производительности O (1).
Показать ещё 26 комментариев
244

Если нам нужно только перетасовать элементы в полностью случайном порядке (просто для смешивания элементов в списке), я предпочитаю этот простой, но эффективный код, который заказывает элементы по guid...

var shuffledcards = cards.OrderBy(a => Guid.NewGuid()).ToList();
  • 1
    можешь объяснить что это?
  • 1
    Это использует метод Linq для изменения порядка в списке. NewGuid создает случайный Guid (16-байтовое значение). OrderBy использует предоставленную функцию для назначения сопоставимого объекта каждой карточке, а затем возвращает новую коллекцию, заказанную Руководствами. Поскольку направляющие очень длинные, шансы на столкновение (один и тот же проводник для двух разных объектов) очень малы.
Показать ещё 21 комментарий
91

Я немного удивлен всеми неуклюжими версиями этого простого алгоритма. Fisher-Yates (или Knuth shuffle) немного сложный, но очень компактный. Если вы перейдете в Википедию, вы увидите версию этого алгоритма, которая имеет обратную связь, и многие люди действительно не понимают, почему это происходит наоборот. Основная причина заключается в том, что эта версия алгоритма предполагает, что генератор случайных чисел Random(n) в вашем распоряжении имеет следующие два свойства:

  • Он принимает n как один входной параметр.
  • Он возвращает число от 0 до n включительно.

Однако генератор случайных чисел .Net не удовлетворяет свойству # 2. Random.Next(n) вместо этого возвращает число от 0 до n-1 включительно. Если вы попытаетесь использовать for-loop в обратном порядке, вам нужно будет вызвать Random.Next(n+1), который добавит еще одну операцию.

Однако генератор случайных чисел .Net имеет еще одну приятную функцию Random.Next(a,b), которая возвращает a в b-1 включительно. Это действительно прекрасно сочетается с реализацией этого алгоритма, который имеет нормальный цикл for. Итак, без дальнейших церемоний, здесь правильная, эффективная и компактная реализация:

public static void Shuffle<T>(this IList<T> list, Random rnd)
{
    for(var i=0; i < list.Count; i++)
        list.Swap(i, rnd.Next(i, list.Count));
}

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
    Не лучше ли изменить rnd (i, list.Count) на rnd (0, list.Count), чтобы можно было поменять любую карту?
  • 10
    @ Донатс - нет. Если вы сделаете это, вы добавите смещение в случайном порядке.
Показать ещё 3 комментария
67

Метод расширения для IEnumerable:

public static IEnumerable<T> Randomize<T>(this IEnumerable<T> source)
{
    Random rnd = new Random();
    return source.OrderBy<T, int>((item) => rnd.Next());
}
  • 3
    Обратите внимание, что это не потокобезопасно, даже если используется в поточно-безопасном списке
  • 1
    как мы передаем список <string> этой функции?
Показать ещё 3 комментария
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;
    }
  • 1
    См. Stackoverflow.com/questions/4412405/…
  • 4
    var listCopy = list.ToList() вы не должны делать что-то вроде var listCopy = list.ToList() чтобы избежать выталкивания всех элементов из входящего списка? Я действительно не понимаю, почему вы хотите изменить эти списки, чтобы очистить.
8

ИЗМЕНИТЬ 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 не является потокобезопасной или криптографически достаточно сильной для ваших нужд, вы можете ввести свою реализацию в операцию.

В этом ответе можно найти подходящую реализацию для криптографически сильной реализации 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]];
}

  • 0
    См. Stackoverflow.com/questions/4412405/… . Вы должны уже знать.
  • 0
    @nawfal посмотрите мою улучшенную реализацию.
Показать ещё 1 комментарий
6

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

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);
}
  • 2
    Я бы Random экземпляр класса Random вне функции как static переменную. В противном случае вы можете получить такое же начальное значение рандомизации из таймера, если вызывается в быстрой последовательности.
  • 0
    Интересное замечание - если вы быстро создаете экземпляр класса Random в цикле, скажем, от 0 до 200 мс друг от друга, то у вас очень высока вероятность получения того же начального числа рандомизации, что затем приводит к повторяющимся результатам. Однако вы можете обойти это, используя Random rand = new Random (Guid.NewGuid (). GetHashCode ()); Это эффективно заставляет рандомизацию быть выведенной из Guid.NewGuid ()
4

Обычно я использую:

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);
}
  • 0
    list.RemoveAt - это операция O (n), которая делает эту реализацию слишком медленной.
3

Это мой предпочтительный метод перетасовки, когда желательно не изменять оригинал. Это вариант алгоритма Fisher-Yates "наизнанку" , который работает с любой перечислимой последовательностью (длина 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)];

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

3

Если у вас есть фиксированное число (75), вы можете создать массив из 75 элементов, а затем перечислить список, перемещая элементы в рандомизированные позиции в массиве. Вы можете сгенерировать отображение номера списка в индекс массива с помощью Fisher-Yates shuffle.

2

Если вы не против использования двух 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);
1

Идея получает анонимный объект с элементом и случайным порядком, а затем переупорядочивает элементы по этому порядку и возвращаемому значению:

var result = items.Select(x => new { value = x, order = rnd.Next() })
            .OrderBy(x => x.order).Select(x => x.value).ToList()
  • 0
    лучшее решение с одним вкладышем
1
 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);

}
  • 1
    Добро пожаловать в стек переполнения! Пожалуйста, подумайте над тем, чтобы добавить в ваш ответ какое-то объяснение, а не просто огромный блок кода. Наша цель - научить людей понимать ответ и применять его в других ситуациях. Если вы прокомментируете свой код и добавите объяснение, вы сделаете свой ответ более полезным не только для человека, который задал вопрос на этот раз, но и для любого человека в будущем, у которого может быть такая же проблема.
  • 3
    Большая часть этого кода совершенно не имеет отношения к вопросу, и единственная полезная часть в основном повторяет ответ Адама Тегена почти 6 лет назад.
1

Здесь эффективный Shuffler, который возвращает массив байтов перетасованных значений. Он никогда не перемешивает больше, чем нужно. Он может быть перезапущен с того места, где он ранее был остановлен. Моя фактическая реализация (не показана) - это MEF-компонент, который позволяет пользователю задавать замену shuffler.

    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

Улучшенная (IMHO) версия алгоритма гранатового ответа, аннотированная для понимания изменений с первого взгляда:

public static class IListExtensions
{
    //Random is perfectly fine if used correctly.
    [NotNull]
    private static readonly Random Random;

    //Delay instantiation to when it actually needed, if it ever is.
    static IListExtensions()
    {
        Random = new Random();
    }

    /// <summary>Shuffles the specified list.</summary>
    /// <typeparam name="T">The type of the elements in the list.</typeparam>
    /// <param name="list">The list.</param>
    /// <exception cref="ArgumentNullException">list - Is null.</exception>
    public static void Shuffle<T>([NotNull] this IList<T> list)
    {
        //An extension method is a static method with syntax sugar sprinkled on top...
        //...A null check is still necessary, which most answers here forget to do.
        if (list == null) throw new ArgumentNullException("list", "Is null.");
        //Using a FOR-LOOP instead of a WHILE-LOOP avoids an unnecessary variable.
        for (var i = list.Count; i > 1; i--)
        {
            var r = Random.Next(i--); //Using 'i--' here avoids an unnecessary line/statement.
            //Swapping with a separate method has no advantage and just introduces overhead.
            var t = list[r];
            list[r] = list[i];
            list[i] = t;
        }
    }
}
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;
}
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());
    }
}
-1

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

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

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

  • 0
    Компактно, но есть ли у вас рекомендации по сортировке последовательных новых гидов, чтобы они были случайными? Некоторые версии quid / uuid имеют метки времени и другие неслучайные части.
  • 7
    Этот ответ уже дан, и, что еще хуже, он предназначен для уникальности, а не случайности.
-5

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

В псевдокоде это будет выглядеть так:

do 
    r1 = randomPositionInList()
    r2 = randomPositionInList()
    swap elements at index r1 and index r2 
for a certain number of times
  • 1
    Одной из проблем этого подхода является знание того, когда остановиться. Он также имеет тенденцию преувеличивать любые смещения в генераторе псевдослучайных чисел.
  • 3
    Да. Весьма неэффективно. Нет смысла использовать такой подход, когда существуют лучшие, более быстрые подходы, которые столь же просты.
Показать ещё 1 комментарий

Ещё вопросы

Сообщество Overcoder
Наверх
Меню