Avatar ·

Как перемешать список List<T> случайным образом в C#?

Всем привет! Я новичок в C# и пишу небольшое приложение для карточной игры. У меня есть список (List), который содержит колоду карт. Мне нужно перемешать эти карты случайным образом перед раздачей, но я не могу найти встроенный метод вроде List.Shuffle(). Вот мой код: ```csharp List deck = new List(); // ... заполнение колоды картами ... // Как теперь перемешать? Я пробовал: deck.OrderBy(x => random.Next()).ToList(); // Но это возвращает новый список, а не меняет исходный ``` Проблема в том, что я хочу изменить исходный список deck, а не создавать новый. Кроме того, я читал, что использование Random с OrderBy может давать неравномерное распределение или даже исключения, если random.Next() вернет одинаковые значения для нескольких элементов. Подскажите, пожалуйста, как правильно и эффективно перемешать List на месте? Желательно с примером кода. Заранее спасибо! <<>>

Лучший способ: алгоритм Фишера-Йетса (Knuth shuffle)

Самый правильный и эффективный способ перемешать список на месте — это реализовать алгоритм Фишера-Йетса (также известный как тасование Кнута). Он работает за O(n) времени и O(1) дополнительной памяти, давая равномерное распределение.

public static void Shuffle<T>(this IList<T> list)
{
    Random rng = new Random();
    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<Card> deck = new List<Card>();
// ... заполнение колоды ...
deck.Shuffle(); // перемешивает исходный список

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

  • Мы идем с конца списка к началу.
  • Для каждого элемента с индексом n выбираем случайный индекс k от 0 до n.
  • Меняем местами элементы на позициях k и n.
  • Таким образом, каждый элемент с одинаковой вероятностью оказывается на любой позиции.

Важные замечания

  • Проблема с Random: В примере выше Random создается внутри метода. В высоконагруженных приложениях это может привести к повторяющимся последовательностям (если метод вызывается очень быстро). Для production-кода лучше использовать один статический экземпляр Random с блокировкой (lock) или использовать System.Security.Cryptography.RandomNumberGenerator.
  • Почему не OrderBy? Ваш вариант с OrderBy(x => random.Next()) работает, но:
    • Создаёт новый список, а не изменяет исходный.
    • Не гарантирует равномерного распределения (особенно если random.Next() даст одинаковые значения).
    • Работает за O(n log n), что медленнее.
  • Для массивов: Если у вас массив T[], можно использовать встроенный метод Random.Shared.Shuffle(array) (начиная с .NET 8). Для списков такого метода пока нет.

Более безопасная версия с криптографическим ГПСЧ

public static void ShuffleSecure<T>(this IList<T> list)
{
    using (var rng = System.Security.Cryptography.RandomNumberGenerator.Create())
    {
        byte[] bytes = new byte[4];
        int n = list.Count;
        while (n > 1)
        {
            n--;
            rng.GetBytes(bytes);
            int k = BitConverter.ToInt32(bytes, 0) & 0x7FFFFFFF; // делаем неотрицательным
            k = k % (n + 1);
            T value = list[k];
            list[k] = list[n];
            list[n] = value;
        }
    }
}

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

<<>>

Альтернативный подход: использование OrderBy с фиксацией ключей

Если вам не нужно изменять исходный список, а достаточно получить новый перемешанный, можно использовать OrderBy с правильным подходом:

Random rng = new Random();
List<Card> shuffledDeck = deck.OrderBy(x => rng.Next()).ToList();

Но есть важный нюанс: rng.Next() может вернуть одинаковое значение для нескольких элементов, что нарушит случайность. Лучше использовать Guid:

List<Card> shuffledDeck = deck.OrderBy(x => Guid.NewGuid()).ToList();

Хотя этот подход не самый производительный (O(n log n)), он очень простой и подходит для небольших списков (до нескольких тысяч элементов). Он не изменяет исходный список, а возвращает новый.

Когда использовать: для прототипирования, небольших проектов или когда исходная коллекция должна остаться неизменной. Для production-кода или больших списков предпочтительнее алгоритм Фишера-Йетса из первого ответа.