DSA DSA In Depth
0%
lesson count-min-sketchstreamingprobabilistic

Count-Min Sketch & Streaming Frequency Estimation Error Bounds

7 min read Module 2.1: Hash Tables, Modern Probing & Probabilistic Sketches Updated Oct 23, 2026
📄 Cheat Sheet
Count-Min Sketch: Streaming Frequency Estimation & Error Bounds
Ben Langmead Count-Min Sketch: Streaming Frequency Estimation & Error Bounds Watch on YouTube ↗

Erik Demaine proves the (ε, δ) Markov inequality bound for Count-Min Sketch, shows why pairwise independence suffices, and demonstrates dyadic range heavy-hitter queries.

0:00 / --:--

Architecture & $(\varepsilon, \delta)$ Markov Proof of the Count-Min Sketch

Given a stream of updates $(x_t, c_t)$ with non-negative increments $c_t > 0$ and total stream weight $N = \sum_t c_t$, a Count-Min Sketch allocates a 2D array $C[0..d-1][0..w-1]$ of integer counters initialized to $0$, paired with $d$ hash functions $h_0, \dots, h_{d-1}: \mathcal{U} \to \{0, \dots, w-1\}$ chosen independently from a pairwise-independent (2-universal) family.

  • update(x, c): For each row $j \in \{0, \dots, d-1\}$, increment $C[j][h_j(x)] \mathrel{+}= c$.
  • estimate(x): Return $\hat{f}_x = \min_{0 \le j < d} C[j][h_j(x)]$. Because all increments are non-negative, every row $C[j][h_j(x)]$ equals the true frequency $f_x$ plus colliding traffic from other keys, so $\hat{f}_x \ge f_x$ always holds (one-sided overestimation error).
  • Conservative Update (Estan & Varghese, 2002): For positive-only streams, instead of blindly incrementing all $d$ rows, first compute the current estimate $\hat{f}_x = \min_j C[j][h_j(x)]$, set the new target $\text{new\_val} = \hat{f}_x + c$, and update each row to $C[j][h_j(x)] \gets \max(C[j][h_j(x)], \text{new\_val})$. This strictly preserves $\hat{f}_x \ge f_x$ while cutting empirical overestimation error by $2\times$ to $5\times$!
Streaming Algorithm Space Complexity Update / Query Time Error Guarantee ($N = \|f\|_1$) Supports Negative Updates?
Count-Min Sketch (CMS) $O\left(\frac{1}{\varepsilon} \ln \frac{1}{\delta}\right)$ counters O(d) = O(ln(1/δ)) $f_x \le \hat{f}_x \le f_x + \varepsilon N$ (w.p. $1-\delta$) Yes (Median query if negative)
CMS + Conservative Update $O\left(\frac{1}{\varepsilon} \ln \frac{1}{\delta}\right)$ counters O(d) = O(ln(1/δ)) Strictly tighter $\hat{f}_x$ in practice No (Positive increments only)
Count Sketch (Charikar et al.) $O\left(\frac{1}{\varepsilon^2} \ln \frac{1}{\delta}\right)$ counters O(d) = O(ln(1/δ)) $|\hat{f}_x - f_x| \le \varepsilon \|f\|_2$ (Unbiased $\ell_2$) Yes (Uses $\pm 1$ sign hashes)
Misra-Gries / Space-Saving $O(1/\varepsilon)$ exact slots O(1) (Hash + Bucket) Deterministic heavy hitters $\ge \varepsilon N$ No (Insertion-only streams)

Formal Proof: Why $w = \lceil e / \varepsilon \rceil$ and $d = \lceil \ln(1/\delta) \rceil$ Bound Error

Fix a query key $x$ and a single row $j$. Let $X_j = C[j][h_j(x)] - f_x = \sum_{y \ne x} f_y \cdot \mathbb{I}[h_j(y) = h_j(x)]$ be the excess noise in row $j$ caused by collisions. Because $h_j$ is chosen from a 2-universal family, $\Pr[h_j(y) = h_j(x)] \le 1/w$. By linearity of expectation:

$$\mathbb{E}[X_j] = \sum_{y \ne x} f_y \Pr[h_j(y) = h_j(x)] \le \frac{1}{w} \sum_{y \ne x} f_y \le \frac{N}{w} = \frac{\varepsilon N}{e}$$

Since $X_j \ge 0$ (all frequencies are non-negative), Markov's Inequality yields for row $j$:

$$\Pr[X_j > \varepsilon N] \le \frac{\mathbb{E}[X_j]}{\varepsilon N} \le \frac{\varepsilon N / e}{\varepsilon N} = \frac{1}{e}$$

Because the $d$ rows use mutually independent hash seeds, $\hat{f}_x - f_x = \min_j X_j > \varepsilon N$ occurs if and only if every single row $j \in \{0, \dots, d-1\}$ exceeds $\varepsilon N$ simultaneously:

$$\Pr[\hat{f}_x > f_x + \varepsilon N] = \prod_{j=0}^{d-1} \Pr[X_j > \varepsilon N] \le \left(\frac{1}{e}\right)^d = e^{-\lceil \ln(1/\delta) \rceil} \le \delta$$

Visualizing the $d \times w$ Count-Min Matrix & Conservative Update

Count-Min Matrix (d = 3 rows, w = 6 columns) querying key x: Row h0(x)=1: 4 19 2 8 0 5 Row h1(x)=4: 7 3 11 1 12 6 Row h2(x)=2: 9 2 15 5 4 1 Query Estimate: min(19, 12, 15) = 12 Conservative Update (+1): target = 12 + 1 = 13 Only Row 1 (12→13) increments! Heavy-Hitter Tracking (Top-K Flows ≥ φ·N): Pair CMS with a bounded Min-Heap of size K. On update(x, c), if est(x) > heap.min().count, update or replace min-heap root in O(log K).
flowchart LR Stream["Stream Item (x, c)"] --> CMS["Conservative Update CMS new_est = min_j C[j][h_j(x)] + c C[j][h_j(x)] = max(C[j][h_j(x)], new_est)"] CMS --> HH{"x in Top-K Heap?"} HH -- "Yes" --> Upd["Update x.est = new_est siftDown(pos[x])"] HH -- "No & new_est > heap.min()" --> Rep["Replace heap.min() with (x, new_est)"]
Count-Min Sketch d × w Matrix & HyperLogLog Register Harmonic Mean
O(1) Stream Sketch · 1.5 KB HLL

Step through Count-Min Sketch d × w counter updates and point query f̂(x) = min_j C[j, h_j(x)] (eliminating additive collision noise), or trace HyperLogLog leading-zero registers M[j] = max ρ(w) and Harmonic Mean cardinality estimation.

Step 1 / 9
Custom Input:
Timeline

Invariant & State

Update("ip_A", 5): increment C[j, h_j("ip_A")] += 5 across all d = 4 pairwise-independent hash rows.
h_1(x) 0 0 5 0 0 0 0 0 h_2(x) 0 0 0 0 0 5 0 0 h_3(x) 0 5 0 0 0 0 0 0 h_4(x) 0 0 0 0 0 0 5 0 f̂("ip_A") = min(C[0,2], C[1,5], C[2,1], C[3,6]) = 5 (Exact = 5)
Operation
ADD("ip_A", +5)
Dimensions (d × w)
4 × 8 Counters
Probed Cells
[(0,2), (1,5), (2,1), (3,6)]
Query Estimate f̂(x)
5 (Exact = 5)
[Step 1] Added +5 for "ip_A" at cells (0,2), (1,5), (2,1), (3,6).

Hardware Reality: Flat Power-of-Two Row Alignment & Linear Mergeability

When implementing Count-Min Sketch in C++ or Rust, rounding the width $w = \lceil e / \varepsilon \rceil$ up to the next power of two $W = 2^r$ replaces slow modulo arithmetic with a single bitwise mask h & (W - 1) and allows all $d$ rows to be stored in a single contiguous 1D allocation table[(j << r) | (h & (W - 1))]. Furthermore, standard CMS (without conservative update) is a linear sketch: two sketches $C^{(A)}$ and $C^{(B)}$ computed on separate CPU cores or data centers with the same seeds can be merged in-place via vectorized SIMD addition $C^{(A+B)} = C^{(A)} + C^{(B)}$ (_mm256_add_epi64).

Production C++20 Implementation: Count-Min Sketch with Conservative Update & Heavy-Hitter Heap

// C++20: Count-Min Sketch with Power-of-2 Width, Conservative Update & Distributed Merge
#include <vector>
#include <cstdint>
#include <algorithm>
#include <limits>
#include <bit>
#include <cmath>

class CountMinSketch {
    uint32_t depth_;
    uint32_t width_;      // Power of 2 >= ceil(e / eps)
    uint32_t widthMask_;
    uint32_t widthShift_;
    std::vector<uint64_t> table_;
    std::vector<uint64_t> seeds_;

    uint32_t hashRow(uint64_t key, uint32_t row) const noexcept {
        uint64_t x = key ^ seeds_[row];
        x = (x ^ (x >> 30)) * 0xbf58476d1ce4e5b9ULL;
        x = (x ^ (x >> 27)) * 0x94d049bb133111ebULL;
        return static_cast<uint32_t>(x ^ (x >> 31)) & widthMask_;
    }

public:
    CountMinSketch(double epsilon, double delta) {
        uint32_t rawW = static_cast<uint32_t>(std::ceil(2.718281828459045 / epsilon));
        width_ = std::bit_ceil(std::max(16U, rawW));
        widthMask_ = width_ - 1;
        widthShift_ = std::countr_zero(width_);
        depth_ = std::max(1U, static_cast<uint32_t>(std::ceil(std::log(1.0 / delta))));
        table_.assign(static_cast<size_t>(depth_) * width_, 0ULL);
        seeds_.resize(depth_);
        for (uint32_t j = 0; j < depth_; ++j) {
            seeds_[j] = 0x9e3779b97f4a7c15ULL * (j + 1) + 0x6a09e667f3bcc908ULL;
        }
    }

    // Standard linear update (supports distributed sketch addition & deletions)
    void updateLinear(uint64_t key, uint64_t count = 1) noexcept {
        for (uint32_t j = 0; j < depth_; ++j) {
            uint32_t col = hashRow(key, j);
            table_[(static_cast<size_t>(j) << widthShift_) | col] += count;
        }
    }

    // Conservative update: tightens accuracy by only raising counters to min_est + count
    uint64_t updateConservative(uint64_t key, uint64_t count = 1) noexcept {
        uint64_t target = estimate(key) + count;
        for (uint32_t j = 0; j < depth_; ++j) {
            uint32_t col = hashRow(key, j);
            uint64_t& cell = table_[(static_cast<size_t>(j) << widthShift_) | col];
            if (cell < target) cell = target;
        }
        return target;
    }

    uint64_t estimate(uint64_t key) const noexcept {
        uint64_t ans = std::numeric_limits<uint64_t>::max();
        for (uint32_t j = 0; j < depth_; ++j) {
            uint32_t col = hashRow(key, j);
            ans = std::min(ans, table_[(static_cast<size_t>(j) << widthShift_) | col]);
        }
        return ans;
    }
};

Failure Modes & Adversarial Pitfalls

  • Using Conservative Update When Deletions or Linear Sketch Merging Are Required: Conservative update is non-linear: if you apply conservative updates and later subtract counts on deletion, or add two conservatively updated sketches from two servers together, the invariant $\hat{f}_x \ge f_x$ is violated and you can suffer underestimation errors! Use standard updateLinear whenever deletions or distributed sketch merges are required.
  • Taking the Minimum When Negative Updates (Turnstile Model) Are Present: In turnstile streams where keys can be both incremented and decremented ($c_t \in \mathbb{Z}$) with bounded negative noise, taking the min fails if a colliding key had a net negative update. In the general turnstile model, query the median across rows (or use Charikar's Count Sketch with $\pm 1$ Rademacher sign hashes).
Check yourself Question 1 of 4
Concept Check Count-Min Sketch Error Guarantee and Direction of Bias recall

A Count-Min Sketch (Cormode & Muthukrishnan) is initialized with $d = \lceil \ln(1/\delta) \rceil$ rows and $w = \lceil e / \varepsilon \rceil$ counters per row, processing a non-negative insertion-only stream of total weight $N = \sum_i f_i$.

⚡ Interactive Challenge · Staff Level

Algorithmic Design & Complexity Check

Analyze the invariant, asymptotic bound, and hardware behavior before selecting.

Why must Count Sketch aggregate across its $d$ rows using the **median** rather than the **minimum**, and what theoretical advantage does it gain?

Key Takeaways

Count-Min Sketch estimates point frequencies within additive error +εN with probability ≥ 1 - δ using w = ⌈e/ε⌉ columns and d = ⌈ln(1/δ)⌉ pairwise-independent rows.
The error proof relies only on linearity of expectation (pairwise independence) to bound expected row collision noise by εN/e, followed by Markov's inequality and row independence.
Conservative Update only increments counters up to min_j C[j][h_j(x)] + c, dramatically reducing overestimation on positive-only streams at the cost of losing linearity.
Pairing a Count-Min Sketch with an O(K)-sized Min-Heap identifies streaming φ-heavy hitters in O(d + log K) time per packet.
Was this lesson helpful? Spot an issue or have a suggestion? Let us know on GitHub.
Report an issue