← Applied algorithms
Chance and live data When setup buys speed

Alias method

Preprocess the table, then draw in constant time.

A game chest has Common, Rare, Epic, and Legendary loot with weights 50, 25, 15, and 10. One weighted draw is easy; a stream of thousands should not rescan the whole table every time.

The alias method pays O(n) once to fill equal-width columns. Each later draw chooses a column and checks one threshold, preserving the original weights with O(1) table work.

TypeScriptGoOne weighted sampler in each language

01 / The idea

A weighted choice can be cheap to set up and expensive to repeat.

A cumulative scan draws a random number from 0 through 99, then walks Common, Rare, Epic, and Legendary until the cumulative weight passes it. That is O(n) work for every chest opening. The usual fix is to store the running totals once and binary-search them, which brings each draw down to O(log n).

The alias method turns the same distribution into equal-width columns, each with a threshold and possibly an alternate outcome. A draw now does two bounded choices and at most one alias jump.

The result is still random. The optimization changes where the work happens: preprocessing knows the weights so the hot draw loop does not need to, not even for a binary search.

alias method

Preprocess the table, then draw in constant time.

LOOT TABLE · INTEGER WEIGHTSRaw weights
10050Common25Rare15Epic10Legendary
01/ 08
Read the weighted outcomes

Give every loot outcome a weight.

Common has weight 50, Rare 25, Epic 15, and Legendary 10, out of 100. A scan could draw them, but every draw would walk the weights again.

Reduced motion: choose a scene to see its completed state.

Read this scene

Common has weight 50, Rare 25, Epic 15, and Legendary 10, out of 100. A scan could draw them, but every draw would walk the weights again.

Four loot outcomes with whole-number weights totaling 100: Common 50, Rare 25, Epic 15, Legendary 10. No columns built yet.

Watch and Step through replay the lesson’s own construction trace, one pairing at a time, then one seeded draw decision and a thousand counts. Try it lets you pick or type the weights, then builds and samples with the same alias table code.

Step through the three pairings, one draw that follows an alias, and the counts from a thousand seeded draws. Rare is the one to watch: it starts on the large list, lends 60 to Legendary, drops to 40, and is topped up from Common in its turn. The units are whole numbers so TypeScript and Go make the same decisions.

02 / Name the rule

Make every column exactly one bucket wide.

With n outcomes and total weight W, scale each weight by n. A scaled value below W is small; one at or above W is large. Pair a small column with a large one: keep the small value as its threshold and use the large outcome as its alias for the leftover.

draw = column; result = coin < threshold ? column : alias[column]

The large column gives exactly what the small one was missing, so the large one shrinks by that much. What it has left decides which list it goes back on: below W it is small now, and it will be topped up in its turn; at or above W it stays large and can lend again. Each pairing fills one column for good, so the work stops when the small list is empty, and every outcome still owns exactly n times its weight across the columns.

Alastair Walker described the alias method in 1974. The small-and-large pairing used here is Michael Vose’s 1991 construction, which builds the table in linear time.

Scale

Equalize the buckets

Multiply every weight by the number of outcomes so each column has the same total capacity.

Pair

Fill the leftover

Give a small column its own threshold and borrow the missing part from a large column.

Draw

Accept or jump

One coin checks the threshold. A rejection follows the stored alias exactly once.

When cumulative weights are enoughThe trade is setup for repeated draws

A cumulative scan is simpler and often the right choice for a small or changing table. Prefix sums with a binary search are the common middle ground: O(n) to build, O(log n) per draw, and a single weight change only means rebuilding the totals. With four outcomes, as here, none of the three is measurably faster; the alias table starts to pay when there are many outcomes and many draws. The alias method pays extra memory and rebuild work when the same fixed weights will be sampled many times. It does not make arbitrary weight updates free: changing a weight means rebuilding or choosing a different dynamic sampler.

03 / Read the shape

The table stores probability work for the future.

Basic form validates non-negative integer weights and builds the columns by pairing small and large ones. traceAliasTable records each pairing as it goes, which is what the film replays; buildAliasTable keeps only the table. In the wild draws from a seeded source, accepts the column or follows its alias, and counts many draws. At the call site preprocesses a loot table, prints each pairing, and counts many outcomes without walking the weights per draw. Both languages print the same five lines.

Validate integer weights and build equal-width threshold/alias columns with a stack of small and large outcomes.

TypeScriptReading
loot.ts
export type AliasErrorCode =
	| 'empty-table'
	| 'too-big'
	| 'bad-id'
	| 'duplicate-id'
	| 'bad-weight'
	| 'zero-total'
	| 'bad-draw-count'
	| 'bad-seed'
	| 'bad-bound';

export class AliasError extends Error {
	readonly code: AliasErrorCode;

	constructor(code: AliasErrorCode, message: string) {
		super(message);
		this.name = 'AliasError';
		this.code = code;
	}
}

export interface LootWeight {
	id: string;
	weight: number;
}

export interface AliasEntry {
	probability: number;
	alias: number;
}

export interface AliasTable {
	items: LootWeight[];
	totalWeight: number;
	entries: AliasEntry[];
}

function validate(weights: LootWeight[]): number {
	if (weights.length === 0)
		throw new AliasError('empty-table', 'Alias table needs at least one outcome.');
	if (weights.length > MAX_OUTCOMES) {
		throw new AliasError('too-big', `Alias table is limited to ${MAX_OUTCOMES} outcomes.`);
	}
	const seen = new Set<string>();
	let total = 0;
	for (const item of weights) {
		if (item.id.length > MAX_ID_LENGTH || !ID.test(item.id)) {
			throw new AliasError('bad-id', 'Outcome ids must be lowercase slugs.');
		}
		if (seen.has(item.id)) throw new AliasError('duplicate-id', 'Outcome ids must be unique.');
		seen.add(item.id);
		if (!Number.isInteger(item.weight) || item.weight < 0 || item.weight > MAX_WEIGHT) {
			throw new AliasError('bad-weight', `Weights must be whole numbers from 0 to ${MAX_WEIGHT}.`);
		}
		total += item.weight;
	}
	if (total <= 0)
		throw new AliasError('zero-total', 'At least one outcome must have a positive weight.');
	return total;
}

/** One pairing: a short column keeps its own height and borrows the rest from a tall one. */
export interface AliasStep {
	small: number;
	large: number;
	threshold: number;
	borrowed: number;
	largeBefore: number;
	largeAfter: number;
	largeBecomes: 'small' | 'large';
}

export interface AliasTrace {
	scaled: number[];
	small: number[];
	large: number[];
	steps: AliasStep[];
}

export function traceAliasTable(weights: LootWeight[]): { table: AliasTable; trace: AliasTrace } {
	const totalWeight = validate(weights);
	const items = weights.map((item) => ({ ...item }));
	const scaled = items.map((item) => item.weight * items.length);
	const small: number[] = [];
	const large: number[] = [];
	for (const [index, value] of scaled.entries()) (value < totalWeight ? small : large).push(index);
	const trace: AliasTrace = {
		scaled: [...scaled],
		small: [...small],
		large: [...large],
		steps: []
	};
	const entries = items.map(() => ({ probability: totalWeight, alias: -1 }));
	while (small.length > 0 && large.length > 0) {
		const smallIndex = small.pop()!;
		const largeIndex = large.pop()!;
		const borrowed = totalWeight - scaled[smallIndex];
		entries[smallIndex] = { probability: scaled[smallIndex], alias: largeIndex };
		const largeBefore = scaled[largeIndex];
		scaled[largeIndex] -= borrowed;
		const largeBecomes = scaled[largeIndex] < totalWeight ? 'small' : 'large';
		(largeBecomes === 'small' ? small : large).push(largeIndex);
		trace.steps.push({
			small: smallIndex,
			large: largeIndex,
			threshold: scaled[smallIndex],
			borrowed,
			largeBefore,
			largeAfter: scaled[largeIndex],
			largeBecomes
		});
	}
	// Anything left in either list is already exactly one column tall.
	return { table: { items, totalWeight, entries }, trace };
}

export function buildAliasTable(weights: LootWeight[]): AliasTable {
	return traceAliasTable(weights).table;
}
GoAlongside
loot.go
type LootWeight struct {
	ID     string
	Weight int
}

type AliasEntry struct {
	Probability int
	Alias       int
}

type AliasTable struct {
	Items       []LootWeight
	TotalWeight int
	Entries     []AliasEntry
}

type AliasError struct {
	Code    string
	Message string
}

func (e *AliasError) Error() string { return e.Message }

func validate(weights []LootWeight) (int, error) {
	if len(weights) == 0 {
		return 0, &AliasError{Code: "empty-table", Message: "Alias table needs at least one outcome."}
	}
	if len(weights) > 32 {
		return 0, &AliasError{Code: "too-big", Message: "Alias table is limited to 32 outcomes."}
	}
	seen := map[string]bool{}
	total := 0
	for _, item := range weights {
		if len(item.ID) == 0 || len(item.ID) > 24 || item.ID[0] < 'a' || item.ID[0] > 'z' {
			return 0, &AliasError{Code: "bad-id", Message: "Outcome ids must be lowercase slugs."}
		}
		for _, character := range item.ID[1:] {
			if !((character >= 'a' && character <= 'z') || (character >= '0' && character <= '9') || character == '-') {
				return 0, &AliasError{Code: "bad-id", Message: "Outcome ids must be lowercase slugs."}
			}
		}
		if seen[item.ID] {
			return 0, &AliasError{Code: "duplicate-id", Message: "Outcome ids must be unique."}
		}
		seen[item.ID] = true
		if item.Weight < 0 || item.Weight > 1_000_000 {
			return 0, &AliasError{Code: "bad-weight", Message: "Weights must be whole numbers from 0 to 1000000."}
		}
		total += item.Weight
	}
	if total <= 0 {
		return 0, &AliasError{Code: "zero-total", Message: "At least one outcome must have a positive weight."}
	}
	return total, nil
}

// AliasStep is one pairing: a short column keeps its own height and borrows the rest from a tall one.
type AliasStep struct {
	Small        int
	Large        int
	Threshold    int
	Borrowed     int
	LargeBefore  int
	LargeAfter   int
	LargeBecomes string
}

type AliasTrace struct {
	Scaled []int
	Small  []int
	Large  []int
	Steps  []AliasStep
}

func traceAliasTable(weights []LootWeight) (AliasTable, AliasTrace, error) {
	total, err := validate(weights)
	if err != nil {
		return AliasTable{}, AliasTrace{}, err
	}
	items := append([]LootWeight(nil), weights...)
	scaled := make([]int, len(items))
	small, large := []int{}, []int{}
	for index, item := range items {
		scaled[index] = item.Weight * len(items)
		if scaled[index] < total {
			small = append(small, index)
		} else {
			large = append(large, index)
		}
	}
	trace := AliasTrace{
		Scaled: append([]int(nil), scaled...),
		Small:  append([]int{}, small...),
		Large:  append([]int{}, large...),
		Steps:  []AliasStep{},
	}
	entries := make([]AliasEntry, len(items))
	for index := range entries {
		entries[index] = AliasEntry{Probability: total, Alias: -1}
	}
	for len(small) > 0 && len(large) > 0 {
		smallIndex := small[len(small)-1]
		small = small[:len(small)-1]
		largeIndex := large[len(large)-1]
		large = large[:len(large)-1]
		borrowed := total - scaled[smallIndex]
		entries[smallIndex] = AliasEntry{Probability: scaled[smallIndex], Alias: largeIndex}
		largeBefore := scaled[largeIndex]
		scaled[largeIndex] -= borrowed
		largeBecomes := "large"
		if scaled[largeIndex] < total {
			largeBecomes = "small"
			small = append(small, largeIndex)
		} else {
			large = append(large, largeIndex)
		}
		trace.Steps = append(trace.Steps, AliasStep{
			Small:        smallIndex,
			Large:        largeIndex,
			Threshold:    scaled[smallIndex],
			Borrowed:     borrowed,
			LargeBefore:  largeBefore,
			LargeAfter:   scaled[largeIndex],
			LargeBecomes: largeBecomes,
		})
	}
	// Anything left in either list is already exactly one column tall.
	return AliasTable{Items: items, TotalWeight: total, Entries: entries}, trace, nil
}

func buildAliasTable(weights []LootWeight) (AliasTable, error) {
	table, _, err := traceAliasTable(weights)
	return table, err
}
Reading the TypeScriptSmall and large stacks

The small and large lists are plain arrays used as stacks: pop takes the most recent index, and a large column that drops below the total is pushed onto small. An entry stores a threshold in the same integer units as the total and an alias index, -1 when the column needs none.

Each AliasStep names the two columns, the threshold, what was borrowed, the large column’s height before and after, and the list it went back on. largeBecomes is a string union, 'small' | 'large'. Errors are an AliasError whose code matches the Go version.

Reading the GoThe same seeded decisions

Go uses uint32 state with the same linear-congruential constants and rejection-safe bounded draws. It copies the input weights before building the table and, like the TypeScript, keeps the first 12 draws for inspection.

traceAliasTable returns the table, the trace, and an error. The lists are slices, popped by reslicing to len - 1, and LargeBecomes is the string "small" or "large". Errors are an *AliasError with the same codes.

What is refusedA valid distribution

Both versions require 1 to 32 outcomes (empty-table, too-big), unique lowercase ids (bad-id, duplicate-id), whole-number weights from 0 to 1,000,000 (bad-weight), and a positive total (zero-total). Zero-weight outcomes may stay in the table; an all-zero table can’t define a distribution. A draw count must be a whole number from 0 to 100,000 (bad-draw-count).

04 / Try a decision

What does a draw need after preprocessing?

The alias table has already paid for the distribution. Follow one draw through a column that has an alias.

In the dungeon table, Legendary’s column has threshold 40/100 and alias Rare. A draw picks Legendary’s column and weight unit 99. Which item comes out?

05 / Follow the cost

Pay once when the same weights will be drawn often.

Alias method: time and extra space per operation
OperationTimeExtra spaceWhat it assumes
Validate and total weightsO(n)O(1)Read every outcome once and require a positive total weight.
Build threshold and alias columnsO(n)O(n)Each pairing fills one short column for good, so there are at most n − 1 of them.
Draw after preprocessingO(1)O(1)Choose a column, draw one weight unit, then accept or follow one alias.
Store the sampler—O(n)Keep one item list and one threshold/alias entry per outcome.

With n outcomes, preprocessing and storage are O(n), and each later draw is O(1). Prefix sums with a binary search cost the same O(n) setup and O(log n) per draw. The comparison is about repeated work, not a promise that the table is always faster: for a tiny or frequently changing distribution, a cumulative scan may be simpler and cheaper overall.

Integer units make the teaching fixture exact. Production samplers should also define their random source, bounded-draw policy, weight precision, and rebuild behavior so rounding and updates are not left implicit.

06 / Give it a real job

Build the drop table when it ships, not when a chest opens.

A game’s drop service owns the loot tables. Designers change weights between releases, not between chest openings, so the service builds one alias table per drop table when a new version is deployed and keeps it in memory. Every chest opened after that is one call to draw: a column, a unit, and at most one jump, however many items the table holds. When the next version ships, it builds the new table first and then swaps it in, so no chest ever draws from a half-built one.

What it leaves out is anything that changes the chances between builds. A “guaranteed Legendary within 50 chests” rule isn’t in the table; it’s a separate check before the draw, and it changes what players actually get, so it belongs in the published odds too. The service also keeps the random source to itself. This runs on the game server that owns the drop table; the client only shows what came out, because a draw made in the player’s browser is a draw the player can repeat.

The same trade shows up in general-purpose libraries. R’s sample(), drawing with replacement, switches to Walker’s alias method “when there are more than 200 reasonably probable values.” Rust’s rand_distr crate ships a WeightedAliasIndex whose documentation describes this lesson’s draw: “Sampling is O(1), it makes a call to Uniform<u32>::sample and a call to Uniform<W>::sample.” One number picks the column; the other is the unit.

07 / Make the call

Choose setup, update, and draw costs together.

Use the alias method for a fixed weighted distribution with many independent draws: loot tables, weighted simulation events, or a recommendation experiment with a stable batch of arms.

Use Fisher–Yates when every item should be equally likely in a permutation. Use reservoir sampling when the data arrives as a stream and the goal is a fair sample, not a weighted one.

Three boundaries to rememberSimilar randomness, different guarantees
  • Uniform shuffle: Fisher–Yates selects positions without replacement.
  • Weighted draw: Alias preserves fixed outcome weights with replacement.
  • Changing weights: rebuild the table or choose a data structure designed for updates.
SourcesTwo papers and two libraries, checked 23 September 2026

For the two papers, only the bibliographic records were checked, not the papers.

08 / Take the idea with you

Explain a weighted draw without saying “alias method.”

“Cut the odds into equal columns, one per outcome. Each column is mostly one outcome, and whatever room is left over belongs to one other outcome. To draw, pick a column at random, then a height in it: below the line you get the column’s own outcome, above it you get the other one.”

Before moving on, find a weighted choice in your work: a loot table, a traffic split, a sampling rate per event type. Ask how often its weights change and how often it is drawn from, and whether anything draws from it in a loop hot enough to care.

Connections to follow nextRelated lessons