Master randomized algorithms — the difference between Las Vegas and Monte Carlo algorithms, randomized quicksort with expected O(n log n), reservoir sampling for streaming data, randomized selection for kth element, Miller-Rabin primality test, and why randomization defeats adversarial inputs.
What Are Randomized Algorithms?
A randomized algorithm is an algorithm that makes random choices during its execution. Instead of following a fixed deterministic path, it uses random numbers to guide decisions. This randomness provides powerful benefits:
- Break adversarial patterns: An attacker cannot craft a worst-case input because the algorithm's behavior is unpredictable
- Simplify algorithms: Complex deterministic algorithms often have elegant randomized equivalents
- Expected efficiency: Many problems that are hard to solve deterministically in O(n log n) have simple randomized solutions with the same expected complexity
Randomized QuickSort — Las Vegas Algorithm
Why Randomize?
Standard QuickSort chooses a fixed pivot (e.g., last element). An adversary who knows this can create sorted or reverse-sorted input that causes O(n²) performance every time.
Randomized QuickSort picks a random pivot, making adversarial inputs impossible to construct.
import random
def randomized_quicksort(arr, low=None, high=None):
"""
Randomized QuickSort — Las Vegas algorithm.
Expected O(n log n) for ANY input, not just random inputs.
No adversary can force O(n²) because pivot is random.
"""
if low is None:
low, high = 0, len(arr) - 1
if low < high:
# Random pivot selection — KEY CHANGE from standard quicksort
pivot_idx = random.randint(low, high)
arr[pivot_idx], arr[high] = arr[high], arr[pivot_idx] # Move to end
pivot_pos = partition(arr, low, high)
randomized_quicksort(arr, low, pivot_pos - 1)
randomized_quicksort(arr, pivot_pos + 1, high)
return arr
def partition(arr, low, high):
pivot = arr[high]
i = low - 1
for j in range(low, high):
if arr[j] <= pivot:
i += 1
arr[i], arr[j] = arr[j], arr[i]
arr[i+1], arr[high] = arr[high], arr[i+1]
return i + 1
# Test on adversarial input (sorted — worst case for deterministic quicksort)
arr = list(range(1000)) # Already sorted — O(n²) for fixed pivot!
result = randomized_quicksort(arr.copy())
print(result[:5], "...", result[-5:]) # Works in O(n log n) expected
A random pivot splits the array into left and right portions. Consider "good splits": where each portion has between n/4 and 3n/4 elements.
- A random pivot gives a good split with probability ≥ 1/2
- With a good split, we make at least 1/4 progress per level
- Expected levels needed: O(log n)
- Total expected work: O(n log n)
Randomized Selection (QuickSelect) — Las Vegas
Problem: Find the kth smallest element in an unsorted array.
Naive approach: Sort then index — O(n log n) QuickSelect: O(n) expected, O(n²) worst case, but randomization makes worst case astronomically unlikely.
def quickselect(arr, k):
"""
Find kth smallest element (0-indexed k).
Expected O(n) time. Las Vegas — always returns correct answer.
"""
if len(arr) == 1:
return arr[0]
# Random pivot
pivot = random.choice(arr)
# Partition into three groups
lower = [x for x in arr if x < pivot]
equal = [x for x in arr if x == pivot]
upper = [x for x in arr if x > pivot]
if k < len(lower):
return quickselect(lower, k)
elif k < len(lower) + len(equal):
return pivot # kth element is the pivot itself
else:
return quickselect(upper, k - len(lower) - len(equal))
arr = [3, 1, 4, 1, 5, 9, 2, 6, 5, 3, 5]
print(quickselect(arr, 0)) # 1 (minimum)
print(quickselect(arr, 5)) # 4 (median-ish)
print(quickselect(arr, 10)) # 9 (maximum)
Expected O(n) proof: On average, a random pivot eliminates at least half the elements. Total work: n + n/2 + n/4 + ... = 2n = O(n).
Problem: Given a stream of unknown length n, select k items uniformly at random. You cannot store all n items (n might be huge or unknown).
import random
def reservoir_sampling(stream, k):
"""
Select k items from stream uniformly at random.
Each item has exactly k/n probability of being selected.
Time: O(n), Space: O(k)
"""
reservoir = []
for i, item in enumerate(stream):
if i < k:
reservoir.append(item) # Fill reservoir with first k items
else:
# Random index: include new item with probability k/(i+1)
j = random.randint(0, i)
if j < k:
reservoir[j] = item # Replace random existing item
return reservoir
# Test: select 3 from 1000 items — each should have 3/1000 = 0.3% chance
stream = range(1000)
sample = reservoir_sampling(stream, 3)
print(f"Sample of 3 from 1000: {sample}")
# Verify uniformity with many trials:
from collections import Counter
selections = Counter()
for _ in range(100000):
sample = reservoir_sampling(range(10), 3)
for item in sample: selections[item] += 1
print({k: v/100000 for k, v in sorted(selections.items())})
# Should be approximately {0: 0.3, 1: 0.3, ..., 9: 0.3}
Why it works: After seeing i+1 elements, each element has k/(i+1) probability of being in the reservoir. Mathematical induction proves this invariant holds at every step.
Applications: Online analytics, A/B testing, sampling social media feeds, distributed computing where you can't hold all data.
Miller-Rabin Primality Test — Monte Carlo
Problem: Is the number n prime?
Naive: Trial division up to √n — O(√n) — too slow for large n (cryptographic primes are 2048 bits = hundreds of digits).
Miller-Rabin: Probabilistic test — O(k log² n) for k rounds. After k rounds, probability of a composite being declared prime is at most 4^(-k).
def miller_rabin(n, k=10):
"""
Monte Carlo primality test.
k rounds: probability of error ≤ 4^(-k)
k=10: error probability < 1 in 1 million
k=40: error probability < 1 in 10^24
Returns:
True → n is PROBABLY prime (composite with probability ≤ 4^(-k))
False → n is DEFINITELY composite
"""
if n < 2: return False
if n == 2 or n == 3: return True
if n % 2 == 0: return False
# Write n-1 as 2^r * d (factor out powers of 2)
r, d = 0, n - 1
while d % 2 == 0:
r += 1
d //= 2
# k rounds of testing
for _ in range(k):
a = random.randrange(2, n - 1) # Random witness
x = pow(a, d, n) # a^d mod n
if x == 1 or x == n - 1:
continue # Passed this round
for _ in range(r - 1):
x = pow(x, 2, n) # Square x
if x == n - 1:
break
else:
return False # n is composite
return True # n is probably prime
# Test
primes = [2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 97, 997, 9973]
composites = [4, 9, 15, 25, 100, 1000, 9999]
print("Primes:", [miller_rabin(p) for p in primes])
# [True, True, True, True, True, True, True, True, True, True, True, True, True]
print("Composites:", [miller_rabin(c) for c in composites])
# [False, False, False, False, False, False, False]
# Large prime (used in RSA)
large_prime = 2**127 - 1 # Mersenne prime
print(f"2^127 - 1 is prime: {miller_rabin(large_prime)}") # True
Random Walk Algorithms
def random_walk_2d(steps):
"""
Random walk on a 2D grid.
Expected distance from origin after n steps: O(√n)
"""
x, y = 0, 0
directions = [(0,1),(0,-1),(1,0),(-1,0)]
for _ in range(steps):
dx, dy = random.choice(directions)
x += dx
y += dy
distance = (x**2 + y**2) ** 0.5
return x, y, distance
x, y, dist = random_walk_2d(10000)
print(f"Final position: ({x}, {y}), distance from origin: {dist:.1f}")
# Expected ~100 (√10000 = 100)
Applications: Search algorithms, physics simulations, financial modeling, network analysis.
Randomized Hashing to Prevent DoS
import hashlib
import os
class SecureHashMap:
"""
Hash map with random salt to prevent hash collision attacks.
Without randomization: adversary can craft inputs causing O(n²) lookups.
"""
def __init__(self):
self.salt = os.urandom(16) # Random secret salt
self._data = {}
def _hash(self, key: str) -> str:
salted = self.salt + key.encode()
return hashlib.sha256(salted).hexdigest()
def set(self, key: str, value) -> None:
h = self._hash(key)
self._data[h] = (key, value)
def get(self, key: str):
h = self._hash(key)
result = self._data.get(h)
return result[1] if result else None
Python's built-in dict uses hash randomization (PYTHONHASHSEED) for exactly this reason — to prevent adversarial inputs from degrading hash map performance.
When to Use Randomized Algorithms
USE randomized algorithms when
✅ Adversarial inputs are possible (use randomized quicksort)
✅ Streaming data of unknown/large size (reservoir sampling)
✅ Exact computation is too slow (Miller-Rabin vs trial division)
✅ Approximate answers are acceptable (Monte Carlo integration)
✅ Deterministic alternatives are extremely complex (skip lists)
DON'T use when
❌ Output must be deterministically correct every time
❌ Reproducing exact results is required (debugging, testing)
❌ Worst-case guarantees are mandatory (use deterministic alternatives)
Summary
| Algorithm | Type | Expected Time | Correctness |
|---|
| Randomized QuickSort | Las Vegas | O(n log n) | Always correct |
| QuickSelect | Las Vegas | O(n) | Always correct |
| Reservoir Sampling | Las Vegas | O(n) | Always correct |
| Miller-Rabin (k rounds) | Monte Carlo | O(k log² n) | Error ≤ 4^(-k) |
| Random Walk | — | O(n) | — |
Randomized algorithms are a fundamental tool in a programmer's toolkit. QuickSelect, randomized hashing, and reservoir sampling appear in production systems every day. Understanding the distinction between Las Vegas (always correct, random time) and Monte Carlo (bounded time, probabilistic correctness) is essential for choosing the right approach.
*Next: Computational Geometry*