"Everyday I’m shufflin." LMFAO
Mixing up some cards. Easy, right?
Physical Shuffles
Most people when given a deck of cards to shuffle will use either the riffle or overhand shuffle.
Riffle
This shuffle involves splitting the deck into two groups and interweaving the cards from each group to reorder the deck. For a standard 52 card deck, there are $n!$ possible permutations. A single riffle, can only result in $2^{52}$ possible outcomes. That means multiple shuffles are needed to get a truly randomized deck. Bayer and Diaconis proposed seven as the optimal iterations for obtaining any arrangement for a standard deck, or $\frac 32\,log_2\,n$ shuffles for a $n$ sized deck.
Entropy from the cut and during the merge is required for the riffle to be effective. For the faro shuffle, or a perfect riffle when the deck is split exactly in half and one item is taken from each group at a time, the identity permutation is obtained after eight iterations for out-faro shuffles and after 52 iterations for in-faro for a standard deck.
Overhand
An overhand shuffle is accomplished by cutting the deck and swapping the top and bottom groups. While the algorithm is simple, the number of iterations required to achieve randomization is high because groups of cards stay together. Pemantle shows that between $n^2$ and $n^2\,log\,n$ shuffles are needed. That’s ~2,500 shuffles for a standard deck.
Modeling these physical shuffles for a program is costly because the number of iterations required to obtain an acceptable amount of randomization, especially when used for large $n$ sized collections.
The Goal
Instead of reproducing standard card shuffles, let’s approach the problem by talking about the goal.
Shuffling in some respects is the logical inverse of sorting. The desire outcome for sorting is an arrangement of the items into a specific ordered sequence. Shuffling’s goal is to arrange items into one of the $n!$ possible permutations selected at random, with each possible outcome having the same probability of selection.
In fact, sorting can be used to shuffle by assigning a unique random value to each element and sorting based on the assigned value. The time complexity becomes dependent on the sort used and the ability to generate and assign unique values to each item. How bout swapping each card with another card chosen at random instead of sorting?
Helpers
First let’s define some helper functions.
Exchange
Now let’s try looping through a deck and swapping each element with another randomly chosen element (including itself).
We did it! Or did we? This approach seems to be working, but upon closer examination, it turns out the results aren’t uniformly distributed as we would expect from a good shuffle. Instead of $n!$ possible permutations, the exchange shuffle
has $n^{n}$ possible swaps, resulting in over representation of certain outcomes.
The problem with this naive approach is discussed in detail in Jeff Atwood’s blog post. Also interesting is Goldstein’s paper states that the identity permutation is the most likely for large $n$.
Using Removal
The exchange shuffle
didn’t come to mind when I tried to come up with my own shuffling algorithm. I thought of something a bit more along the lines of how a child might do it. Throwing them all in the air and picking one at a time to get a new order. The throwing part isn’t necessary as long as you pick the cards at random. We can call it the removal shuffle
for lack of a better name.
Turns out this is pretty close to the Fisher-Yates Shuffle
, the basis of most modern shuffle implementations. Instead of removing elements, the original Fisher-Yates shuffle
strikes out elements when selected.
Fisher-Yates
The results of using these shuffles is uniformly distributed over the $n!$ possible permutations. However, the removal shuffle
has $n$ splices (average $O(\frac n2)$ time) and the Fisher-Yates
has $n$ strike out operations (also average $O(\frac n2)$ time), making both these shuffles $O(n^2)$ time complexity. We can do better than that.
The Solution
The missing piece of the puzzle is most obvious when looking at the change in sizes of the initial and shuffled arrays of the removal shuffle
. Let’s take another look:
We can see that the size of the shuffled array grows as the size of the initial array shrinks. Instead of removing or striking out selected elements, if we swap them into a ‘processed’ region of the initial array the time complexity reduced to $O(n)$ and the shuffle can be done in-place with $O(1)$ space. This modernized version of the Fisher-Yates shuffle
is often referred to as Knuth Shuffle.
Knuth
There are only $n-1$ operations because the last swap would always result in a no-op swap with itself.
Usages
The modernized Fisher-Yates shuffle
and its various forms is used in all sorts of libraries and languages. Here are some examples:
Python’s random library
uses the standard in place form.
Lodash’s _.shuffle
uses the “inside-out” form to initialize and shuffle a new array.
Java’s util.Collection.shuffle
uses the standard form. It is interesting to note that for large lists that do not implement RandomAccess
, the shuffle method makes a copy to an array to prevent quadratic time complexity.