Cairn
data structures · weighted random sampling from a fixed discrete distribution · O(n) build, O(1) per sample

back to Probabilistic

Alias Method

Every other Probabilistic entry on this site spends randomness on a question about one item at a time — has it been seen, how many times, is it similar to another. The Alias Method answers a different kind of question entirely: given n outcomes with arbitrary, fixed weights (a loaded die, a roulette wheel with unequal slices, a word list weighted by frequency), draw repeatedly from that exact distribution, as fast as possible, forever. The two obvious approaches both cost more per draw than they need to: build a running total (a cumulative distribution) and scan it linearly until the target is passed — O(n) per draw — or binary-search that same cumulative array the way Binary Search searches any sorted array — O(log n) per draw, better, but still growing with n. The Alias Method, introduced by Alistair Walker in 1974 and given an O(n)-time construction by Michael Vose in 1991 (Walker's original needed a full sort, O(n log n)), reaches true O(1) per draw, no matter how skewed the weights or how large n is — genuinely optimal, since no algorithm can sample faster than constant time or build a table without looking at every weight at least once.

The trick is a change of shape, not a clever search: instead of one array of cumulative probabilities, build two flat arrays of length nprob[] and alias[]. A draw picks a uniformly random index i in [0, n), flips one biased coin using prob[i], and returns either i itself or the other outcome parked at alias[i]. Every index is checked exactly once, every draw does exactly one random index pick and one coin flip — the weight skew that would slow down a scan or a binary search is entirely absorbed into the construction step, once, and never touched again.

Try it

A fixed 5-outcome distribution — apple, banana, cherry, date, elderberry, weighted 1, 2, 3, 4, 10 (total 20) — walked through Vose's construction one pairing at a time. Each outcome's weight is first rescaled to n·weight/total so the five values average exactly 1: apple 0.25, banana 0.5, cherry 0.75 start in the "small" pile (below 1), date 1.0 and elderberry 2.5 start in "large" (1 or above). Click Next to watch each step pop one from each pile, lock in that small index's row, and hand its leftover probability mass to the large index, which then goes back into whichever pile its new value belongs to.

small pile (scaled share < 1)
large pile (scaled share ≥ 1)
itemweightscaled shareprob[i]alias[i]
Click Next to begin.

Once the table above is complete, it's a working alias table for these five weights. The demo below draws from it directly — one random index, one coin flip, repeated as many times as you like — and tracks the running frequency against the true weighted proportions.

Table not built yet — step through the construction above first.
itemexpectedobserveddraws

How construction works

Reference implementation

class AliasMethod {
  constructor(weights) {
    const n = weights.length;
    const total = weights.reduce((a, b) => a + b, 0);
    const scaled = weights.map(w => (w / total) * n);

    this.prob = new Array(n).fill(1);   // leftovers default to 1, no alias needed
    this.alias = new Array(n).fill(-1);

    const small = [], large = [];
    scaled.forEach((p, i) => (p < 1 ? small : large).push(i));

    while (small.length && large.length) {
      const s = small.pop();
      const l = large.pop();
      this.prob[s] = scaled[s];
      this.alias[s] = l;
      scaled[l] -= (1 - scaled[s]);
      (scaled[l] < 1 ? small : large).push(l);
    }
  }

  sample() {
    const i = Math.floor(Math.random() * this.prob.length);
    return Math.random() < this.prob[i] ? i : this.alias[i];
  }
}

Verified three ways in Node before any page prose was written. (1) Constructing this exact class on the demo's own weights [1, 2, 3, 4, 10] reproduces the shown table exactly: prob = [0.25, 0.5, 0.75, 1, 1], alias = [4, 4, 4, -1, -1]. (2) 300,000 calls to sample() on that table land at 5.01% / 9.92% / 14.95% / 20.11% / 50.01%, against true weighted proportions of 5% / 10% / 15% / 20% / 50%. (3) A separate step-generator that produces the same table one pairing at a time (driving the Try it demo above) was checked to reach the identical final prob[]/alias[] as this class, not just a plausible-looking one — the two are independent implementations of the same algorithm, and disagreeing would have meant one of them was wrong. See /tmp/alias/*.js, scratch, not committed.

Pitfalls

Forgetting to multiply by n silently collapses every draw to uniform — no error, no crash, just the wrong distribution. The scaling step exists specifically so the piles' shared average lands at exactly 1; skip the * n and use raw probabilities (which sum to 1, not to n) instead, and every value except a single outcome holding essentially all the weight ends up under 1 — the "large" pile starts empty, the construction's while loop never runs at all, and every index falls straight through to the "leftover" branch, which sets prob[i] = 1 for all of them. The resulting table has no aliases at all and prob[i] = 1 everywhere, which means the coin flip always returns i itself — sampling degrades into nothing but the uniform random index pick, with the real weights thrown away entirely. Measured directly on the same five weights: a correctly-scaled table's 300,000 draws land at 5.0% / 9.9% / 15.0% / 20.0% / 50.1% as expected; the unscaled table's 300,000 draws land at 20.1% / 20.0% / 20.0% / 20.0% / 20.0% — dead even, exactly what discarding the weights and picking a uniform index would produce. Nothing about the unscaled build throws or looks obviously broken; only a frequency check across many draws exposes it.

itemexpectedcorrect (measured)no-scale bug (measured)
Not run yet — click above.

An alias table doesn't support changing one weight in place — it needs a full rebuild, because every column is coupled to whichever other column it borrowed from. It's tempting to treat a weight update the way a plain cumulative-probability array might: find the one changed outcome and patch just its own entry. But prob[] and alias[] encode the whole distribution jointly — an outcome's column can hold another outcome's leftover mass, and that other outcome's own column has no idea its partner's weight just changed. Patched in place, only the one touched cell changes; every other cell, including any column that happens to alias into the changed index, keeps routing draws exactly as it did under the old weights. Measured: starting from the same [1, 2, 3, 4, 10] table above, then raising elderberry's weight from 10 to 40 (new true proportions 2% / 4% / 6% / 8% / 80%) — a freshly rebuilt table's 300,000 draws land at 2.0% / 4.1% / 6.0% / 8.1% / 79.9%, correctly tracking the new weights, while a table with only prob[4] patched to reflect elderberry's new share still draws 5.1% / 10.0% / 15.0% / 20.1% / 49.9% — indistinguishable from the old distribution, because apple's, banana's, and cherry's own columns still each alias into elderberry using the old, un-rebuilt split. Any update to the underlying weights needs a full O(n) reconstruction, the same cost as building the table the first time — there's no cheaper incremental path.

itemexpected (new weights)rebuilt (measured)naive patch (measured)
Not run yet — click above.

A quieter, honest footnote on the leftover step: the "leftovers land within floating-point error of exactly 1" claim above was checked, not assumed — a sweep from n = 10 up to n = 1,000,000 on random weights found the drift on a leftover index growing with n but staying minuscule throughout (about 4×10-16 at n = 10, about 2.6×10-8 at n = 1,000,000) — never enough to flip which pile an index would belong to, or to leave both piles non-empty at once. That's exactly why the reference implementation above assigns leftovers prob[i] = 1 directly instead of checking scaled[i] === 1 or trusting the arithmetic to land there exactly: the drift is real and measured, just never large enough at any practical n to matter on its own — the robustness comes from not depending on exact equality in the first place, not from the drift happening to stay small.

Where the Alias Method shows up

Complexity

Construction: O(n) time and space — each of the n outcomes is pushed onto exactly one pile once and popped exactly once, with O(1) work per pop (Vose's refinement over Walker's original method, which needed a full O(n log n) sort of the probabilities first). Sampling: O(1) time per draw, always — one uniform integer in [0, n), one coin flip, one conditional branch, regardless of n or how skewed the weights are. That's strictly better than a cumulative-sum array's O(log n) binary-search-per-draw, and the two share the same O(n) space cost, so there's no space trade being made to get there. The one real limitation is exactly the second Pitfall above: no incremental update. Changing any single weight requires rebuilding the whole table from scratch, O(n) again — the Alias Method is the right structure when the distribution is fixed for a long run of draws, and the wrong one when weights change every few samples.

This site's guide, Choosing a Probabilistic Structure, calls this entry out as a standalone fourth question rather than a competitor to the stream-summarization family above — every other entry in the guide builds up state from a stream one item at a time, while this one takes a whole, already-known distribution up front and answers "draw from this, repeatedly, in O(1)."