In reservoir sampling over a stream of unknown length, why keep item i with probability k/i?
answer
- you never learn the length in advance
- what must hold after every item?
- each item seen so far, equally likely
- the draw range grows with the count
- survival times k/(i-1) must give k/i
basics
~20 sKeeping the i-th item with probability k/i, and evicting a uniformly chosen resident, holds the invariant that after i items each is held with probability k/i. That holds at every prefix, so the length is never needed.
solid answer
~50 sFill the reservoir with the first k items, then for the i-th item — counting from one — keep it with probability `k/i` and, when kept, drop one of the k residents chosen uniformly. The invariant is that after processing i items, every one of them sits in the reservoir with probability exactly `k/i`, so the sample is uniform at every prefix of the stream, not only at the end. The induction is short: a resident survives item i either because the arrival is rejected, with probability `1 - k/i`, or because it is accepted but evicts someone else, with probability `(k/i)(k-1)/k`; together that is `(i-1)/i`, and `k/(i-1) x (i-1)/i = k/i`. What you buy is the cost profile: one pass, O(k) memory, O(1) work per item, and no advance knowledge of the length. The boundary people fumble is where the rule starts — items 1 through k are admitted unconditionally, and the probabilistic rule begins at item k+1, the first index where `k/i` drops below one.
code
pseudocode · 12 lines// reservoir holds k items; the stream length is never known
for i in 1..k:
reservoir[i] = next item of stream
i = k
while stream has more items:
i = i + 1
x = next item of stream
j = random_int(1, i) // inclusive at both ends
if j <= k: // happens with probability k/i
reservoir[j] = x // the victim slot is uniform among k
...go deeper
Know the shape: admit the first k items, then let each later arrival replace a random resident with a shrinking chance. Be able to state that memory is O(k) and that the stream is read exactly once.
Expect to justify k/i out loud with the survival argument, and to say where the probabilistic rule starts — item k+1, not item 1. Trace the single-item case by hand to check yourself.
Show the operational side: sampling live traffic without buffering, merging per-shard reservoirs weighted by their item counts, and recording the seed so a contested draw can be replayed exactly.
Decide whether a sample is the right answer at all. It buys bounded memory and single-pass I/O at the cost of exactness, so name what the business needs — an auditable exact draw, or a bounded-cost estimate over an unbounded stream.
## The problem the technique exists for You need k items drawn uniformly at random from a sequence whose length you will not know until it ends — or which never ends. A prize draw over a live clickstream is the shape: entries arrive one at a time, you cannot buffer them all, and at any moment someone may ask for the current winners. "Uniformly" here means that at the instant you are asked, after n items have gone past, every one of those n items is equally likely to be in your set of k, and every k-subset is equally likely. The obvious approaches both fail the constraints. Buffering the stream and choosing at the end costs memory proportional to the stream and cannot answer early. Counting the stream in a first pass and then picking k indices in a second pass needs a replayable, finite stream, which live traffic is not, and doubles the I/O when the data sits on remote storage. ## The algorithm Admit the first k items unconditionally. From then on, for the i-th item overall, keep it with probability `k/i`; if kept, choose one of the k residents uniformly and replace it. ``` for i in 1..k: reservoir[i] = next item of stream i = k while stream has more items: i = i + 1 x = next item of stream j = random_int(1, i) if j <= k: reservoir[j] = x ``` The single draw does double duty: `j <= k` happens with probability `k/i`, which is the keep decision, and the value of `j` names the victim uniformly among the k slots. ## Why k/i is the right number The invariant is: **after i items have been processed, each of those i items is in the reservoir with probability k/i.** It holds trivially at i = k, where all k admitted items are present with probability 1 = k/k. Assume it at i-1 and take the arrival of item i. The new item enters with probability `k/i` by construction, which is what the invariant demands of it. An existing resident survives in two disjoint ways: the arrival is rejected, probability `1 - k/i`; or the arrival is accepted, probability `k/i`, but the victim is one of the other k-1 slots, probability `(k-1)/k`. Adding them gives `1 - k/i + (k-1)/i = 1 - 1/i = (i-1)/i`. An item that was present with probability `k/(i-1)` is therefore present afterwards with probability `k/(i-1) x (i-1)/i = k/i`. The invariant is restored, and it holds after every single item — which is exactly why the length never has to be known. Stop the stream anywhere and the sample you are holding is uniform for the prefix you saw. The k = 1 case is worth tracing by hand: keep item i with probability `1/i`, replacing whatever you hold. After 3 items each has probability 1/3; the third by construction, the second as `(1/2)(2/3)`, the first as `(1)(1/2)(2/3)`. ## The boundaries that break it Three off-by-ones account for most broken implementations. Starting the probabilistic rule at item 1 instead of item k+1 is meaningless for i below k, where `k/i` exceeds one, and under-fills the reservoir. Counting i from zero rather than one shifts every probability by one item and biases the early arrivals. And evicting a fixed slot — the oldest, or slot 0 — rather than a uniformly chosen one destroys the survival term in the induction, producing a sample skewed toward recent items. A fourth is subtler: the reservoir must be reset per stream. Carrying one across two streams silently samples their concatenation. ## The cost lens One pass, O(k) memory regardless of stream length, O(1) work per item, and an answer available at any moment. The random draws dominate the per-item constant, and there is a refinement that removes most of them: rather than drawing once per item, draw how many items to *skip* before the next acceptance, since acceptances become rare as i grows. The expected number of replacements over n items is proportional to `k log(n/k)`, so the skip-based form does that many draws instead of n. ## Variants worth naming Weighted sampling drops out of a change of key: give each item the key `u^(1/w)` with u drawn uniformly from (0,1) and w the item's weight, keep the k largest keys in a size-k structure, and you have a one-pass weighted sample at O(log k) per item. Sharding works by bookkeeping: each shard runs its own reservoir and reports both its sample and how many items it saw. To merge, draw from the shard reservoirs in proportion to those counts. Without the counts the merge is wrong, because a shard that saw a thousand items and one that saw ten would contribute equally. Finally, the sample is random but should not be unrepeatable. Recording the seed alongside the result lets a disputed prize draw be replayed exactly, which is usually the difference between a technique and a technique you are allowed to ship.
- Why not count the stream first and then pick k random positions?That needs two passes and a replayable, finite stream. Live traffic cannot be rewound, an unbounded stream has no end to count to, and when the data lives on remote storage a second pass doubles the I/O bill. Reservoir sampling also answers at any moment: stop it after any prefix and the sample you hold is already uniform for what you have seen.
- What changes when items carry weights?Give each item the key `u^(1/w)`, with u uniform in (0,1) and w its weight, and keep the k largest keys in a size-k structure. Heavier items get larger keys in expectation, so they survive more often. It is still one pass and O(k) memory, at O(log k) per item instead of O(1).
- How do you run this across shards and merge the results?Each shard keeps its own reservoir and also reports how many items it saw. The merge draws from the shard reservoirs in proportion to those counts, which reconstructs a uniform sample of the union. Merging without the counts is the common bug: a shard that saw a thousand items would contribute as much as one that saw ten.
A raffle drum that only holds k tickets: each new entrant gets a k/i chance of pushing out a randomly chosen ticket already inside, so nobody's odds depend on how early they arrived.
saying these in an interview costs you the question
- You must know the stream length to sample uniformly
- Keep each item with probability k/n using an estimated n
- The first k items end up over-represented
- Applying the k/i rule from the very first item is fine
- Always evict the oldest resident; it is simpler