As Janos and a couple of other people have already mentioned, the easiest way to draw three random cards fairly (without putting any back in the deck) is to do what effectively amounts to three steps of a Fisher-Yates shuffle. Janos' explanation is doubtless the best.
However, it should be noted that a random generator needs some horsepower in order to drive a 54-card deck. It needs to have at least ceil(log(54!)/log(2))
bits of state - about 238 - to be able to (potentially) generate every possible permutation of 54 cards.
Some current language runtimes have generators that meet this requirement but none - not Java, not .Net, not C++ - ship with any decent generators that fulfil basic quality criteria for pseudorandom number generators (PRNGs). Yes, even the mighty mt19937
Mersenne generator with its astronomical period fails certain basic tests. I doubt that so many people would still like it if they knew what a prissy beast it is and all the caveats that govern its proper use.
The random generators that ship with current language runtimes aren't good for much except for homework assignments and for light testing duty.
There's one additional caveat that concerns drawing unbiased random numbers in some given range, as when following Janos' description how to draw three cards fairly.
For example, if a generator is available that returns random 32-bit numbers - like Java's Random.nextInt()
- then one might be tempted to use modulo division in order to get a random number in some reduced range. Now take this example (unsigned for the sake of exposition):
random_uint32() % 0xAAAAAAAAu
It will indeed yield values in the half-open range [0, 0xAAAAAAAAu)
but all values in the first half of the range are exactly twice as likely as those in the second half. Why? The period of the generator is not an exact multiple of the modulus, and so some results must occur more often than others. That's called bias. Just as problematic is multiplying a random float in the range [0.0, 1.0)
with the modulus:
random_double() * modulus
It suffers from exactly the same problem, except that the excess isn't dumped at the beginning of the range as in the earlier example. Instead the difference is smeared over the whole range, so that more likely results are mixed with less likely ones in a regular pattern.
One standard way of eliminating bias in this situation is to use the rejection method. It can be used to achieve unbiassed results of virtually any size or shape from a uniform random source. In the case under consideration it works by keeping only such random numbers that fall in the range [0, floor(R / modulus) * modulus)
where R is the period of the random generator (2^32 in the earlier example). The other numbers are thrown away and a new one is drawn. The result is that a subsequent modulo division with the modulus leaves a perfectly unbiassed result.
Java's Random.nextInt(int)
employs exactly this method (see source code), so all is well.
However, its twin method in the .Net framework - Random.Next(Int32)
- cuts corners by using the multiplication method above, after needlessly converting the random bits to a double. So it has not only poor quality, but also poor performance to boot. The full scale of the disaster can be inspected in the published source code.
In other words, just because something is a basic part of a modern/recent language runtime or standard library doesn't mean it has to be any good. And something that works perfectly well in one Javaish/Hashish language may not work quite so well in the other (C# isn't always the black sheep here), even though the statements look virtually identical and the classes even have the same names. Caveat emptor.
Note: the problems I mentioned will not cause mission failure in a homework assignment but it would be wise to keep them in mind whenever the topic veers towards computers, gambling and fair dice... It's not hard to build (or find) a decent random generator for whatever one's requirements might be; however, saying new Random()
works only for the lightest of duties.