weighted-random-selection
arrays
algorithm
programming
closed-question

Weighted random selection from array

Master System Design with Codemia

Enhance your system design skills with over 120 practice problems, detailed solutions, and hands-on exercises.

Introduction

Weighted random selection chooses an item with probability proportional to its weight instead of assigning equal chance to every item. This pattern is used in recommendation ranking, game rewards, load balancing, and simulation systems. A robust implementation needs correct probability math, input validation, and predictable behavior under repeated sampling. Choosing the right sampling strategy early prevents subtle fairness and probability bugs in production ranking systems.

Basic Cumulative-Weight Algorithm

The most practical method is cumulative weights plus binary search. Build prefix sums, draw one random number in total range, and locate the first prefix that exceeds the draw.

python
1import bisect
2import random
3
4items = ["small", "medium", "large"]
5weights = [1, 3, 6]
6
7prefix = []
8running = 0.0
9for w in weights:
10    running += w
11    prefix.append(running)
12
13draw = random.random() * running
14idx = bisect.bisect_left(prefix, draw)
15print(items[idx])

This method is easy to audit and works well for many services.

Build a Reusable Sampler Class

For production code, validate weights and precompute state once.

python
1import bisect
2import random
3from typing import Sequence, Any
4
5
6class WeightedSampler:
7    def __init__(self, items: Sequence[Any], weights: Sequence[float]):
8        if len(items) != len(weights):
9            raise ValueError("items and weights must have same length")
10        if len(items) == 0:
11            raise ValueError("items cannot be empty")
12        if any(w < 0 for w in weights):
13            raise ValueError("weights must be non-negative")
14
15        total = sum(weights)
16        if total <= 0:
17            raise ValueError("sum of weights must be positive")
18
19        self._items = list(items)
20        self._prefix = []
21        running = 0.0
22        for w in weights:
23            running += w
24            self._prefix.append(running)
25        self._total = running
26
27    def pick(self, rng: random.Random | None = None):
28        rnd = rng if rng is not None else random
29        draw = rnd.random() * self._total
30        idx = bisect.bisect_left(self._prefix, draw)
31        return self._items[idx]

Using a class avoids rebuilding prefix sums for every call.

Validate Distribution Quality

Weighted randomness should be tested statistically, not by one or two picks.

python
1from collections import Counter
2
3sampler = WeightedSampler(["A", "B", "C"], [2, 3, 5])
4counts = Counter(sampler.pick() for _ in range(100000))
5
6for key in ["A", "B", "C"]:
7    print(key, counts[key] / 100000)

Expected frequencies are roughly 0.2, 0.3, and 0.5. Small drift is normal, but large drift suggests a bug.

Performance Tradeoffs and Alternative Methods

Cumulative plus binary search gives:

  • preprocessing time linear in item count
  • draw time logarithmic in item count
  • memory linear in item count

For very high-frequency draws with static weights, alias method can provide near constant-time sampling after heavier preprocessing. For frequently changing weights, cumulative method is often easier because rebuild cost is low and implementation risk is smaller.

Choose based on workload:

  • stable weights and huge draw volume: alias method may win
  • changing weights or moderate volume: cumulative method is usually best

Deterministic Testing and Reproducibility

Injecting a seeded random generator makes tests stable.

python
rng = random.Random(42)
sampler = WeightedSampler(["x", "y"], [1, 9])
print([sampler.pick(rng) for _ in range(10)])

In analytics pipelines and experiments, seeded randomness is useful for repeatable comparisons between algorithm versions.

Dynamic Weight Updates

If weights change over time, you can rebuild sampler state when updates arrive.

python
1def rebuild_sampler(score_map):
2    items = list(score_map.keys())
3    weights = [max(v, 0.0) for v in score_map.values()]
4    return WeightedSampler(items, weights)
5
6scores = {"item1": 0.8, "item2": 0.1, "item3": 0.4}
7sampler = rebuild_sampler(scores)
8print(sampler.pick())

Keep rebuild logic explicit so data errors such as negative scores are handled consistently.

Numerical Stability Notes

With extremely large or tiny floating-point weights, precision can degrade. Two common mitigations are:

  • normalize weights relative to max value before building prefix sums
  • use integer weights when natural counts are available

These steps are usually unnecessary for ordinary business scores, but they matter in simulation engines with wide value ranges.

Common Pitfalls

A frequent mistake is allowing negative or all-zero weights, which makes probability semantics invalid.

Another mistake is rebuilding cumulative arrays for every draw in a hot path. That destroys throughput.

A third mistake is asserting exact probability values in tiny test samples. Randomness needs large-run checks.

Teams also forget reproducible seeds during tests, leading to flaky pipelines and confusing failures.

Summary

  • Weighted selection picks items proportional to their weights.
  • Cumulative prefix sums plus binary search is a reliable baseline algorithm.
  • Validate input weights and precompute sampler state for repeated draws.
  • Use statistical checks and seeded tests to verify correctness.
  • Match algorithm choice to workload profile and update frequency.

Course illustration
Course illustration

All Rights Reserved.