Aleatorizar uma lista <T>

855

Qual é a melhor maneira de randomizar a ordem de uma lista genérica em C #? Eu tenho um conjunto finito de 75 números em uma lista à qual gostaria de atribuir uma ordem aleatória, para desenhá-los para um aplicativo do tipo loteria.

mirezus
fonte
3
Há um problema em aberto para integrar essa funcionalidade ao .NET: github.com/dotnet/corefx/issues/461
Natan
5
Você pode estar interessado em este pacote NuGet , que contém métodos de extensão para baralhar IList <T> e IEnumerable <T> usando o algoritmo de Fisher-Yates mencionados abaixo
ChaseMedallion
3
@ Natan, eles fecharam a questão porque alguém "trabalhou em muitos projetos e desenvolveu muitas bibliotecas e nunca teve necessidade de tal método" que me irritou. Agora temos que nos investigar, procurar as melhores implementações, perder tempo para simplesmente reinventar a roda.
Vitalii Isaenko 16/07/19
1
Estou vendo isso certo? Nenhuma resposta funcional válida é válida após 10 anos? Talvez precisemos de outra recompensa para uma solução que lide com a quantidade de entropia necessária, para embaralhar uma lista com 75 números $ log2 (75!) = 364 $ e como podemos obter isso. Seria necessário replanejar até mesmo um RNG criptograficamente seguro com 256 bits de entropia pelo menos uma vez durante um embaralhamento de pescadores.
Falco
1
E se o codificador comum não puder resolver esse problema, todos nós jogamos o mesmo 0,01% dos possíveis jogos de paciência para sempre?
Falco

Respostas:

1137

Shuffle qualquer (I)Listcom um método de extensão baseado no shuffle de 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;  
    }  
}

Uso:

List<Product> products = GetProducts();
products.Shuffle();

O código acima usa o método System.Random, muito criticado, para selecionar candidatos à troca. É rápido, mas não tão aleatório quanto deveria ser. Se você precisar de uma melhor qualidade de aleatoriedade em seus shuffles, use o gerador de números aleatórios em System.Security.Cryptography da seguinte forma:

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

Uma comparação simples está disponível neste blog (WayBack Machine).

Edit: Desde que escrevi esta resposta há alguns anos, muitas pessoas comentaram ou escreveram para mim, para apontar a grande falha boba da minha comparação. Claro que eles estão certos. Não há nada de errado com o System.Random se ele for usado da maneira que foi planejado. No meu primeiro exemplo acima, instanciamos a variável rng dentro do método Shuffle, que está solicitando problemas se o método for chamado repetidamente. Abaixo está um exemplo fixo e completo, com base em um comentário realmente útil recebido hoje de @weston aqui no 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;
      }
    }
  }
}
Grenade
fonte
32
E se list.Count for> Byte.MaxValue? Se n = 1000, 255/1000 = 0, então o loop do será um loop infinito, pois a caixa [0] <0 é sempre falsa.
precisa saber é o seguinte
18
Gostaria de salientar que a comparação é falha. Usar <code> new Random () </code> em um loop é o problema, não a aleatoriedade de <code> Random </code> Explicação
Sven
9
É uma boa ideia passar uma instância do método Random para o Shuffle, em vez de criá-lo como se você estivesse chamando o Shuffle várias vezes em rápida sucessão (por exemplo, embaralhando muitas listas curtas), as listas serão embaralhadas da mesma maneira maneira (por exemplo, o primeiro item sempre é movido para a posição 3).
Mark Heath
7
Apenas fazer Random rng = new Random();um staticresolveria o problema no post de comparação. Como cada chamada subsequente seguiria as anteriores, o último resultado aleatório.
28512 weston
5
# 2, não está claro que a versão com o gerador Crypto funcione porque o intervalo máximo de um byte é 255, portanto, qualquer lista maior que isso não será embaralhada corretamente.
Mark Sowul
336

Se precisarmos embaralhar itens em uma ordem completamente aleatória (apenas para misturar os itens de uma lista), prefiro esse código simples, porém eficaz, que ordena itens por guia ...

var shuffledcards = cards.OrderBy(a => Guid.NewGuid()).ToList();
user453230
fonte
40
Os GUIDs devem ser exclusivos e não aleatórios. Parte dela é baseada em máquina e outra parte em tempo parcial e apenas uma pequena parte é aleatória. blogs.msdn.com/b/oldnewthing/archive/2008/06/27/8659071.aspx
Despertar
99
Esta é uma solução elegante e agradável. Se você quiser algo diferente de um guid para gerar aleatoriedade, faça o pedido por outra coisa. Por exemplo: var shuffledcards = cards.OrderBy(a => rng.Next()); compilr.com/grenade/sandbox/Program.cs
grenade
20
Por favor não. Isto está errado. "ordenação por acaso" não é totalmente aleatoriamente: você introduzir um viés e, pior, você corre o risco de ir em loops infinitos
Vito De Tullio
78
@VitoDeTullio: Você está se lembrando errado. Você corre o risco de loops infinitos ao fornecer uma função de comparação aleatória ; é necessária uma função de comparação para produzir um pedido total consistente . Uma chave aleatória está correta. Essa sugestão está errada porque não é garantido que os guias sejam aleatórios , não porque a técnica de classificação por uma chave aleatória está errada.
Eric Lippert 13/09
24
@ Doug: NewGuidgarante apenas que ele fornece um GUID exclusivo. Não garante a aleatoriedade. Se você estiver usando um GUID para outra finalidade que não seja a criação de um valor exclusivo , estará fazendo errado.
Eric Lippert 13/09
120

Estou um pouco surpreso com todas as versões desajeitadas desse algoritmo simples aqui. Fisher-Yates (ou Knuth shuffle) é um pouco complicado, mas muito compacto. Por que é complicado? Porque você precisa prestar atenção se o gerador de números aleatórios r(a,b)retorna valor onde bé inclusivo ou exclusivo. Também editei a descrição da Wikipedia para que as pessoas não sigam cegamente o pseudocódigo e criem bugs difíceis de detectar. Para .Net, Random.Next(a,b)retorna um número exclusivo b, sem mais delongas, veja como ele pode ser implementado em 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;
}

Experimente este código .

Shital Shah
fonte
Não seria melhor alterar rnd (i, list.Count) para rnd (0, list.Count) para que qualquer cartão pudesse ser trocado?
Donuts
16
@ Donuts - não. Se você fizer isso, você adicionará um viés no shuffle.
Shital Shah
2
Ao separar Swap <T> para um método separado, parece que você causa muitas alocações desnecessárias de T para temp.
argila
2
Eu argumentaria que o LINQ poderia potencialmente diminuir o desempenho do embaralhamento, e isso seria um motivo para não usá-lo, especialmente devido à relativa simplicidade do código.
winglerw28
7
Quando i = list.Count - 1, ou seja, a última iteração, rnd.Next(i, list.Count)retornará a você. Portanto, você precisa i < list.Count -1como condição de loop. Bem, você não 'precisa', mas salva uma iteração;)
Pod
78

Método de extensão para IEnumerable:

public static IEnumerable<T> Randomize<T>(this IEnumerable<T> source)
{
    Random rnd = new Random();
    return source.OrderBy<T, int>((item) => rnd.Next());
}
Denis
fonte
3
Note que este não é thread-safe, mesmo se usado em uma lista de thread-safe
BlueRaja - Danny Pflughoeft
1
como damos a lista <string> para esta função?
MonsterMMORPG
8
Existem dois problemas significativos com esse algoritmo: - OrderByusa uma variante do QuickSort para classificar os itens por suas chaves (ostensivamente aleatórias). O desempenho do QuickSort é O (N log N) ; em contraste, um embaralhamento de Fisher-Yates é O (N) . Para uma coleção de 75 elementos, isso pode não ser um grande problema, mas a diferença será acentuada para coleções maiores.
John Beyer
10
... - Random.Next()pode produzir uma distribuição de valores razoavelmente pseudo-aleatória, mas não garante que os valores sejam únicos. A probabilidade de chaves duplicadas aumenta (não linearmente) com N até atingir certeza quando N atinge 2 ^ 32 + 1. O OrderByQuickSort é uma classificação estável ; portanto, se vários elementos receberem o mesmo valor de índice pseudo-aleatório, sua ordem na sequência de saída será a mesma que na sequência de entrada; assim, um viés é introduzido no "shuffle".
John Beyer
27
@ JohnBeyer: Existem problemas muito, muito maiores do que a fonte do viés. Existem apenas quatro bilhões de sementes possíveis para o Random, que é muito, muito menor que o número possível de embaralhamento de um conjunto de tamanho moderado. Somente uma fração minúscula dos shuffles possíveis pode ser gerada. Esse viés diminui o viés devido a colisões acidentais.
Eric Lippert 13/09
16

A idéia é obter um objeto anônimo com o item e a ordem aleatória e, em seguida, reordenar os itens por esse pedido e retornar o valor:

var result = items.Select(x => new { value = x, order = rnd.Next() })
            .OrderBy(x => x.order).Select(x => x.value).ToList()
Andrey Kucher
fonte
3
melhor solução de um forro
vipin8169 08/03/19
1
Falta um ponto e vírgula no final fyi
reggaeguitar 17/04
Se alguém não tiver certeza sobre rnd, adicione isso antes do código acima Random rnd = new Random ();
Greg Trevellick
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;
    }
Adam Tegen
fonte
4
Você não deve fazer algo como var listCopy = list.ToList()evitar colocar todos os itens da lista de entrada? Realmente não entendo por que você deseja alterar essas listas para esvaziar.
Chris Marisic
9

EDIT O RemoveAté uma fraqueza na minha versão anterior. Esta solução supera isso.

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

Observe o opcional Random generator: se a implementação da estrutura base Randomnão for segura para threads ou criptograficamente forte o suficiente para suas necessidades, você poderá injetar sua implementação na operação.

Uma implementação adequada para uma implementação criptograficamente segura de thread-safe Randompode ser encontrada nesta resposta.


Aqui está uma idéia, estenda o IList de uma maneira (esperançosamente) eficiente.

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

Jodrell
fonte
Consulte stackoverflow.com/questions/4412405/… . você já deve estar ciente.
nawfal 30/05
@nawfal ver minha implementação melhorada.
Jodrell
1
hmm justo o suficiente. É GetNextou Next?
Nawfal 9/07
4

Você pode conseguir isso usando este método de extensão simples

public static class IEnumerableExtensions
{

    public static IEnumerable<t> Randomize<t>(this IEnumerable<t> target)
    {
        Random r = new Random();

        return target.OrderBy(x=>(r.Next()));
    }        
}

e você pode usá-lo fazendo o seguinte

// 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);
}
Shehab Fawzy
fonte
3
Eu manteria a Randominstância da classe fora da função como uma staticvariável. Caso contrário, você poderá obter a mesma semente de randomização do cronômetro se chamado em sucessão rápida.
Lemonseed
Uma observação interessante - se você instanciar a classe Random rapidamente em um loop, digamos entre 0 ms e 200 ms um do outro, você tem uma chance muito alta de obter a mesma semente de randomização - o que resulta em resultados repetidos. No entanto, você pode contornar isso usando Random rand = new Random (Guid.NewGuid (). GetHashCode ()); Isso efetivamente força a randomização a ser derivada do Guid.NewGuid ()
Baaleos
4

Esse é o meu método preferido de reprodução aleatória quando é desejável não modificar o original. É uma variante do algoritmo "de dentro para fora" de Fisher – Yates que funciona em qualquer sequência enumerável (o tamanho de sourcenão precisa ser conhecido desde o início).

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

Este algoritmo também pode ser implementado através da atribuição de um intervalo de 0ao length - 1e desgastante aleatoriamente os índices trocando o índice escolhido aleatoriamente com o último índice até que todos os índices foram escolhidos exatamente uma vez. Esse código acima realiza exatamente a mesma coisa, mas sem a alocação adicional. O que é bem legal.

Com relação à Randomclasse, é um gerador de números de uso geral (e, se eu estivesse na loteria, consideraria usar algo diferente). Ele também conta com um valor de semente baseado no tempo por padrão. Um pequeno alívio do problema é propagar a Randomclasse com RNGCryptoServiceProviderou você pode usar o RNGCryptoServiceProvidermétodo semelhante a este (veja abaixo) para gerar valores aleatórios de ponto flutuante duplo aleatoriamente escolhidos de maneira uniforme, mas a execução de uma loteria requer compreensão da aleatoriedade e da natureza de a fonte aleatória.

var bytes = new byte[8];
_secureRng.GetBytes(bytes);
var v = BitConverter.ToUInt64(bytes, 0);
return (double)v / ((double)ulong.MaxValue + 1);

O ponto de gerar um duplo aleatório (entre 0 e 1 exclusivamente) é usar para dimensionar para uma solução inteira. Se você precisar escolher algo de uma lista com base em um duplo aleatório xque sempre será, 0 <= x && x < 1é direto.

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

Aproveitar!

John Leidegren
fonte
4

Se você não se importa em usar dois Lists, provavelmente esta é a maneira mais fácil de fazê-lo, mas provavelmente não é a mais eficiente ou imprevisível:

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);
Xelights
fonte
3

Se você tiver um número fixo (75), poderá criar uma matriz com 75 elementos e enumerar sua lista, movendo os elementos para posições aleatórias na matriz. Você pode gerar o mapeamento do número da lista para o índice da matriz usando o shuffle Fisher-Yates .

dmo
fonte
3

Eu costumo usar:

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);
}
Albertein
fonte
list.RemoveAt é uma operação O (n), que torna essa implementação proibitivamente lenta.
George Polevoy
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.
user7767814
fonte
0

Aqui está um Shuffler eficiente que retorna uma matriz de bytes de valores aleatórios. Nunca embaralha mais do que o necessário. Pode ser reiniciado de onde parou anteriormente. Minha implementação real (não mostrada) é um componente MEF que permite a um shuffler de substituição especificado pelo usuário.

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

`

BSalita
fonte
0

Aqui está uma maneira segura de thread para fazer isso:

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

}
sumit laddha
fonte
2
Bem-vindo ao Stack Overflow! Considere adicionar alguma explicação à sua resposta, em vez de apenas um enorme bloco de código. Nosso objetivo aqui é educar as pessoas para que elas entendam a resposta e possam aplicá-la em outras situações. Se você comentar seu código e adicionar uma explicação, sua resposta será mais útil não apenas para a pessoa que fez a pergunta neste momento, mas para qualquer pessoa no futuro que possa estar tendo o mesmo problema.
starsplusplus
4
A maior parte desse código é totalmente irrelevante para a pergunta, e a única parte útil basicamente repete a resposta de Adam Tegen de quase 6 anos atrás.
TC
0

Uma modificação simples da resposta aceita que retorna uma nova lista em vez de trabalhar no local e aceita o mais geral IEnumerable<T>como muitos outros métodos do 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;
}
Extragorey
fonte
-5

Post antigo, com certeza, mas eu apenas uso um GUID.

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

Um GUID é sempre único e, uma vez que é regenerado toda vez que o resultado muda a cada vez.

DavidMc
fonte
Compacto, mas você tem uma referência na classificação de newGuids consecutivos como aleatórios de alta qualidade? Algumas versões do quid / uuid possuem carimbos de data e hora e outras partes não aleatórias.
Johan Lundberg
8
Essa resposta já foi dada e, pior, foi projetada para singularidade, não aleatoriedade.
Alex Angas
-7

Uma abordagem muito simples para esse tipo de problema é usar várias trocas de elementos aleatórios na lista.

No pseudo-código, seria assim:

do 
    r1 = randomPositionInList()
    r2 = randomPositionInList()
    swap elements at index r1 and index r2 
for a certain number of times
Aleris
fonte
1
Um problema com essa abordagem é saber quando parar. Ele também tende a exagerar quaisquer preconceitos no gerador de números pseudo-aleatórios.
Mark Bessey
3
Sim. Altamente ineficiente. Não há razão para usar uma abordagem como essa quando existem abordagens melhores e mais rápidas, que são igualmente simples.
7118 PeterAllenWebb
1
não muito eficiente ou eficaz ... A execução N vezes provavelmente deixaria muitos elementos em sua posição original.
NSJonas #