Prove an Incremental Shuffle Is Uniform
ModalSoftware Engineer · Technical ScreenLast updated
Tests formal inductive reasoning on randomized algorithms by proving the uniformity of the Fisher-Yates shuffle, and evaluates practical knowledge of common implementation pitfalls like off-by-one bounds and modulo bias.
Problem Description
Consider the forward variation of the Fisher-Yates (Knuth) shuffle operating in place on an array arr of n distinct elements:
import random
def shuffle(arr: list[int]) -> None:
n = len(arr)
for i in range(n - 1):
# Pick a random index j uniformly from [i, n - 1]
j = random.randint(i, n - 1)
arr[i], arr[j] = arr[j], arr[i]
Provide a rigorous proof of correctness and analyze two standard implementation pitfalls:
Part 1: Induction Proof
Formulate a precise loop invariant over the prefix arr[0 : i] and prove by induction that:
- After step
i(fori = 0, 1, ..., n - 2), the prefixarr[0 : i + 1]contains a uniformly random permutation of some subset ofi + 1elements from the original array. - Upon loop termination, every one of the
n!possible permutations of the input array is produced with equal probability1 / n!.
Part 2: Endpoint / Range Mistake
A common bug in naive implementations is choosing j uniformly from [0, n - 1] at every iteration instead of [i, n - 1]:
# Buggy line:
j = random.randint(0, n - 1)
Explain why this breaks uniformity. Use a counting argument (for example, with n = 3) to prove that not all permutations can have equal probability.
Part 3: Modulo Bias
Suppose randint(i, n - 1) is implemented using a standard C-style pseudo-random function as i + (rand() % (n - i)), where rand() produces an integer uniformly distributed in [0, RAND_MAX].
Explain mathematically why this introduces modulo bias when (RAND_MAX + 1) is not divisible by (n - i), and quantify which outcomes receive higher probabilities.
Hints
Hint 1
For the induction step, consider the conditional probability that a specific remaining element is selected for position i given that it was not chosen in any earlier step 0 through i - 1.
Hint 2
For the endpoint mistake, count the total number of distinct execution paths the algorithm can take (a power of n) and compare it to the number of permutations n!.
Hint 3
For modulo bias, partition the range [0, RAND_MAX] into full buckets of size (n - i) and observe what happens to the remaining leftover values.
Follow-up questions
- How would you implement an unbiased random integer generator in range [0, k - 1] using rejection sampling given an arbitrary rand() in [0, RAND_MAX]?
- Could a 32-bit PRNG ever produce every permutation of a standard 52-card deck, even if Fisher-Yates is implemented without bugs? Why or why not?
- How does the proof adapt if you stream elements of unknown total size n, such as in Reservoir Sampling?
Solution walkthrough is for members
Get the full walkthrough with reference code and complexity analysis, plus Kitty Coach at 300 messages a day. Hints, the code runner and community answers stay free.