(I have asked the question at stackoverflow.com here but maybe it's better to ask here for the statistical part. Feel free to correct my question, fix the tags, redirect me...)
I need a generator for many (up to one trillion, $10^{12}$) unique, pseudo random 64-bit numbers, that is range $[0..2^{64})$. It should be as "fair" as possible, have the same statistical distribution as if generated using a regular pseudo-RNG and then sorted. The problem is that sorting $10^{12}$ numbers is slow. The use case is replicating a test that was run for BBHash (in the paper, 4.5 Indexing a trillion keys).
The simplest (wrong, in my view) solution, which is not random, is to generate an evenly spaced sequence like 0, 1000, 2000,... . Looking at the source code, this seems to be about what the BBHash test does. Another solution is to iterate over all numbers, and with probability about ${10^{12}}/{2^{64}}$ return the number, otherwise skip it. But this is too slow, and will not return exactly that many results (and adjusting the probability while you go sounds like the distribution would not be fully correct).
I think there are three approaches:
Random Gaps
Maybe the solution in "An Efficient Algorithm for Sequential Random Sampling" would work, but it is slow because for each iteration, many floating point numbers need to be calculated. Also, the sum of such gaps, after $10^{12}$ , may not land where it should due to rounding (I wouldn't want to try that).
Randomly sized ranges with fixed number of entries
With a random number generator, split the range $[0..2^{64})$ into one million randomly sized sub-ranges that each should contain one million entries. But how, and which distribution to use. Then generate one million numbers in that range using a pseudo-RNG.
Random number of entries for each fixed (sub-)range
Create a tree: For each bit level (starting with the most significant bit), recursively generate a random number of how many entries should have the bit at that level set to 0, using the normal distribution. The remaining entries have the bit at this level set to 1. At each recursion level, this will narrow the range by about half. Stop for example when there are less than 1 million entries, and then switch to using an in-memory pseudo-RNG and sort those numbers (or use a bit field).
I can calculate the probability that, for $x$ coin flips, more than $y$ are heads, using the normal distribution (with a CDF approximation). And then pick a random number and do a binary search. Is that the correct, best (easiest, fastest) way? Here is the code that I use so far (Java code, but the algorithm and formulas should be relatively easy to understand even if you don't know Java):
public static void main(String... args) {
Random r = new Random();
Iterator<Long> it = randomSequence(r, 10, 32);
while(it.hasNext()) {
System.out.println(it.next());
}
}
/**
* Random sequence generator.
*
* @param r the random generator
* @param size the number of entries to generate
* @param shift the number of bits of the result
* @return the iterator
*/
static Iterator<Long> randomSequence(final Random r, final long size, final int shift) {
if (size < 5) {
// small lists are generated using a regular hash set
TreeSet<Long> set = new TreeSet<Long>();
while (set.size() < size) {
set.add(r.nextLong() & ((2L << shift) - 1));
}
return set.iterator();
}
// large lists are created recursively
return new Iterator<Long>() {
long remaining = size, zeros = randomHalf(r, size);
Iterator<Long> lowBits0 = randomSequence(r, zeros, shift - 1);
Iterator<Long> lowBits1;
@Override
public boolean hasNext() {
return remaining > 0;
}
@Override
public Long next() {
remaining--;
if (lowBits0.hasNext()) {
return lowBits0.next();
}
if (lowBits1 == null) {
lowBits1 = randomSequence(r, size - zeros, shift - 1);
}
return (1L << shift) + lowBits1.next();
}
};
}
/**
* Get the number of entries that are supposed to be below the half,
* according to the probability theory. For example, for a number of coin
* flips, how many are heads.
*
* @param r the random generator
* @param samples the total number of entries
* @return the number of entries that should be used for one half
*/
static long randomHalf(Random r, long samples) {
long low = 0, high = samples;
double x = r.nextDouble();
while (low + 1 < high) {
long mid = (low + high) / 2;
double p = probabilityBucketAtMost(samples, mid);
if (x > p) {
low = mid;
} else {
high = mid;
}
}
return (low + high) / 2;
}
static double probabilityBucketAtMost(long flips, long heads) {
// https://www.fourmilab.ch/rpkp/experiments/statistics.html
long x = heads;
long n = flips;
double variance = Math.sqrt(n/4);
// mean
long mu = n / 2;
// https://en.wikipedia.org/wiki/Normal_distribution
// Numerical approximations for the normal CDF
// the probability that the value of a standard normal random variable X is <= x
return phi((x - mu) / variance);
}
static double phi(double x) {
return 0.5 * (1 + Math.signum(x) * Math.sqrt(1 - Math.exp(-2 * x * x / Math.PI)));
}