Edit: Então, basicamente, o que estou tentando escrever é um hash de 1 bit double
.
Quero mapear uma double
para true
ou false
com uma chance de 50/50. Para isso, escrevi um código que seleciona alguns números aleatórios (como exemplo, quero usar isso em dados com regularidades e ainda obter um resultado 50/50) , y
verifico seu último bit e incrementa se for 1 ou n
se for 0
No entanto, esse código resulta constantemente em 25% y
e 75% n
. Por que não é 50/50? E por que uma distribuição tão estranha, mas direta (1/3)?
public class DoubleToBoolean {
@Test
public void test() {
int y = 0;
int n = 0;
Random r = new Random();
for (int i = 0; i < 1000000; i++) {
double randomValue = r.nextDouble();
long lastBit = Double.doubleToLongBits(randomValue) & 1;
if (lastBit == 1) {
y++;
} else {
n++;
}
}
System.out.println(y + " " + n);
}
}
Exemplo de saída:
250167 749833
java
random
double
bit-manipulation
probability
gvlasov
fonte
fonte
doubleValue % 1 > 0.5
, mas isso seria muito grosseiro, pois pode introduzir regularidades visíveis em alguns casos (todos os valores estão dentro do intervalo 1). Se isso é muito granular, provavelmente deveríamos tentar intervalos menores, comodoubleValue % 1e-10 > 0.5e-10
? Bem, sim. E tomar apenas o último pedaço como um hash de adouble
é o que acontece quando você segue essa abordagem até o fim, com o módulo menos possível.(lastbit & 3) == 0
iria funcionar, por mais estranho que seja.Respostas:
Porque nextDouble funciona assim: ( fonte )
next(x)
fazx
bits aleatórios.Agora, por que isso importa? Como cerca da metade dos números gerados pela primeira parte (antes da divisão) é menor
1L << 52
e, portanto, seu significado não preenche inteiramente os 53 bits que ele poderia preencher, significando que o bit menos significativo do significando é sempre zero para eles.Devido à quantidade de atenção que está recebendo, aqui estão algumas explicações extras sobre o que
double
realmente é um Java (e muitas outras linguagens) e por que isso importava nesta pergunta.Basicamente, a
double
aparência é a seguinte: ( fonte )Um detalhe muito importante que não é visível nesta figura é que os números são "normalizados" 1, de modo que a fração de 53 bits começa com 1 (escolhendo o expoente tal que é assim), que 1 é omitido. É por isso que a imagem mostra 52 bits para a fração (significando), mas existem efetivamente 53 bits nela.
A normalização significa que, se o código
nextDouble
do 53º bit estiver definido, esse bit será o líder implícito 1 e desaparecerá, e os outros 52 bits serão copiados literalmente para o significando do resultadodouble
. Se esse bit não estiver definido, no entanto, os bits restantes deverão ser deslocados para a esquerda até que sejam definidos.Em média, metade dos números gerados se enquadra no caso em que o significando não foi alterado para a esquerda (e cerca de metade deles tem 0 como o bit menos significativo), e a outra metade é alterada em pelo menos 1 (ou é apenas completamente zero), então o bit menos significativo é sempre 0.
1: nem sempre, claramente isso não pode ser feito para zero, que não possui o mais alto 1. Esses números são chamados de números anormais ou subnormais, consulte a Wikipedia: número denormal .
fonte
random.nextDouble()
normalmente é o "melhor" caminho para o que se destina, mas a maioria das pessoas não está tentando produzir um hash de 1 bit a partir do dobro aleatório. Você está procurando distribuição uniforme, resistência à criptoanálise ou o quê?next
deve retornar umint
, para que possa ter apenas até 32 bits de qualquer maneira #Dos documentos :
Mas também afirma o seguinte (ênfase minha):
Esta observação existe desde o Java 5 pelo menos (os documentos para Java <= 1.4 estão atrás de uma parede de login, com preguiça de verificar). Isso é interessante, porque o problema aparentemente ainda existe até no Java 8. Talvez a versão "fixa" nunca tenha sido testada?
fonte
Esse resultado não me surpreende, dado que os números de ponto flutuante são representados. Suponhamos que tivéssemos um tipo de ponto flutuante muito curto, com apenas 4 bits de precisão. Se gerássemos um número aleatório entre 0 e 1, distribuído uniformemente, haveria 16 valores possíveis:
Se era assim que pareciam na máquina, você poderia testar o bit de ordem baixa para obter uma distribuição 50/50. No entanto, os flutuadores IEEE são representados como uma potência de 2 vezes uma mantissa; um campo no flutuador é a potência de 2 (mais um deslocamento fixo). A potência de 2 é selecionada para que a parte "mantissa" seja sempre um número> = 1.0 e <2.0. Isso significa que, na verdade, outros números que
0.0000
não seriam representados assim:(
1
Antes do ponto binário é um valor implícito; para flutuações de 32 e 64 bits, nenhum bit é realmente alocado para reter isso1
.)Mas observar o acima exposto deve demonstrar por que, se você converter a representação em bits e observar o bit baixo, receberá zero 75% do tempo. Isso ocorre devido a todos os valores menores que 0,5 (binários
0.1000
), que são metade dos valores possíveis, com as mantissas deslocadas, fazendo com que 0 apareça no bit mais baixo. A situação é essencialmente a mesma quando a mantissa possui 52 bits (não incluindo o 1 implícito) comodouble
faz.(Na verdade, como @sneftel sugeriu em um comentário, poderíamos incluir mais de 16 valores possíveis na distribuição, gerando:
Mas não tenho certeza de que seja o tipo de distribuição que a maioria dos programadores esperaria, por isso provavelmente não vale a pena. Além disso, não ganha muito quando os valores são usados para gerar números inteiros, como costumam ser os valores aleatórios de ponto flutuante.)
fonte