Как перемешать список List<T> случайным образом в C#?
Лучший способ: алгоритм Фишера-Йетса (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-кода или больших списков предпочтительнее алгоритм Фишера-Йетса из первого ответа.