Sampling and summarizing data that arrives as a stream: unknown length, one pass, memory far smaller than the data. If you do know the length and have the data, simply shuffle and take the first , or pick random indices.
| Problem | Algorithm | Space | Guarantee |
|---|---|---|---|
| uniform sample of | reservoir sampling (Algorithm R; Algorithm L skips ahead) | exact uniformity | |
| weighted sample of (without replacement) | A-Res: keep the largest keys | exact | |
| distinct indices from | Floyd’s algorithm | exact | |
| frequent items (heavy hitters) | Misra–Gries ( counters) | undercounts by | |
| frequency of any item | Count–Min sketch | overcounts by w.p. | |
| membership | Bloom filter | bits | no false negatives; false positives |
| count distinct elements | HyperLogLog | small registers ( bits) | relative error |
| approximate quantiles | t-digest, GK summary | rank error | |
| majority element | Boyer–Moore voting | exact if a strict majority exists: Classic Array Problems |
| When reservoir sampling applies | Why |
|---|---|
| input length unknown in advance | cannot pre-allocate or index |
| data does not fit in memory, single pass | space, streaming |
| sampling log lines, telemetry, network packets | the standard production use |
| random element of a linked list, random node during a DFS | one traversal, |
| random element of a filtered subset | apply the filter while streaming |
| distributed data | each machine keeps a reservoir and its count; merge by sampling from the reservoirs weighted by their counts |
Reservoir sampling and exact samplers
template<class T> vector<T> reservoirR(const vector<T>& stream, int k, mt19937_64& rng) { // Algorithm R
vector<T> res;
for (ll i = 0; i < sz(stream); i++) {
if (i < k) res.push_back(stream[i]);
else { ll j = uniform_int_distribution<ll>(0, i)(rng); if (j < k) res[j] = stream[i]; } // keep w.p. k/(i+1)
}
return res;
}
template<class T> vector<T> reservoirL(const vector<T>& stream, int k, mt19937_64& rng) { // Algorithm L (Li)
uniform_real_distribution<double> U(0, 1); int n = sz(stream);
vector<T> res(stream.begin(), stream.begin() + min(k, n));
if (n <= k) return res;
auto u = [&] { double x; do x = U(rng); while (x == 0); return x; };
double W = exp(log(u()) / k); ll i = k - 1;
while (true) {
i += (ll)floor(log(u()) / log1p(-W)) + 1; // skip a geometric number of items
if (i >= n) break;
res[rng() % k] = stream[i];
W *= exp(log(u()) / k);
}
return res;
}
vector<int> weightedReservoir(const vector<double>& w, int k, mt19937_64& rng) { // A-Res: indices, prob. ~ weight
uniform_real_distribution<double> U(0, 1);
priority_queue<pair<double, int>, vector<pair<double, int>>, greater<>> pq; // min-heap of keys
for (int i = 0; i < sz(w); i++) {
double key = pow(U(rng), 1.0 / w[i]);
if (sz(pq) < k) pq.push({key, i}); else if (key > pq.top().first) pq.pop(), pq.push({key, i});
}
vector<int> res; while (!pq.empty()) res.push_back(pq.top().second), pq.pop();
return res;
}
vector<ll> floydSample(ll n, int k, mt19937_64& rng) { // k distinct uniform values of [0, n), O(k) expected
unordered_set<ll> S;
for (ll j = n - k; j < n; j++) { ll t = uniform_int_distribution<ll>(0, j)(rng); if (!S.insert(t).second) S.insert(j); }
return vector<ll>(all(S));
}Proof: Algorithm R, the k = 1 case, A-Res and Floyd are exact
Algorithm R. Claim: after items each is in the reservoir with probability . Induction: true for . Item enters with probability . An earlier item, present with probability , survives if the new item is rejected () or accepted into another slot (): . The same induction shows every -subset is equally likely. For : keep the -th item (0-based) with probability .
Algorithm L produces the same distribution: in Algorithm R the reservoir is the items with the smallest of independent uniform labels; tracks the -th smallest label so far (distributed as the max of uniforms, hence the update by ), and the number of items until a label falls below is geometric with parameter , sampled directly. It draws random numbers instead of .
A-Res (Efraimidis and Spirakis). . For , item has the largest key with probability ; conditioning on the winner and repeating gives weighted sampling without replacement for .
Floyd. Claim: after the iteration for , is a uniform random subset of of size , where . Assume is a uniform -subset of and is uniform in . A given -subset of containing arises from with : values of . A subset not containing arises from and , for each of the elements . Either way the probability is , the same for every . Checked by frequency tests: all four samplers match the exact inclusion probabilities within statistical error.
Sketches
ull splitmix(ull x) { x += 0x9e3779b97f4a7c15ULL; x = (x ^ (x >> 30)) * 0xbf58476d1ce4e5b9ULL; x = (x ^ (x >> 27)) * 0x94d049bb133111ebULL; return x ^ (x >> 31); }
struct MisraGries { // heavy hitters with k counters
int k; map<ll, ll> c;
MisraGries(int k) : k(k) {}
void add(ll x) {
if (c.count(x) || sz(c) < k) { c[x]++; return; }
for (auto it = c.begin(); it != c.end();) { if (--it->second == 0) it = c.erase(it); else ++it; } // decrement all
}
ll estimate(ll x) const { auto it = c.find(x); return it == c.end() ? 0 : it->second; } // f - n/(k+1) <= est <= f
};
struct CountMin { // width w ~ e / eps, depth d ~ ln(1 / delta)
int w, d; vector<vector<ll>> t; vector<ull> seed;
CountMin(int w, int d, ull s) : w(w), d(d), t(d, vector<ll>(w, 0)), seed(d) { for (int i = 0; i < d; i++) seed[i] = splitmix(s + i); }
void add(ll x, ll c = 1) { for (int i = 0; i < d; i++) t[i][splitmix(x ^ seed[i]) % w] += c; }
ll estimate(ll x) const { ll r = LLONG_MAX; for (int i = 0; i < d; i++) r = min(r, t[i][splitmix(x ^ seed[i]) % w]); return r; }
};
struct Bloom { // m bits, k hash functions (double hashing)
int m, k; vector<ull> bits; ull s;
Bloom(int m, int k, ull s) : m(m), k(k), bits((m + 63) / 64), s(s) {}
void add(ll x) { ull h1 = splitmix(x ^ s), h2 = splitmix(h1) | 1; for (int i = 0; i < k; i++) { ull b = (h1 + i * h2) % m; bits[b >> 6] |= 1ULL << (b & 63); } }
bool maybe(ll x) const { ull h1 = splitmix(x ^ s), h2 = splitmix(h1) | 1; for (int i = 0; i < k; i++) { ull b = (h1 + i * h2) % m; if (!(bits[b >> 6] >> (b & 63) & 1)) return false; } return true; }
};
struct HyperLogLog { // m = 2^b registers
int b, m; vector<uint8_t> reg; ull s;
HyperLogLog(int b, ull s) : b(b), m(1 << b), reg(1 << b, 0), s(s) {}
void add(ll x) { ull h = splitmix(x ^ s); int idx = h >> (64 - b); ull rest = h << b; int r = rest ? __builtin_clzll(rest) + 1 : 64 - b + 1; reg[idx] = max<int>(reg[idx], r); }
double estimate() const {
double sum = 0; int zeros = 0;
for (int r : reg) sum += ldexp(1.0, -r), zeros += r == 0;
double alpha = 0.7213 / (1 + 1.079 / m), E = alpha * m * m / sum;
if (E <= 2.5 * m && zeros) E = m * log((double)m / zeros); // small-range correction (linear counting)
return E;
}
};Proof: Misra–Gries, Count–Min and the Bloom filter bound
Misra–Gries. A counter is only incremented for its own item, so estimates never exceed true counts. Each “decrement all” step discards occurrences (the new item and one from each of the counters), so there are at most such steps, and an item loses at most one count per step: . Hence every item with survives (and a second pass can verify candidates).
Count–Min. Every row counts plus the colliding items, so each row overestimates. In one row, the expected collision mass is at most (pairwise independent hashing), so by Markov for ; the minimum over independent rows fails with probability .
Bloom filter. An inserted element sets all its bits, so there are no false negatives. Assuming independent uniform hash positions, a fixed bit stays 0 after insertions with probability , so a non-member’s bits are all 1 with probability about , minimized at , where it is (about 1% at 9.6 bits per element). HyperLogLog’s estimator and its standard error are the analysis of Flajolet, Fusy, Gandouet and Meunier (2007); here it was checked empirically (errors within a few standard errors on random streams). Count–Min and Misra–Gries were checked for their deterministic bounds, the Bloom filter for zero false negatives and a false-positive rate near the formula.
Related notes
- Randomized Algorithms
- Randomized Hashing and Verification
- Classic Array Problems (Boyer–Moore voting)