The idea of a random generator that simulates shuffling is good if you can get one whose maximum period you can control.
A Linear congruent generator calculates a random number by the formula:
x[i + 1] = (a * x[i] + c) % m;
The maximum period is m, and this is achieved by observing the following properties:
- Parameters c and m are coprime.
- For each prime r dividing m, a - 1 is a multiple of r.
- If m is a multiple of 4, then also a-1 is a multiple of 4.
My first darft involved creating the next multiple of 4 after the length of the array, and then finding the appropriate values of a and c. It was (a) a lot of work and (b) sometimes yielded very obvious results.
I rethought this approach. We can make m the smallest power of two so that the length of the array is included. The only prime coefficient m is 2, which will make every odd number relatively simple with it. With the exception of 1 and 2, m will be divided by 4, which means that we must make a - 1 a a multiple of 4.
Having more than m than the length of the array, we must discard all values that are illegal array indices. This will happen in almost every other corner and should be negligible.
The following code lists pseudo-random numbers with a period of exceeding m. I avoided the trivial values for a and c and my (not too numerous) spotted checks, the results looked good. At least there was no obvious cycling scheme.
So:
class RandomIndexer { public: RandomIndexer(size_t length) : len(length) { m = 8; while (m < length) m <<= 1; c = m / 6 + uniform(5 * m / 6); c |= 1; a = m / 12 * uniform(m / 6); a = 4*a + 1; x = uniform(m); } size_t next() { do { x = (a*x + c) % m; } while (x >= len); return x; } private: static size_t uniform(size_t m) { double p = std::rand() / (1.0 + RAND_MAX); return static_cast<int>(m * p); } size_t len; size_t x; size_t a; size_t c; size_t m; };
Then you can use the generator as follows:
std::vector<int> list; for (size_t i = 0; i < 3; i++) list.push_back(i); RandomIndexer ix(list.size()); for (size_t i = 0; i < list.size(); i++) { std::cout << list[ix.next()]<< std::endl; }
I know this is still not a great random number generator, but it is fast enough, does not require a copy of the array, and seems to work fine.
If the approach of choosing a and c randomly yields bad results, it might be a good idea to limit the generator to some powers of two and hard-coded literary values that turn out to be good.