Chapter 29: Probabilistic Data Structures

1. Why Probabilistic

Probabilistic data structures trade perfect accuracy for massive improvements in space and time complexity. They provide approximate answers with bounded, quantifiable error rates.

Problem Deterministic Approach Space Probabilistic Approach Space
Set membership (1B items) HashSet ~8 GB Bloom Filter (1% FPR) ~1.2 GB
Frequency counting (1M items) HashMap ~40 MB Count-Min Sketch ~200 KB
Cardinality (1B uniques) HashSet ~8 GB HyperLogLog 12 KB
95th percentile (1B values) Sorted array ~4 GB t-digest ~10 KB

Key Properties

Property Description Example
Sublinear Space Uses much less space than number of elements HyperLogLog: O(log log n) bits
Bounded Error Error rate is configurable and guaranteed Bloom: 1% false positive rate
One-Way Errors Errors only in one direction Count-Min: over-estimates, never under
Constant Time Ops Insert/query independent of data size O(k) for k hash functions
Mergeable Combine sketches from distributed sources HLL sketches can be unioned
Interview Tip: When asked about handling billion-scale data, always consider probabilistic structures first. The key question is: "Can I tolerate X% error for 100x space savings?" Most analytics and caching problems can.

2. Bloom Filter

A space-efficient probabilistic data structure for set membership testing. Uses a bit array and multiple hash functions.

Structure

graph TD subgraph "Bloom Filter: m=16 bits, k=3 hash functions" A[Bit Array] --> B[0 1 0 1 1 0 0 1 0 0 1 1 0 1 0 0] end subgraph "Insert 'apple'" C[h1 apple = 3] --> D[Set bit 3] E[h2 apple = 7] --> F[Set bit 7] G[h3 apple = 11] --> H[Set bit 11] end subgraph "Query 'apple'" I[Check bit 3] --> J{All set?} K[Check bit 7] --> J L[Check bit 11] --> J J -->|Yes| M[Probably in set] J -->|No| N[Definitely NOT in set] end

Operations

class BloomFilter:
    def __init__(self, m, k):
        self.m = m              # bit array size
        self.k = k              # number of hash functions
        self.bits = [0] * m

    def add(self, item):
        for i in range(self.k):
            index = hash_i(item, i) % self.m
            self.bits[index] = 1

    def contains(self, item):
        for i in range(self.k):
            index = hash_i(item, i) % self.m
            if self.bits[index] == 0:
                return False    # Definitely not in set
        return True             # Probably in set (might be false positive)

False Positive Rate

After n insertions:
P(false positive) ≈ (1 - e^(-kn/m))^k

Optimal k (minimize FPR):
k = (m/n) × ln(2) ≈ 0.693 × m/n

Required bits for target FPR:
m = -n × ln(p) / (ln(2))^2
where p = desired false positive rate

Parameter Selection

n (items) Target FPR m (bits) k (hashes) Bits per Item
1,000,000 1% 9,585,058 7 9.6
1,000,000 0.1% 14,377,588 10 14.4
1,000,000 0.01% 19,170,117 13 19.2
10,000,000 1% 95,850,584 7 9.6

Pros

  • Constant O(k) insert and query time
  • Space-efficient: ~10 bits per item for 1% FPR
  • No false negatives
  • Simple to implement

Cons

  • Cannot delete items (basic version)
  • False positives increase as filter fills
  • Cannot list items in set
  • Fixed size (must know n upfront)
Interview Question: "Why can't you delete from a Bloom filter?" Answer: Clearing a bit might remove other items that also hashed to that position. Use counting Bloom filter instead.

3. Bloom Filter Applications

Application Problem Solved Impact Real-World Example
Cache Prefetching Avoid expensive DB lookups for missing keys Eliminates 99% of cache misses Chrome uses for malicious URL detection
Duplicate Detection Quick check if URL/email already seen 10x throughput increase Web crawlers avoid re-crawling pages
Database Query Optimization Skip partition scans that can't have data 50-90% reduction in disk I/O Cassandra SSTables, Parquet row groups
Network Routers Fast IP blacklist lookup Wire-speed filtering DDoS mitigation systems
Spell Checking Quick dictionary membership test Reduces full dictionary lookups First-pass filter before edit distance
Weak Password Detection Check against billions of leaked passwords 200 MB vs 20 GB dictionary Troy Hunt's Pwned Passwords API

Cassandra SSTable Bloom Filters

// Query: SELECT * FROM users WHERE user_id = 12345
// Without Bloom filter: Check all 10 SSTables on disk
// With Bloom filter:
//   - 8 SSTables return "definitely not present" (skip disk I/O)
//   - 2 SSTables return "maybe present" (read these)
//   Result: 80% reduction in disk reads

// Configuration
CREATE TABLE users (
    user_id bigint PRIMARY KEY,
    name text
) WITH bloom_filter_fp_chance = 0.01;  // 1% FPR

Web Crawler Architecture

graph LR A[URL Queue] --> B{Bloom Filter
contains URL?} B -->|Yes - skip| C[Next URL] B -->|No - probably new| D[Fetch Page] D --> E[Add to Bloom] D --> F[Extract Links] F --> A style B fill:#ffe6cc
Interview Scenario: "Design a system to detect duplicate uploads in a file-sharing service with 10B files." Use Bloom filter with 0.1% FPR (14.4 bits/file = 17 GB). On upload, check filter. If "definitely not present", add it. If "maybe present", compute full hash to verify.

4. Counting Bloom Filter

Extension of Bloom filter that supports deletion by replacing each bit with a small counter (typically 4 bits).

Structure Comparison

Feature Standard Bloom Counting Bloom
Storage per position 1 bit 4 bits (counter 0-15)
Insert operation Set bit to 1 Increment counter
Delete operation Not supported Decrement counter
Query operation All bits == 1? All counters > 0?
Space overhead 1x 4x
Counter overflow risk N/A Yes (max 15 with 4-bit)

Implementation

class CountingBloomFilter:
    def __init__(self, m, k, counter_bits=4):
        self.m = m
        self.k = k
        self.max_count = (1 << counter_bits) - 1  # 15 for 4-bit
        self.counters = [0] * m

    def add(self, item):
        for i in range(self.k):
            index = hash_i(item, i) % self.m
            if self.counters[index] < self.max_count:
                self.counters[index] += 1

    def remove(self, item):
        # First verify item is present
        if not self.contains(item):
            return False

        for i in range(self.k):
            index = hash_i(item, i) % self.m
            if self.counters[index] > 0:
                self.counters[index] -= 1
        return True

    def contains(self, item):
        for i in range(self.k):
            index = hash_i(item, i) % self.m
            if self.counters[index] == 0:
                return False
        return True

Counter Overflow Problem

P(overflow) increases with:
- Load factor (n/m)
- Number of hash functions (k)
- Counter size (bits)

For 4-bit counters (max 15):
- At 50% load: overflow risk ~0.001%
- At 80% load: overflow risk ~1%
- At 95% load: overflow risk ~15%

Use Cases

  • LRU cache with dynamic eviction
  • Network flow tracking with timeouts
  • Session management
  • Sliding window duplicate detection

Tradeoffs

  • 4x space overhead vs standard Bloom
  • Still get false positives
  • Counter overflow causes false negatives
  • More complex than standard Bloom

5. Count-Min Sketch

Probabilistic data structure for frequency estimation. Uses a 2D array with multiple hash functions to estimate item counts with over-estimation guarantee.

Structure

graph TD subgraph "Count-Min Sketch: d=4 rows, w=8 columns" A[Item: 'apple'] --> B[h1 apple = 3] A --> C[h2 apple = 6] A --> D[h3 apple = 1] A --> E[h4 apple = 5] B --> F[Row 0: 2 5 1 8 3 4 1 2] C --> G[Row 1: 1 3 4 2 6 1 9 3] D --> H[Row 2: 4 7 2 5 1 3 2 1] E --> I[Row 3: 3 1 4 2 8 5 1 2] end J[Query 'apple'] --> K[Get counts: 8, 9, 7, 8] K --> L[Return min = 7]

Algorithm

class CountMinSketch:
    def __init__(self, width, depth):
        self.w = width          # w columns
        self.d = depth          # d rows (hash functions)
        self.table = [[0] * width for _ in range(depth)]

    def add(self, item, count=1):
        for i in range(self.d):
            index = hash_i(item, i) % self.w
            self.table[i][index] += count

    def estimate(self, item):
        estimates = []
        for i in range(self.d):
            index = hash_i(item, i) % self.w
            estimates.append(self.table[i][index])
        return min(estimates)   # Return minimum to reduce over-estimation

Error Guarantees

With probability at least (1 - δ):
count(item) ≤ estimate(item) ≤ count(item) + ε × N

where:
- N = total count of all items
- ε = error bound
- δ = failure probability

Parameters:
w = ⌈e / ε⌉ (e ≈ 2.718)
d = ⌈ln(1/δ)⌉

Example: ε=0.01 (1% error), δ=0.01 (99% confidence)
w = 272, d = 5
Space: 272 × 5 = 1,360 counters

Parameter Selection

Error (ε) Confidence (1-δ) Width (w) Depth (d) Space (4-byte counters)
1% of N 99% 272 5 5.3 KB
0.1% of N 99% 2,718 5 53 KB
1% of N 99.9% 272 7 7.4 KB
0.01% of N 99.99% 27,183 9 955 KB
Key Property: Count-Min Sketch NEVER under-estimates. It can only over-estimate due to hash collisions. This makes it safe for rate limiting and abuse detection where false negatives are dangerous.

6. Count-Min Sketch Applications

Application Use Case Why CMS Example
Heavy Hitters Find top-K most frequent items O(1) updates, sublinear space Most viewed videos on YouTube
Network Traffic Analysis Track packet counts per IP Wire-speed processing DDoS detection (find IPs with >10K packets/sec)
Rate Limiting Enforce per-user API quotas Over-estimation is safe (blocks slightly early) Cloudflare rate limiting
Database Query Stats Track query execution counts Lightweight vs full query log PostgreSQL slow query tracking
Ad Click Fraud Detect users clicking same ad repeatedly Scales to billions of events Flag if user_id + ad_id > 100 clicks

Heavy Hitters Algorithm

class HeavyHitters:
    def __init__(self, k, epsilon, delta):
        self.k = k
        self.cms = CountMinSketch(width=int(2.718/epsilon), depth=int(math.log(1/delta)))
        self.heap = []  # Min-heap of (count, item)

    def add(self, item):
        self.cms.add(item)
        count = self.cms.estimate(item)

        # If item in heap, update it
        # If heap not full or count > min, add to heap
        if len(self.heap) < self.k:
            heapq.heappush(self.heap, (count, item))
        elif count > self.heap[0][0]:
            heapq.heapreplace(self.heap, (count, item))

    def get_top_k(self):
        return sorted(self.heap, reverse=True)

# Example: Find top 100 URLs with epsilon=0.001, delta=0.01
# Space: ~53 KB for CMS + ~3 KB for heap = 56 KB
# vs HashMap tracking all URLs: potentially GBs

Rate Limiting with CMS

sequenceDiagram participant Client participant API participant CMS Client->>API: Request with API key API->>CMS: Increment count for key CMS-->>API: Estimated count = 98 API->>Client: 200 OK (2 remaining) Client->>API: Request with API key API->>CMS: Increment count for key CMS-->>API: Estimated count = 99 API->>Client: 200 OK (1 remaining) Client->>API: Request with API key API->>CMS: Increment count for key CMS-->>API: Estimated count = 100 API->>Client: 429 Too Many Requests
Interview Question: "Find top 10 trending hashtags from 1B tweets using 100 MB memory." Use Count-Min Sketch (ε=0.001, δ=0.01, ~53 KB) + min-heap (10 items). Process tweets in one pass, update CMS and heap. Over-estimation bias doesn't affect relative ordering of top items.

7. HyperLogLog

Probabilistic algorithm for estimating cardinality (count of distinct elements) with remarkable space efficiency: 12 KB for 0.8% error on billions of items.

Core Intuition

If you hash items uniformly:
- 50% have pattern 1xxxxxxx... (leading 0)
- 25% have pattern 01xxxxxx... (two leading 0s)
- 12.5% have pattern 001xxxxx... (three leading 0s)
- 6.25% have pattern 0001xxxx... (four leading 0s)

If max leading zeros seen = ρ:
Estimated cardinality ≈ 2^ρ

Problem: High variance (factor of 2 error)
Solution: Use m buckets, average their estimates

Algorithm

graph TD A[Item: 'apple'] --> B[Hash to 64 bits] B --> C[01101010...00010110] C --> D[First 14 bits = bucket index] C --> E[Remaining 50 bits = count leading zeros + 1] D --> F[Bucket 6852] E --> G[Leading zeros = 3, ρ = 4] F --> H[M 6852 = max 4, 7 = 7] G --> H I[Query cardinality] --> J[Compute harmonic mean
of 2^M j for all buckets] J --> K[Apply correction factors] K --> L[Return estimate]

Implementation

class HyperLogLog:
    def __init__(self, precision=14):
        self.p = precision              # Number of bits for bucket index
        self.m = 1 << precision         # Number of buckets (2^p)
        self.M = [0] * self.m           # Max leading zeros per bucket

        # Alpha constant for bias correction
        if self.m >= 128:
            self.alpha = 0.7213 / (1 + 1.079 / self.m)
        elif self.m >= 64:
            self.alpha = 0.709
        elif self.m >= 32:
            self.alpha = 0.697
        elif self.m >= 16:
            self.alpha = 0.673

    def add(self, item):
        # Hash to 64 bits
        h = hash64(item)

        # First p bits determine bucket
        j = h & ((1 << self.p) - 1)

        # Remaining bits: count leading zeros + 1
        w = h >> self.p
        rho = leading_zeros(w) + 1

        # Update max for this bucket
        self.M[j] = max(self.M[j], rho)

    def cardinality(self):
        # Harmonic mean
        raw_estimate = self.alpha * (self.m ** 2) / sum(2 ** (-x) for x in self.M)

        # Small range correction
        if raw_estimate <= 2.5 * self.m:
            zeros = self.M.count(0)
            if zeros != 0:
                return self.m * math.log(self.m / zeros)

        # Large range correction
        if raw_estimate > (1/30) * (2 ** 32):
            return -2 ** 32 * math.log(1 - raw_estimate / (2 ** 32))

        return raw_estimate

Precision vs Space Tradeoff

Precision (p) Buckets (m=2^p) Space (6-bit registers) Standard Error Relative Error
10 1,024 768 bytes 1.04 / √m = 3.2% ±6.4% at 95% confidence
12 4,096 3 KB 1.6% ±3.2%
14 16,384 12 KB 0.8% ±1.6%
16 65,536 48 KB 0.4% ±0.8%
18 262,144 192 KB 0.2% ±0.4%

Merging HyperLogLogs

def merge(hll1, hll2):
    # HLLs must have same precision
    assert hll1.p == hll2.p

    result = HyperLogLog(hll1.p)
    for i in range(hll1.m):
        result.M[i] = max(hll1.M[i], hll2.M[i])

    return result

# Example: Union cardinality across shards
shard_hlls = [shard.get_hll() for shard in shards]
global_hll = reduce(merge, shard_hlls)
total_uniques = global_hll.cardinality()
Magic Numbers: 12 KB and 0.8% error (p=14) is the sweet spot. Redis uses this default. Can count trillions of distinct items with higher accuracy than most engineers need.

8. HyperLogLog Applications

Application Metric Why HLL Space Savings
Web Analytics Daily unique visitors 0.8% error acceptable for dashboards 12 KB vs 8 GB for 1B users
Database Statistics Distinct values in column (for query optimization) Fast cardinality estimates Constant space per column
Network Monitoring Unique source IPs per minute Real-time processing 12 KB per time window
Social Networks Unique viewers of post Mergeable across datacenters 12 KB per post vs storing all viewer IDs
Advertising Unique users reached by campaign Union/intersection of audiences 12 KB per campaign segment
Log Analysis Distinct error messages Space-efficient aggregation 12 KB per log stream

Redis HyperLogLog Commands

# Add elements
PFADD unique_visitors user:123 user:456 user:789

# Get cardinality estimate
PFCOUNT unique_visitors
# Returns: 3

# Merge multiple HLLs
PFADD page:home user:123 user:456
PFADD page:about user:456 user:789
PFMERGE total_visitors page:home page:about
PFCOUNT total_visitors
# Returns: 3 (union of both sets)

# Memory usage
MEMORY USAGE unique_visitors
# Returns: ~12KB regardless of cardinality

Multi-Dimensional Cardinality

# Track unique visitors per (country, device) dimension
# Without HLL: Need HashSet per dimension combo = 200 countries × 10 devices = 2000 sets
# With HLL: 12 KB × 2000 = 24 MB for all dimensions

for event in stream:
    key = f"uniques:{event.country}:{event.device}:{date}"
    redis.pfadd(key, event.user_id)

# Query: Unique mobile users in US on 2026-05-12
count = redis.pfcount("uniques:US:mobile:2026-05-12")

# Query: Unique users across all devices in France
keys = [f"uniques:FR:{device}:{date}" for device in devices]
redis.pfmerge("temp:query", *keys)
count = redis.pfcount("temp:query")

Real-World Performance

Company Use Case Scale Result
Reddit Unique post views 100M+ posts 1.2 TB → 1.2 GB (1000x reduction)
Twitter Unique tweet impressions 500M tweets/day Real-time cardinality with 0.8% error
Aggregate Knowledge Ad audience measurement Billions of users 99.97% accuracy vs ground truth
Interview Question: "Count unique visitors to 10M web pages with 1B total visitors." Use HyperLogLog with 12 KB per page = 120 GB total. Can fit in memory on single server. HashSet would need 8 bytes × 1B × 10M = 80 PB (impossible).

9. t-digest

Probabilistic data structure for estimating percentiles and quantiles from streaming data with bounded error and mergeability for distributed systems.

Core Concept

Instead of storing all values (O(n) space):
- Group nearby values into centroids
- Store (mean, weight) for each centroid
- More centroids near 0/100 percentile (tails)
- Fewer centroids near 50 percentile (median)

Space: O(compression) centroids
Typical: δ=100 → ~100-200 centroids → 2-4 KB

Why Non-Uniform Clustering

Percentile Range Importance Centroid Density Reason
0-1%, 99-100% Critical (SLA, outliers) High Need accuracy for p99, p99.9
1-10%, 90-99% Important (tails) Medium-High Tail latency matters
40-60% Less critical (median) Low Few outliers near median

Implementation Sketch

class TDigest:
    def __init__(self, compression=100):
        self.compression = compression  # δ parameter
        self.centroids = []             # List of (mean, weight)
        self.count = 0

    def add(self, value, weight=1):
        self.count += weight

        # Find nearest centroid
        nearest = self._find_nearest(value)

        # Compute capacity using scaling function
        q = self._quantile_of_centroid(nearest)
        capacity = self._capacity_at_quantile(q)

        if nearest.weight + weight <= capacity:
            # Merge into existing centroid
            nearest.merge(value, weight)
        else:
            # Create new centroid
            self.centroids.append(Centroid(value, weight))

        # Periodically compress
        if len(self.centroids) > self.compression * 3:
            self._compress()

    def quantile(self, q):
        # q in [0, 1]
        target = q * self.count
        sum_weight = 0

        for i, c in enumerate(self.centroids):
            sum_weight += c.weight
            if sum_weight >= target:
                # Interpolate between centroids
                if i == 0:
                    return c.mean
                prev = self.centroids[i-1]
                delta = (target - (sum_weight - c.weight)) / c.weight
                return prev.mean + delta * (c.mean - prev.mean)

        return self.centroids[-1].mean

    def _capacity_at_quantile(self, q):
        # Scaling function k(q,δ) determines centroid capacity
        # More capacity near median, less at tails
        return 4 * self.count * q * (1 - q) / self.compression

    def merge(self, other):
        # Combine centroids from both digests
        self.centroids.extend(other.centroids)
        self.count += other.count
        self._compress()

Error Guarantees

Percentile Max Error (δ=100) Max Error (δ=200) Example (1M latencies)
p50 (median) ±1% ±0.5% If true p50=100ms, estimate 99-101ms
p90 ±0.5% ±0.25% If true p90=200ms, estimate 199-201ms
p99 ±0.1% ±0.05% If true p99=500ms, estimate 499.5-500.5ms
p99.9 ±0.01% ±0.005% If true p99.9=1000ms, estimate 999.9-1000.1ms

Applications

System Metric Compression (δ) Space
Prometheus API latency percentiles 100 ~2 KB per metric
ElasticSearch Aggregation percentiles 100 Distributed merge across shards
PostgreSQL (pg_stats) Column value distribution 200 Query planner estimates
DataDog Custom metrics percentiles 100 Real-time dashboard
Key Advantage: t-digest is mergeable. Can compute percentiles across distributed data by merging digests from each node. This is impossible with naive approaches like reservoir sampling.

10. Cuckoo Filter

Probabilistic data structure for set membership that supports deletion, offers better space efficiency than Bloom filters at low false positive rates, and provides faster lookups.

Key Differences from Bloom Filter

Feature Bloom Filter Cuckoo Filter
Storage Bit array Array of buckets with fingerprints
Deletion Not supported (or 4x space for counting) Supported natively
Space at 1% FPR 9.6 bits/item 12.5 bits/item
Space at 0.1% FPR 14.4 bits/item 13.5 bits/item (better)
Lookup Speed k hash computations 2 bucket lookups (faster)
Insertion Failure Never (always succeeds) Possible if table full (rare)

Structure

graph TD subgraph "Cuckoo Filter" A[Item: 'apple'] --> B[Fingerprint f = hash 'apple' mod 2^b] A --> C[Hash h = hash 'apple'] C --> D[Bucket i1 = h mod m] B --> E[Bucket i2 = i1 XOR hash f mod m] D --> F[Store f in bucket i1 or i2] E --> F G[Each bucket holds 4 fingerprints] F --> G end H[Lookup 'apple'] --> I[Check bucket i1] H --> J[Check bucket i2] I --> K{f in bucket?} J --> K K -->|Yes| L[Probably present] K -->|No both| M[Definitely not present]

Algorithm

class CuckooFilter:
    def __init__(self, capacity, bucket_size=4, fingerprint_size=8):
        self.m = capacity // bucket_size    # Number of buckets
        self.b = bucket_size                # Entries per bucket
        self.f = fingerprint_size           # Fingerprint bits
        self.buckets = [[None]*self.b for _ in range(self.m)]

    def _fingerprint(self, item):
        return hash(item) % (1 << self.f)

    def _index(self, item):
        return hash(item) % self.m

    def _alt_index(self, index, fingerprint):
        return (index ^ hash(fingerprint)) % self.m

    def insert(self, item):
        f = self._fingerprint(item)
        i1 = self._index(item)
        i2 = self._alt_index(i1, f)

        # Try to insert in i1 or i2
        if self._insert_to_bucket(i1, f):
            return True
        if self._insert_to_bucket(i2, f):
            return True

        # Both buckets full: relocate existing items (cuckoo hashing)
        i = random.choice([i1, i2])
        for _ in range(500):  # Max kicks
            f, self.buckets[i][random.randint(0, self.b-1)] = \
                self.buckets[i][random.randint(0, self.b-1)], f
            i = self._alt_index(i, f)
            if self._insert_to_bucket(i, f):
                return True

        return False  # Filter full

    def contains(self, item):
        f = self._fingerprint(item)
        i1 = self._index(item)
        i2 = self._alt_index(i1, f)
        return f in self.buckets[i1] or f in self.buckets[i2]

    def delete(self, item):
        f = self._fingerprint(item)
        i1 = self._index(item)
        i2 = self._alt_index(i1, f)

        if f in self.buckets[i1]:
            self.buckets[i1].remove(f)
            return True
        if f in self.buckets[i2]:
            self.buckets[i2].remove(f)
            return True
        return False

Performance Comparison

Metric Bloom (1% FPR) Cuckoo (1% FPR) Bloom (0.1% FPR) Cuckoo (0.1% FPR)
Bits per item 9.6 12.5 14.4 13.5 (better)
Lookup cost 7 hashes 2 buckets 10 hashes 2 buckets (better)
Cache misses 7 random locations 2 buckets (better) 10 random locations 2 buckets (better)
Deletion Not supported O(1) 4x space overhead O(1) (better)
Load factor Arbitrary ~95% max Arbitrary ~95% max

When to Use Cuckoo Filter

  • Need deletion (e.g., cache eviction)
  • Low FPR required (< 1%)
  • Lookup speed critical
  • CPU cache efficiency matters

When to Use Bloom Filter

  • Write-once, no deletion needed
  • Higher FPR acceptable (1-5%)
  • Simpler implementation preferred
  • Slightly better space at high FPR

11. Comparison Matrix

Structure Question Answered Space Complexity Error Type Deletion Mergeable
Bloom Filter Is x in set? O(1) per item
~10 bits/item
False positives
No false negatives
No Yes (OR bits)
Counting Bloom Is x in set? O(1) per item
~40 bits/item
False positives
False negatives (overflow)
Yes Yes (sum counters)
Count-Min Sketch How many x? O(1/ε × log 1/δ)
~5 KB typical
Over-estimates
Never under-estimates
Yes (decrement) Yes (sum arrays)
HyperLogLog How many distinct x? O(log log n)
12 KB typical
±1.6% at 95% conf No Yes (max registers)
t-digest What is pXX? O(compression)
2-4 KB typical
Bounded by compression No (streaming) Yes (merge centroids)
Cuckoo Filter Is x in set? O(1) per item
~13 bits/item
False positives
No false negatives
Yes No (complex)

Detailed Feature Matrix

Structure Insert Time Query Time Memory Accuracy Tunable Best For
Bloom O(k) O(k) Excellent Yes (k, m) Cache filters, deduplication
Counting Bloom O(k) O(k) Good (4x Bloom) Yes (k, m) Dynamic sets with eviction
Count-Min O(d) O(d) Excellent Yes (ε, δ) Heavy hitters, rate limiting
HyperLogLog O(1) O(m) Exceptional Yes (precision) Cardinality, unique counts
t-digest O(log c) O(c) Excellent Yes (compression) Percentiles, quantiles
Cuckoo O(1) amortized O(1) Very Good Yes (bucket, fingerprint) Low-FPR membership + deletion

Error Characteristics

Structure False Positives False Negatives Magnitude Error Tail Accuracy
Bloom Yes (configurable) Never N/A N/A
Count-Min N/A N/A Only over-estimates N/A
HyperLogLog N/A N/A ±1.6% (unbiased) N/A
t-digest N/A N/A Bounded by δ Better at extremes
Cuckoo Yes (configurable) Never N/A N/A

12. When to Use

Decision Tree by Problem Type

Problem Scale Recommended Structure Alternative Reason
Check if URL seen before Billions Bloom Filter Cuckoo (if need deletion) Simple, space-efficient
Cache negative lookups Millions Bloom Filter Cuckoo (lower FPR) Avoid expensive DB queries
Track top-100 URLs Billions Count-Min Sketch - Heavy hitters with minimal space
Rate limiting per user Millions Count-Min Sketch Token bucket Over-estimation is safe
Count distinct visitors Billions HyperLogLog - Best space/accuracy tradeoff
Database cardinality stats Millions HyperLogLog - Fast query planner estimates
p99 latency monitoring Billions t-digest Reservoir sampling Better tail accuracy, mergeable
SLA breach detection (> p99.9) Millions t-digest - High accuracy at extremes
LRU cache with membership Millions Cuckoo Filter Counting Bloom Fast deletion support
Malicious IP blocking Millions Cuckoo Filter Bloom Need to remove IPs after timeout

System Design Patterns

Pattern Structures Used Architecture Example
Cache Aside with Bloom Bloom + Cache + DB Check Bloom → Cache → DB Avoid DB lookups for missing keys
Two-Level Frequency Count-Min + HashMap CMS estimates all, map stores top-K Heavy hitter detection
Distributed Cardinality HLL per shard + merge Each shard maintains HLL, merge at query Global unique visitors across DCs
Percentile Aggregation t-digest per server + merge Per-server digests merged for fleet view Global p99 latency
Write-Through with Cuckoo Cuckoo + Cache Update both on write, check Cuckoo on read Fast membership + eviction support

Common Combinations

Use Case Combination Why Both
Web Analytics Dashboard HLL (uniques) + Count-Min (heavy hitters) + t-digest (latency) Each answers different question with minimal space
Rate Limiter Bloom (blocked users) + Count-Min (rate tracking) Fast blocked check, then accurate count
Log Analysis HLL (distinct errors) + Count-Min (error frequency) + Bloom (seen before) Comprehensive stats with < 100 KB total
Network Monitoring Cuckoo (active flows) + HLL (unique IPs) + t-digest (packet size distribution) Track active flows with deletion + stats
Interview Strategy: When given a large-scale problem, always ask: "Can I use approximate counting?" Then match the problem to the right structure:
  • Membership → Bloom/Cuckoo
  • Frequency → Count-Min Sketch
  • Cardinality → HyperLogLog
  • Percentiles → t-digest
Justify with space savings and acceptable error bounds.

Implementation Checklist

Consideration Bloom/Cuckoo Count-Min HyperLogLog t-digest
Choose Parameters Set FPR, calculate m and k Set ε and δ, calculate w and d Set precision (12-16 typical) Set compression (100-200)
Hash Functions MurmurHash3, xxHash Independent hashes or double hashing 64-bit hash (MurmurHash3) Any good hash
Serialization Bit array to bytes 2D array of ints Array of bytes (registers) List of (mean, weight) pairs
Distributed OR bits across replicas Sum arrays across shards Max registers across DCs Merge centroids from nodes
Monitoring Track load factor (n/m) Track collision rate Track register distribution Track centroid count