Search code examples
javarandomprng

Stable mapping of an integer to a random number


I need a stable and fast one way mapping function of an integer to a random number. By "stable" I mean that the same integer should always map to the same random number. And by "random number" I actually mean "some number which behaves like random".

e.g.

1 -> 329423
2 -> -12398791234
3 -> -984
4 -> 42342435
...

If I had enough memory (and time) I would ideally use:

for( int i=Integer.MIN_VALUE; i<Integer.MAX_VALUE; i++ ){
    map[i]=i;
}
shuffle( map );

I could use some secure hash function like MD5 or SHA but these are to slow for my purposes and I don't need any crypto/security properties.

I only need this in one way. So I will never have to translate the random number back to its integer.

Background: (For those who want to know more)

I'm planing to use this to invalidate a complete cache over a given amount of time. The invalidation is done "randomly" on access of the cache member with an increasing chance while time passes. I need this to be stable so that isValid( entry ) does not "flicker" and for consistent testing. The input to this function will be the java hash of the key of the entry which typically is in the range of "1000"-"15000" (but can contain some other stuff, too) and comes in bulks. The invalidation is done on the condition of:

elapsedTime / timeout * Integer.MAX_VALUE > abs( random( key.hashCode() ) )

EDIT: (this is to long for a comment so I put it here)

I tried gexicide's answer and it turns out this isn't random enough. Here is what I tried:

        for( int i=0; i<12000; i++ ){

            int hash = (""+i).hashCode();

            Random rng = new Random( hash );
            int random = rng.nextInt();

            System.out.printf( "%05d, %08x, %08x\n", i, hash, random );

        }

The output starts with:

00000, 00000030, bac2c591
00001, 00000031, babce6a4
00002, 00000032, bace836b
00003, 00000033, bac8a47e
00004, 00000034, baab49de
00005, 00000035, baa56af1
00006, 00000036, bab707b7
00007, 00000037, bab128ca
00008, 00000038, ba93ce2a
00009, 00000039, ba8def3d
00010, 0000061f, 98048199

and it goes on in this way.

I could use SecureRandom instead:

    for( int i=0; i<12000; i++ ){

            SecureRandom rng = new SecureRandom( (""+i).getBytes() );
            int random = rng.nextInt();

            System.out.printf( "%05d, %08x\n", i, random );
        }

which indeed looks pretty random but this is not stable anymore and 10 times slower than the method above.


Solution

  • Use a Random and seed it with your number:

    Random generator = new Random(i);
    return generator.nextInt();
    

    As your testing exposes, the problem with this method is that such a seed creates a very poor random number in the first iteration. To increase the quality of the result, we need to run the random generator a few times; this will fill up the state of the random generator with pseudo-random values and will increase the quality of the following values.

    To make sure that the random generator spreads the values enough, use it a few times before outputting the number. This should make the resulting number more pseudo-random:

    Random generator = new Random(i);
    for(int i = 0; i < 5; i++) generator.nextInt();
    return generator.nextInt();
    

    Try different values, maybe 5 is enough.