Weighted Random PickLoading saved progress…

Weighted Random Pick

A weighted random pick chooses one item from a list where each item's chance of being selected is proportional to its weight: an item with weight 3 is picked three times as often as one with weight 1. It is the machinery behind loot drops in games, spreading traffic across servers by capacity, and choosing an A/B test variant by rollout percentage. The standard technique — sometimes called roulette-wheel selection — turns the weights into cumulative sums, then draws a single random point and looks up which item owns it. You will implement it with an injectable random source so the picks are deterministic under test.

Signature

weightedRandom(
  items: T[],          // the values to choose from
  weights: number[],   // weights[i] is the relative chance of items[i]
  rng?: () => number   // returns a float in [0, 1); defaults to Math.random
): T                   // one element of items

Examples

// weights [3, 1, 2] give a, b, c the slices [0,3), [3,4), [4,6) of a 0..6 line.
weightedRandom(['a', 'b', 'c'], [3, 1, 2], () => 0);    // 'a'
weightedRandom(['a', 'b', 'c'], [3, 1, 2], () => 0.62); // 'b'  (point 3.72)
// A zero weight means "never pick me":
weightedRandom(['x', 'y', 'z'], [1, 0, 1], () => 0.5);  // 'z', never 'y'
// Invalid inputs throw instead of guessing:
weightedRandom(['a'], [1, 2]);       // lengths differ
weightedRandom(['a', 'b'], [0, 0]);  // weights sum to zero

Notes

  • Only ratios matter — weights are relative, so [1, 2] and [10, 20] give the same 1-to-2 odds. They do not need to sum to 1.
  • Weights must be finite and non-negative — a negative, NaN, or Infinity weight throws, and so does a set that sums to zero (there would be nothing to pick).
  • Same length, non-emptyitems and weights must be arrays of the same, non-zero length.
  • Zero is allowed — an item with weight 0 is valid input; it simply never wins.
  • Inject the RNG for tests — pass your own rng returning a float in [0, 1) to force a specific pick; it defaults to Math.random. You may assume rng stays in range, as Math.random does.

FAQ

Why use prefix sums and binary search instead of a simple loop?
A linear scan over the weights is also correct and runs in O(n) per pick. Prefix sums are sorted because weights are non-negative, so a binary search finds the chosen item in O(log n). If you draw many times from the same table, build the prefix sums once and reuse them so each pick stays fast.
How are zero-weight items handled?
They can never be picked. A weight of 0 adds nothing to the running total, so its prefix sum equals the previous one, forming a zero-width interval. Because the lookup takes the first prefix strictly greater than the random point, no draw can ever land on that empty slice.
Why inject the random generator instead of calling Math.random directly?
Passing rng as a parameter that defaults to Math.random makes the function deterministic and testable: a test can supply a fixed value like () => 0.5 to force a specific pick and assert on it. In production you omit the argument and get Math.random.
Loading editor…