Não é uma maneira de embaralhar que eu gosto, principalmente porque é O (n log n) sem um bom motivo, quando é fácil implementar um embaralhamento de O (n). O código da pergunta "funciona" basicamente atribuindo um número aleatório (espero que único!) A cada elemento e, em seguida, ordenando os elementos de acordo com esse número.
Prefiro a variante de Durstenfield do shuffle de Fisher-Yates que troca elementos.
A implementação de um Shuffle
método simples de extensão consistiria basicamente em chamarToList
ou ToArray
na entrada e no uso de uma implementação existente do Fisher-Yates. (Passe Random
como um parâmetro para tornar a vida geralmente mais agradável.) Existem muitas implementações por aí ... Provavelmente, tenho uma resposta em algum lugar.
O lado bom desse método de extensão é que seria muito claro para o leitor o que você realmente está tentando fazer.
EDIT: Aqui está uma implementação simples (sem verificação de erro!):
public static IEnumerable<T> Shuffle<T>(this IEnumerable<T> source, Random rng)
{
T[] elements = source.ToArray();
// Note i > 0 to avoid final pointless iteration
for (int i = elements.Length-1; i > 0; i--)
{
// Swap element "i" with a random earlier element it (or itself)
int swapIndex = rng.Next(i + 1);
T tmp = elements[i];
elements[i] = elements[swapIndex];
elements[swapIndex] = tmp;
}
// Lazily yield (avoiding aliasing issues etc)
foreach (T element in elements)
{
yield return element;
}
}
EDIT: Os comentários sobre o desempenho abaixo me lembraram que podemos realmente retornar os elementos à medida que os embaralhamos:
public static IEnumerable<T> Shuffle<T>(this IEnumerable<T> source, Random rng)
{
T[] elements = source.ToArray();
for (int i = elements.Length - 1; i >= 0; i--)
{
// Swap element "i" with a random earlier element it (or itself)
// ... except we don't really need to swap it fully, as we can
// return it immediately, and afterwards it's irrelevant.
int swapIndex = rng.Next(i + 1);
yield return elements[swapIndex];
elements[swapIndex] = elements[i];
}
}
Agora, isso fará apenas o trabalho necessário.
Observe que nos dois casos, você precisa ter cuidado com a instância Random
que usa como:
- Criar duas instâncias
Random
aproximadamente ao mesmo tempo produzirá a mesma sequência de números aleatórios (quando usado da mesma maneira)
Random
não é seguro para threads.
Eu tenho um artigoRandom
que detalha essas questões e fornece soluções.
source.ToArray();
que você deve terusing System.Linq;
o mesmo arquivo. Caso contrário, você receberá este erro:'System.Collections.Generic.IEnumerable<T>' does not contain a definition for 'ToArray' and no extension method 'ToArray' accepting a first argument of type 'System.Collections.Generic.IEnumerable<T>' could be found (are you missing a using directive or an assembly reference?)
Isso se baseia na resposta de Jon Skeet .
Nessa resposta, a matriz é embaralhada e retornada usando
yield
. O resultado líquido é que a matriz é mantida na memória durante a duração do foreach, além dos objetos necessários para a iteração, e ainda assim o custo está no início - o rendimento é basicamente um loop vazio.Esse algoritmo é muito usado em jogos, onde os três primeiros itens são escolhidos e os outros só serão necessários mais tarde, se houver. Minha sugestão é para
yield
os números assim que eles forem trocados. Isso reduzirá o custo de inicialização, mantendo o custo de iteração em O (1) (basicamente 5 operações por iteração). O custo total permaneceria o mesmo, mas o embaralhamento seria mais rápido. Nos casos em que isso é chamado,collection.Shuffle().ToArray()
pois teoricamente não fará diferença, mas nos casos de uso acima mencionados, acelerará a inicialização. Além disso, isso tornaria o algoritmo útil para casos em que você precisa apenas de alguns itens exclusivos. Por exemplo, se você precisar retirar três cartas de um baralho de 52, poderá pagardeck.Shuffle().Take(3)
e apenas três trocas ocorrerão (embora toda a matriz precise ser copiada primeiro).fonte
A partir desta citação de Skeet:
Vou explicar um pouco o motivo do que se espera que seja único!
Agora, a partir do Enumerable.OrderBy :
Isto é muito importante! O que acontece se dois elementos "recebem" o mesmo número aleatório? Acontece que eles permanecem na mesma ordem em que estão na matriz. Agora, qual é a possibilidade disso acontecer? É difícil calcular exatamente, mas existe o problema do aniversário que é exatamente esse problema.
Agora é real? É verdade?
Como sempre, em caso de dúvida, escreva algumas linhas de programa: http://pastebin.com/5CDnUxPG
Esse pequeno bloco de código embaralha uma matriz de 3 elementos um certo número de vezes, usando o algoritmo Fisher-Yates feito para trás, o algoritmo Fisher-Yates feito para frente (na página da wiki existem dois algoritmos de pseudo-código ... Eles produzem equivalentes resultados, mas um é feito do primeiro ao último elemento, enquanto o outro é feito do último ao primeiro elemento), o ingênuo algoritmo errado de http://blog.codinghorror.com/the-danger-of-naivete/ e usando o
.OrderBy(x => r.Next())
e o.OrderBy(x => r.Next(someValue))
.Agora, Random.Next é
então é equivalente a
Para testar se esse problema existe, podemos aumentar a matriz (algo muito lento) ou simplesmente reduzir o valor máximo do gerador de números aleatórios (
int.MaxValue
não é um número "especial" ... É simplesmente um número muito grande). No final, se o algoritmo não for influenciado pela estabilidade doOrderBy
, qualquer faixa de valores deverá fornecer o mesmo resultado.O programa então testa alguns valores, no intervalo de 1 a 4096. Observando o resultado, é bastante claro que, para valores baixos (<128), o algoritmo é muito tendencioso (4-8%). Com 3 valores você precisa pelo menos
r.Next(1024)
. Se você aumentar a matriz (4 ou 5), nem issor.Next(1024)
será suficiente. Eu não sou especialista em baralhar e em matemática, mas acho que para cada bit extra de comprimento da matriz, você precisa de 2 bits extras de valor máximo (porque o paradoxo do aniversário está conectado ao sqrt (numvalues)), então que, se o valor máximo for 2 ^ 31, direi que você poderá classificar matrizes de até 2 ^ 12/2 ^ 13 bits (4096-8192 elementos)fonte
Provavelmente está ok para a maioria dos propósitos, e quase sempre gera uma distribuição verdadeiramente aleatória (exceto quando Random.Next () produz dois números inteiros aleatórios idênticos).
Ele funciona atribuindo a cada elemento da série um número inteiro aleatório e ordenando a sequência por esses números inteiros.
É totalmente aceitável para 99,9% dos aplicativos (a menos que você precise absolutamente lidar com o caso de borda acima). Além disso, a objeção de skeet ao seu tempo de execução é válida; portanto, se você estiver embaralhando uma lista longa, poderá não querer usá-la.
fonte
Isso já aconteceu várias vezes antes. Pesquise Fisher-Yates no StackOverflow.
Aqui está um exemplo de código C # que escrevi para esse algoritmo. Você pode parametrizar em algum outro tipo, se preferir.
fonte
Random
como uma variável estática como esta -Random
não é seguro para threads. Veja csharpindepth.com/Articles/Chapter12/Random.aspxRandom
é uma dor de usar, como observado no meu artigo.Parece um bom algoritmo de embaralhamento, se você não está muito preocupado com o desempenho. O único problema que eu apontaria é que seu comportamento não é controlável; portanto, você pode ter dificuldade em testá-lo.
Uma opção possível é ter uma semente a ser passada como parâmetro para o gerador de números aleatórios (ou o gerador aleatório como parâmetro), para que você possa ter mais controle e testá-lo mais facilmente.
fonte
Achei a resposta de Jon Skeet inteiramente satisfatória, mas o robo-scanner do meu cliente relatará qualquer instância
Random
como uma falha de segurança. Então eu troquei por issoSystem.Security.Cryptography.RNGCryptoServiceProvider
. Como bônus, ele corrige o problema de segurança do thread mencionado. Por outro lado,RNGCryptoServiceProvider
foi medido como 300x mais lento que o usoRandom
.Uso:
Método:
fonte
Procurando por um algoritmo? Você pode usar minha
ShuffleList
classe:Em seguida, use-o assim:
Como funciona?
Vamos dar uma lista ordenada inicial dos 5 primeiros números inteiros:
{ 0, 1, 2, 3, 4 }
.O método começa contando o número de elementos e o chama
count
. Então, com acount
diminuição em cada etapa, é necessário um número aleatório entre0
ecount
e move-lo para o final da lista.No exemplo passo a passo a seguir, os itens que podem ser movidos estão em itálico , o item selecionado está em negrito :
0 1 2 3 4
0 1 2 3 4
0 1 2 4 3
0 1 2 4 3
1 2 4 3 0
1 2 4 3 0
1 2 3 0 4
1 2 3 0 4
2 3 0 4 1
2 3 0 4 1
3 0 4 1 2
fonte
Esse algoritmo embaralha, gerando um novo valor aleatório para cada valor em uma lista e, em seguida, ordenando a lista por esses valores aleatórios. Pense nisso como adicionar uma nova coluna a uma tabela na memória, preenchê-la com GUIDs e classificar por essa coluna. Parece uma maneira eficiente para mim (especialmente com o açúcar lambda!)
fonte
Um pouco sem relação, mas aqui está um método interessante (que mesmo sendo realmente excessivo, REALMENTE foi implementado) para a geração verdadeiramente aleatória de dados!
Dados-O-Matic
A razão pela qual estou postando isso aqui é que ele faz alguns pontos interessantes sobre como seus usuários reagiram à ideia de usar algoritmos para embaralhar, sobre dados reais. É claro que, no mundo real, essa solução é apenas para os extremos realmente extremos do espectro, onde a aleatoriedade tem um impacto tão grande e talvez o impacto afeta o dinheiro;).
fonte
Eu diria que muitas respostas aqui como "Esse algoritmo embaralha, gerando um novo valor aleatório para cada valor em uma lista e ordenando a lista por esses valores aleatórios" pode estar muito errado!
Eu acho que isso NÃO atribui um valor aleatório a cada elemento da coleção de origem. Em vez disso, pode haver um algoritmo de classificação em execução como o Quicksort que chamaria uma função de comparação aproximadamente n log n vezes. Algum tipo de algortihm realmente espera que esta função de comparação seja estável e sempre retorne o mesmo resultado!
Não seria possível que o IEnumerableSorter chame uma função de comparação para cada etapa do algoritmo, por exemplo, quicksort, e cada vez chame a função
x => r.Next()
para os dois parâmetros sem armazenar esses em cache!Nesse caso, você pode realmente atrapalhar o algoritmo de classificação e torná-lo muito pior do que as expectativas em que o algoritmo se baseia. Obviamente, eventualmente se tornará estável e retornará algo.
Eu poderia checar mais tarde colocando a saída de depuração dentro de uma nova função "Next" para ver o que acontece. No Reflector, não consegui descobrir imediatamente como funciona.
fonte
Hora de inicialização para executar no código, limpar todos os threads e armazenar em cache a cada novo teste,
Primeiro código malsucedido. É executado no LINQPad. Se você seguir para testar este código.
list.OrderBy (x => r.Next ()) usa 38.6528 ms
list.OrderBy (x => Guid.NewGuid ()) usa 36.7634 ms (recomendado do MSDN.)
depois da segunda vez, ambos usam ao mesmo tempo.
EDIT: CÓDIGO DE TESTE no Intel Core i7 [email protected], RAM 8 GB DDR3 @ 1600, HDD SATA 5200 rpm com [Dados: www.dropbox.com/s/pbtmh5s9lw285kp/data]
Descrição do resultado: https://www.dropbox.com/s/9dw9wl259dfs04g/ResultDescription.PNG
Estatística do resultado: https://www.dropbox.com/s/ewq5ybtsvesme4d/ResultStat.PNG
Conclusão:
Suponha que: LINQ OrderBy (r.Next ()) e OrderBy (Guid.NewGuid ()) não são piores que o método Shuffle definido pelo usuário na primeira solução.
Resposta: Eles são contraditórios.
fonte