Ir para o conteúdo
OneKitly

Baralhar é mais difícil do que parece: um milhão de execuções do baralhamento de uma linha

Publicado a 11/06/2025 · 13 min de leitura · Calculadoras do dia a dia

Lena Hoffmann

Lena Hoffmann — Redatora de Ciência e Educação na OneKitly

Matemática · Física

Verificado a partir de 4 fontes

Ver perfil →
Em resumo

Ordenar um array com um comparador aleatório não o baralha. Corra um milhão de baralhamentos dos quatro elementos A, B, C, D através de `array.sort(() => Math.random() - 0.5)` e as vinte e quatro ordens possíveis deveriam sair cerca de 41 667 vezes cada uma. Não saem. No Node 26.3.0 (V8 14.6) a ordem identidade ABCD voltou 62 485 vezes e DBCA apenas 30 998 — uma razão de 2,03 para 1, e um qui-quadrado de 125 397 com 23 graus de liberdade contra um valor crítico de 27,32 a 5 %. A estrutura não é ruído: exatamente oito das vinte e quatro ordens caem na probabilidade 1/16 e as outras dezasseis em 1/32, um modelo que ajusta mais quatro milhões de execuções com um qui-quadrado de 19,7. O mesmo milhão passado por Fisher-Yates deu um qui-quadrado de 10,3, folgadamente dentro do acaso. A causa é que um comparador que devolve um sinal aleatório não é uma ordenação coerente, pelo que o resultado depende das entranhas do algoritmo de ordenação — o que torna o enviesamento específico do motor e da versão, e não apenas pequeno. Fisher-Yates são três linhas, é exato para qualquer comprimento e não tem essa dependência. Use-o, tire o índice de troca de forma inclusiva e recuse o atalho do módulo quando levar um inteiro aleatório para um intervalo.

O baralhamento que toda a gente escreve — ordenar com um comparador aleatório — é enviesado, e não pouco. Um milhão de execuções medidas mostram oito das vinte e quatro ordens a sair o dobro das vezes das outras dezasseis.

Um milhão de baralhamentos de quatro elementos

Quatro elementos têm vinte e quatro ordens. É pequeno o suficiente para contar exaustivamente, e é isso que faz dele o caso de teste certo: corra um baralhamento um milhão de vezes, conte com que frequência sai cada uma das vinte e quatro, e um baralhamento justo tem de pôr cerca de 41 667 em cada casa. Tudo o que favoreça sistematicamente algumas ordens aparecerá como um alto, e um único número — o qui-quadrado — resume a tabela inteira num veredito. Com 23 graus de liberdade, um método justo pontua à volta de 23, e qualquer coisa acima de 27,32 aconteceria menos de 5 % das vezes só por acaso.

O de uma linha pontuou 125 397. Nem 30, nem 300 — cinco mil vezes o limiar. A ordem mais frequente, BADC, saiu 62 810 vezes; a mais rara, DBCA, 30 998. A ordem original ABCD sobreviveu intacta 62 485 vezes, vez e meia mais do que devia. Passar o mesmo milhão por um Fisher-Yates bem escrito produziu 10,3, que é o aspeto de um método justo: os extremos foram 41 434 e 41 879, uma dispersão de cerca de 1 %. Esticar o teste para cinco elementos alarga ainda mais a distância — 229 683 contra 89,0 — porque cada elemento extra dá ao algoritmo de ordenação mais uma decisão para tomar mal.

Não vinte e quatro probabilidades, mas duas

As contagens não estão dispersas. Ordenadas, caem em dois grupos apertados: oito ordens à volta de 62 500 e dezasseis à volta de 31 250. São exatamente 1/16 e 1/32 de um milhão, e a aritmética fecha — 8 × (1/16) + 16 × (1/32) = 1. Mais quatro milhões de execuções testaram essa hipótese diretamente e produziram um qui-quadrado de 19,7 com 23 graus de liberdade, um bom ajuste. Portanto o baralhamento de uma linha neste motor não é aproximadamente uniforme com um tremor: é uma distribuição de dois valores em que metade da massa de probabilidade está entalada num terço dos resultados.

As oito ordens favorecidas partilham uma propriedade que vale a pena notar: ABCD, ABDC, ADBC, BACD, BADC, BDAC, DABC e DBAC mantêm todas o C fora das duas primeiras posições. Não é misticismo, é o algoritmo de ordenação a transparecer. Um array de quatro elementos é curto, por isso o V8 nunca sai do seu caminho de inserção binária; o comparador é chamado 4,5 vezes em média, e 4,5 lançamentos de moeda não podem gerar 24 resultados equiprováveis, porque 24 não divide nenhuma potência de dois. O enviesamento está cozido na forma da árvore de decisão antes sequer de o acaso entrar.

O enviesamento pertence ao motor, não à linguagem

A ECMA-262 exige que a função de comparação passada a sort seja um comparador coerente: transitivo, antissimétrico e que dê a mesma resposta para o mesmo par sempre. Um comparador construído sobre Math.random parte os três numa só chamada. A resposta da especificação não é definir o que acontece, mas dizer que se o comparador for incoerente o resultado da ordenação fica definido pela implementação. Essa única frase é a história toda. Não escreveu um baralhamento com estatísticas invulgares — escreveu um programa cuja saída a norma se recusa a especificar.

A consequência prática é que estas contagens medem um motor numa versão, não uma constante universal. O V8 mudou a sua ordenação mais de uma vez; uma execução noutro motor, ou no mesmo após uma atualização, produzirá outra distribuição irregular, e um array suficientemente longo para disparar o caminho de fusão produzirá ainda outra. Nada na norma o proíbe, e nada o avisa quando acontece. Um baralhamento cujas propriedades estatísticas se mexem quando se aplica um patch ao ambiente de execução não é um baralhamento testável.

Fisher-Yates e o erro de índice que o arruína

O algoritmo correto — o Algoritmo P de Knuth — percorre o array do último índice até ao segundo, e em cada posição i tira um índice j uniformemente entre 0 e i inclusive, trocando depois as posições i e j. Três detalhes sustentam toda a demonstração. O ciclo desce. O sorteio inclui o próprio i, portanto um elemento pode ficar onde está. E o intervalo encolhe em um a cada iteração, de modo que o número de caminhos de execução é n × (n−1) × … × 2 = n!, exatamente o número de permutações. Uma bijeção entre caminhos de execução e resultados é o que significa uniformidade, e vale para todo n, não apenas para os que testou.

Mude um carácter e parte-se. A variante comum percorre o array para cima e tira j do array inteiro de cada vez, o que parece mais aleatório e não é. Essa versão tem n^n caminhos de execução, e n^n nunca é múltiplo de n! para n acima de 2, pelo que algumas permutações têm de ser alcançáveis por mais vias do que outras. Medida no mesmo milhão de execuções pontuou 29 913 — mil vezes o limiar, com a ordem mais frequente em 58 698 e a mais rara em 31 233. É o mais perigoso dos dois erros precisamente porque se parece com a versão do manual e passa em qualquer inspeção a olho.

Enviesamento do módulo: a segunda forma de torcer um baralhamento

Fisher-Yates precisa de um inteiro uniforme num intervalo, e a forma óbvia de o obter de um valor aleatório de 32 bits é tirar o módulo do tamanho do intervalo. Isso é uniforme só quando o intervalo divide exatamente 2^32. Normalmente não divide: 2^32 módulo 52 é 48, portanto 48 dos 52 resultados recebem mais uma pré-imagem do que os outros quatro. A 32 bits o excesso resultante anda pelo milionésimo de por cento e ninguém o verá alguma vez. Encolha a fonte para um único byte e a mesma aritmética torna-se brutal: 256 módulo 52 é outra vez 48, mas agora 48 resultados ficam com 5 pré-imagens e 4 com apenas 4 — um excesso de 25 %, visível em uns poucos milhares de sorteios.

A correção é a amostragem por rejeição e não custa quase nada. Calcule o maior múltiplo do intervalo que cabe na sua fonte — para um sorteio de 32 bits e um intervalo de 52 é 2^32 menos 48 — sorteie de novo sempre que o valor caia acima disso, e tire o módulo apenas dos valores aceites. A região de rejeição são 48 valores em 4 294 967 296, portanto o número esperado de sorteios extra é de cerca de um em noventa milhões. Paga uma comparação por chamada e compra uniformidade exata, a melhor troca deste artigo inteiro.

Math.random não é uma máquina de baralhar

Mesmo um Fisher-Yates perfeito está limitado pelo gerador que tem por baixo. Um baralho de 52 cartas tem 52! ordens, ou seja 8,07 × 10^67, cerca de 2^225,6. O gerador por trás do Math.random no V8 carrega 128 bits de estado interno, logo pode alcançar no máximo 2^128 ≈ 3,4 × 10^38 ordens de baralho — uma fração de 4,2 × 10^-30 do total. A esmagadora maioria dos baralhamentos de um baralho padrão simplesmente não é produzível, por muitas vezes que o chame. É um teto matemático duro, não uma falha de implementação, e aplica-se a todo o gerador pseudoaleatório com menos estado do que o espaço que lhe é pedido cobrir.

Outras duas propriedades contam na prática. O Math.random não é semeável nem reproduzível: a especificação não oferece forma de fixar um ponto de partida, e exige explicitamente que realms distintos produzam sequências distintas, pelo que um bug que viu uma vez não pode ser repetido. E não é imprevisível no sentido criptográfico — um observador que veja saídas suficientes pode reconstruir o estado e prever o resto. Se alguém puder ganhar ao adivinhar o seu baralhamento — uma rifa, um sorteio com prémio, um token de segurança, tudo o que seja baralhado perante uma plateia — use crypto.getRandomValues, com amostragem por rejeição por cima. Se o baralhamento for um plano de lugares ou um questionário de treino, Math.random com um Fisher-Yates correto serve perfeitamente.

Razão
Um milhão de baralhamentos dos quatro elementos A, B, C, D por três métodos, contados permutação a permutação (Node 26.3.0, V8 14.6). Num baralhamento justo cada uma das 24 ordens deveria aparecer cerca de 41 667 vezes.
MétodoOrdem mais raraOrdem mais frequenteRazãoQui-quadrado, 23 glVeredito a 5 % (valor crítico 27,32)
sort(() => Math.random() - 0.5)30 998 (DBCA)62 810 (BADC)2,026125 397,2Enviesado sem qualquer dúvida
Fisher-Yates (ciclo descendente, índice inclusivo)41 434 (ABDC)41 879 (BDAC)1,01110,3Indistinguível do uniforme
Fisher-Yates com o erro de índice (índice tirado de todo o array)31 233 (DBCA)58 698 (BADC)1,87929 913,6Enviesado, e parece correto
O que daria um baralhamento justocerca de 41 667cerca de 41 6671,000cerca de 23A linha de referência
Aleatorizar linhasBaralha linhas de texto com escolha de algoritmo, opções de limpeza e uma semente reproduzível opcional.Experimentar a ferramenta →

Perguntas frequentes

Ordenar com um comparador aleatório está sempre enviesado, ou só nalguns navegadores?
Sempre enviesado, mas enviesado de forma diferente em cada lado. A norma exige um comparador coerente e declara o resultado definido pela implementação quando não o recebe, pelo que cada motor — e cada versão de cada motor — produz a sua própria distribuição irregular. No Node 26.3.0 o caso de quatro elementos colapsa em apenas duas probabilidades, 1/16 para oito ordens e 1/32 para as outras dezasseis, o que um teste de quatro milhões de execuções confirma com um qui-quadrado de 19,7 com 23 graus de liberdade. Outro motor não lhe dará esses números exatos: dar-lhe-á outros números errados. Isso é pior do que um enviesamento conhecido e fixo, porque não há nada estável contra que testar e uma atualização do ambiente de execução pode mudar a estatística do seu sorteio sem mudar uma linha do seu código.
Como escrevo Fisher-Yates para que fique realmente correto?
Comece no último índice e desça até ao índice 1. Em cada posição i, tire j uniformemente entre 0 e i inclusive, e depois troque os elementos em i e j. Três coisas têm de estar certas ao mesmo tempo: o ciclo desce, o sorteio inclui o próprio i, e o intervalo encolhe em um a cada iteração. Consiga as três e o número de caminhos de execução é exatamente n fatorial, um por permutação, o que torna a saída uniforme para qualquer comprimento de array e não apenas para os que calhou testar. A variante que sobe e tira j de todo o array de cada vez é o erro clássico: tem n elevado a n caminhos, que nunca é múltiplo de n fatorial acima de n = 2, e mediu um qui-quadrado de 29 913 no teste de quatro elementos onde um baralhamento justo pontua à volta de 23.
Preciso de crypto.getRandomValues, ou o Math.random chega?
O teste é se alguém poderia ganhar ao prever o resultado. Baralhar perguntas de um questionário, sentar uma turma, aleatorizar a ordem de exercícios: Math.random dentro de um Fisher-Yates correto serve, e a diferença nunca se verá. Sortear um prémio, escolher uma amostra de auditoria, gerar qualquer coisa que se comporte como um token: use crypto.getRandomValues, porque o Math.random é um gerador pseudoaleatório cujo estado interno se reconstrói a partir de uma série modesta de saídas, após o que todo o valor futuro é previsível. Há uma segunda razão, mais silenciosa. Um baralho de 52 cartas tem cerca de 2^225,6 ordens e o gerador do V8 carrega 128 bits de estado, portanto alcança no máximo uma ordem de baralho em 10^30. Esse teto é inerente ao tamanho do estado, não à qualidade do algoritmo.
O que é exatamente o enviesamento do módulo, e quando importa?
É o que acontece quando se aperta um intervalo que não divide a sua fonte. Pegue num valor aleatório de 32 bits e reduza-o módulo 52: obtém um número de 0 a 51, mas 2^32 dividido por 52 deixa resto 48, portanto 48 desses resultados têm mais um valor de origem a mapear neles do que os quatro restantes. A 32 bits o excesso resultante anda pelo milionésimo de por cento — verdadeiramente desprezável. A mesma aritmética numa fonte de 8 bits é outro animal: 256 módulo 52 também é 48, mas agora os resultados favorecidos recebem 5 valores de origem e os outros apenas 4, um excesso de 25 % que uns poucos milhares de sorteios expõem. A correção é a amostragem por rejeição: rejeitar todo o sorteio igual ou superior ao maior múltiplo do intervalo que caiba na sua fonte, o que para 32 bits e um intervalo de 52 descarta 48 valores em 4,29 mil milhões, cerca de um sorteio em noventa milhões.
Como testo o meu próprio baralhamento sem formação em estatística?
Encolha o problema até poder contar tudo. Pegue num array de quatro elementos, baralhe-o um milhão de vezes e mantenha a contagem de quantas vezes sai cada uma das vinte e quatro ordens — um dicionário indexado pela cadeia concatenada chega. Depois olhe para dois números: a contagem maior dividida pela menor, e a contagem da ordem original por baralhar. Um baralhamento justo dá uma razão perto de 1,01 nesse tamanho de amostra e deixa a ordem original à volta de 1 em 24. Nas medições aqui, o Fisher-Yates deu 1,011 e a ordenação de uma linha deu 2,026, com a ordem intacta a aparecer 50 % mais do que devia. Não precisa do qui-quadrado para ver essa diferença; o qui-quadrado só lhe diz quão impossível é, e 125 397 contra um limiar de 27,32 é quase o mais impossível que uma medição consegue ser.

Artigos que podem interessar-lhe

Todos os guias →
GuiaDividir pessoas em grupos justos: aleatório e justo não são a mesma exigência23 pessoas não se dividem por quatro, e uma divisão uniformemente aleatória pode entregar a um grupo todos os jogadores fortes. Eis a aritmética do resto, o custo medido do puro acaso e a correção por estratos.GuiaQuadros de torneio: byes, cabeças de série e porque os números têm de ser potências de doisUm quadro de eliminação direta reduz a metade o seu plantel a cada ronda, portanto só fecha numa potência de dois. O número de byes, o de rondas, a ordem das cabeças de série e o total de jogos decorrem todos desse único facto — e cada um cabe numa linha.TutorialComo sortear um nome sem que ninguém duvide do resultadoUm sorteio justo precisa de mais do que um número aleatório: equiprobabilidade, nenhum método enviesado e um resultado que outra pessoa possa verificar. Eis como fazer.ExplicaçãoProbabilidades das mãos de póquer, deduzidas em vez de memorizadasCada probabilidade de mão de póquer de cinco cartas é um argumento de contagem sobre 2 598 960 mãos, e cada um cabe numa linha. Aqui estão as nove, com a verificação que as prova: as contagens têm de somar exatamente C(52,5).ExplicaçãoA soma das probabilidades implícitas nunca dá cemDois lados ambos a -110 implicam 52,4 % cada um. Isso soma 104,8 %, e os 4,8 pontos que não deviam existir são a margem — visível assim que se convertem as cotas.ExplicaçãoNúmeros de cartão de teste: para que serve realmente o algoritmo de Luhn e o que não lhe pode dizerLuhn é uma soma de verificação para apanhar gralhas, patenteada em 1960, e é esse todo o seu trabalho. Um número que a passa não lhe diz nada sobre conta nenhuma. Para testar uma integração de pagamentos precisa dos números publicados pelo seu prestador, não de um gerado.

Ferramentas relacionadas

Fontes

Detetaste um erro neste artigo?