Vibe Engines is free, interactive AI engineering & system design — runs in your browser. Pick a learning path →
Bloom Filter
A tiny bit-array that answers "have I seen this?" using a fraction of the memory of a real set — at the cost of occasional false positives, but never a false negative. It guards databases, CDNs and crawlers from pointless lookups. Build one with double hashing. Provided hashes keep Python and TypeScript in sync. Hidden tests.
The problem
Build a Bloom filter of m bits using k hash functions, insert every item in items, then answer each query in queries: return True if the item is possibly present (all k of its bits are set) or False if it is definitely absent. Derive the k bit positions by double hashing: bit i is (hash32(x) + i·hb(x)) % m for i in 0…k-1. Both hashes are provided. Implement bloom(items, queries, m, k).
Your turn — write it
Edit the stub, hit Run (or ⌘/Ctrl + Enter), and watch the hidden tests. Stuck? open the hints under the problem, or Reveal solution is one click away.
Implement bloom(items, queries, m, k): make an m-bit array, set the k double-hashed bits of every item, then for each query return whether all of its k bits are set.
Examples and constraints 3 examples
bloom(['cat','dog','bird'], ['cat','dog','bird','zebra','fish'], 64, 3)[True, True, True, False, False]bloom(['apple','banana','cherry','date','elder'], ['apple','fish','zebra'], 16, 2)[True, True, False]bloom([], ['a','b'], 64, 3)[False, False]- Use the provided
hash32andhb— the tests depend on the exact bit positions. - No false negatives: every inserted item must return True. Bloom filters can only err the other way.
- A false positive is possible (an absent item whose k bits all happen to be set) — that is allowed by design.
- An empty filter (no items) must return False for everything.
Hints 4 ideas
- A bit array is just a list of 0/1 of length
m. Setting a bit twice is fine (idempotent). - The
i-th bit position for itemxis(hash32(x) + i * hb(x)) % m— this is double hashing, giving k independent-looking positions from two hashes. - Insert: for every item, set all k of its bits to 1.
- Query: the item is "possibly present" only if every one of its k bits is already 1; a single 0 proves it was never inserted.
Approach, complexity & discussion — open after you solve
Start from brute force
The obvious approach is an exact set: add every item to a hash set and answer each query with q in seen. Membership is exact, and each insert or query costs O(L) expected time to hash a string of length L. The cost is space: O(total size of all items), because the set keeps every string plus per-entry overhead. A billion 50-byte URLs need at least 50 GB for the strings alone, while a Bloom filter at a 1% false-positive rate needs about 9.6 bits per item, roughly 1.2 GB, however long the items are. It is also wrong for this spec, which defines the answer by the bits: in the third example 'fish' must return True, and a set says False.
The approach
The insight is that an item's bit positions depend only on the item, so insertion and lookup touch exactly the same k bits. Insert sets them; lookup checks them. That gives the one guarantee a Bloom filter makes: an inserted item always finds all of its bits set, so there are no false negatives. An absent item returns True only when other items happened to set all k of its positions, which is a false positive and is allowed.
The reference solution makes bits = [0] * m, then for every x in items and every i in range(k) sets bits[(hash32(x) + i * hb(x)) % m] = 1. For each query it appends all(bits[(hash32(q) + i * hb(q)) % m] for i in range(k)), which stops at the first 0 it reads. Double hashing is what makes k hash functions cheap: two base hashes generate all k positions, and Kirsch and Mitzenmacher showed that this loses nothing in the asymptotic false-positive rate compared with k independent hashes.
Walk through an example
Trace the first example, bloom(['cat','dog','bird'], ['cat','dog','bird','zebra','fish'], 64, 3), where each row lists the three positions (hash32(x) + i * hb(x)) % 64 for i = 0, 1, 2.
| step | operation | positions | bits there | result |
|---|---|---|---|---|
| 1 | insert 'cat' | 21, 55, 25 | set to 1 | 3 bits set |
| 2 | insert 'dog' | 3, 45, 23 | set to 1 | 6 bits set |
| 3 | insert 'bird' | 56, 44, 32 | set to 1 | 9 of 64 bits set |
| 4 | query 'cat' | 21, 55, 25 | 1, 1, 1 | True |
| 5 | query 'dog' | 3, 45, 23 | 1, 1, 1 | True |
| 6 | query 'bird' | 56, 44, 32 | 1, 1, 1 | True |
| 7 | query 'zebra' | 29, 23, 17 | 0, 1, 0 | False (stops at bit 29) |
| 8 | query 'fish' | 8, 30, 52 | 0, 0, 0 | False (stops at bit 8) |
Complexity
Optimal: O(k) bit operations per insert and per query, plus O(L) to hash a length-L string once (the reference recomputes both hashes for every i, which makes it O(k·L)); space is m bits, independent of item size. Brute force (a hash set): O(L) expected per operation, but space grows with every stored string.
Edge cases to test
- Empty filter:
bloom([], ['a', 'b'], 64, 3)→[False, False]. Every bit is 0, so the first bit checked already proves absence. - A false positive:
bloom(['apple', 'banana', 'cherry', 'date', 'elder'], ['fish'], 16, 2)→[True].'fish'maps to bits 8 and 14, both set by other words; returningFalsemeans the code is not reading the bits. - A second hash that shares a factor with
m: withm = 64,hb('olive') % 64is 32, so its positions cycle52, 20, 52andk = 3sets only two distinct bits. Still no false negatives, just a weaker filter; real implementations use a primemor force the second hash odd. m = 1: every position is bit 0, so after any insert every query returnsTrue:bloom(['a'], ['a', 'b'], 1, 3)→[True, True].k = 0:all()over zero bits isTrue, so every query is “possibly present”, even on an empty filter. Valid input needsk ≥ 1.- Duplicate inserts: setting a bit that is already 1 changes nothing, so inserting
'cat'twice gives the same filter as inserting it once.
Common mistakes
- Sizing
mtoo small or picking the wrongkfor the target rate, so false positives spike. - Reading “maybe present” as “present” — it must be verified; only “absent” is certain.
- Trying to delete an element — a standard Bloom filter cannot; use a counting variant.
- Deriving positions from Python’s built-in
hash(): string hashing is randomized per process, so a filter saved and reloaded by another process returns false negatives. Use a fixed hash, as the providedhash32is.
Follow-ups an interviewer asks next
- How do you choose m and k?
- For
nitems and a target false-positive ratep, the optimal size ism = −n·ln p / (ln 2)²and the best hash count isk = (m / n)·ln 2. Atp = 1%that is about 9.6 bits per item andk ≈ 7. For any chosen values, the rate is approximately(1 − e^(−kn/m))^k. - How would you support deletion?
- A counting Bloom filter replaces each bit with a small counter, often 4 bits: insert increments the k counters, delete decrements them, and a query checks that all k are nonzero. It costs several times the memory, and deleting an item that was never inserted corrupts other items. A cuckoo filter supports deletion more compactly by storing short fingerprints instead of bits.
- What if the set grows past the n you sized it for?
- The false-positive rate climbs as the bits fill up, and nothing signals it. A scalable Bloom filter adds a new, larger layer with a tighter error target each time the current one reaches capacity, and a query checks every layer. The simpler option is to rebuild the filter at a larger
mfrom the source of truth. - How do two machines combine their filters?
- Two filters built with the same
m,kand hash functions merge with a bitwise OR, and the result is exactly the filter of the union of their items. AND gives only an approximation of the intersection. Distributed databases use this to ship a compact filter of one side’s join keys so the other side can skip rows that cannot match.
Where this shows up
Bloom filters guard expensive lookups with tiny memory: an LSM-tree checks one before hitting disk, a crawler dedups URLs, a cache skips a certain miss. Trading a small, tunable false-positive rate for “definitely not here, skip the slow path” is the deal that makes them ubiquitous.
Reference solution
Reference solution — Python
def hash32(s): # provided (xmur3 32-bit, matches TypeScript)
h = (1779033703 ^ len(s)) & 0xffffffff
for ch in s:
h = ((h ^ ord(ch)) * 3432918353) & 0xffffffff
h = ((h << 13) | (h >> 19)) & 0xffffffff
h = ((h ^ (h >> 16)) * 2246822507) & 0xffffffff
h = ((h ^ (h >> 13)) * 3266489909) & 0xffffffff
return (h ^ (h >> 16)) & 0xffffffff
def hb(s): # provided second hash (for double hashing)
return hash32("salt:" + s)
def bloom(items, queries, m, k):
bits = [0] * m
for x in items: # insert: set all k bits of every item
for i in range(k):
bits[(hash32(x) + i * hb(x)) % m] = 1
out = []
for q in queries: # possibly-present iff every bit is set
present = all(bits[(hash32(q) + i * hb(q)) % m] for i in range(k))
out.append(present)
return out
print(bloom(['cat', 'dog', 'bird'], ['cat', 'dog', 'bird', 'zebra', 'fish'], 64, 3))
# -> [True, True, True, False, False]Reference solution — TypeScript
function hash32(s: string): number { // provided (xmur3 32-bit, matches Python)
let h = (1779033703 ^ s.length) >>> 0;
for (let i = 0; i < s.length; i++) {
h = Math.imul(h ^ s.charCodeAt(i), 3432918353);
h = (h << 13) | (h >>> 19);
}
h = Math.imul(h ^ (h >>> 16), 2246822507);
h = Math.imul(h ^ (h >>> 13), 3266489909);
h ^= h >>> 16;
return h >>> 0;
}
function hb(s: string): number { return hash32("salt:" + s); } // second hash
function bloom(items: string[], queries: string[], m: number, k: number): boolean[] {
const bits = new Array(m).fill(0);
for (const x of items) // insert: set all k bits
for (let i = 0; i < k; i++) bits[(hash32(x) + i * hb(x)) % m] = 1;
return queries.map((q) => {
for (let i = 0; i < k; i++) if (!bits[(hash32(q) + i * hb(q)) % m]) return false;
return true; // every bit set → possibly present
});
}
console.log(bloom(['cat', 'dog', 'bird'], ['cat', 'dog', 'bird', 'zebra', 'fish'], 64, 3));
// -> [true, true, true, false, false]Questions about Bloom Filter
Can a Bloom filter give a false negative?
Not if it is used as designed. Inserting an item sets its k bits, bits are never cleared, and a lookup recomputes the same k positions, so an inserted item always finds every one of its bits set. False negatives only appear when something breaks that chain: deleting by clearing bits, using a hash that changes between processes such as Python’s salted built-in hash for strings, or querying with different m, k or hash functions than the filter was built with. Within this challenge every inserted item must return True, and the hidden tests check it.
How do you calculate the false positive rate of a Bloom filter?
With n items inserted into m bits using k hash functions, the chance that a given bit is still 0 is about e to the power of minus kn over m. A false positive needs all k bits of an absent item to be 1, so the rate is approximately one minus that, raised to the power k. The best k for a given m and n is m over n times ln 2, and at that k a 1 percent false-positive rate takes about 9.6 bits per item. The formula assumes independent, uniform hashes, which double hashing approximates well.
What is double hashing in a Bloom filter?
A Bloom filter needs k bit positions per item, and computing k independent hash functions is slow. Double hashing computes two hashes, h1 and h2, and derives the rest as h1 plus i times h2, modulo m, for i from 0 to k minus 1. In this challenge h1 is hash32 and h2 is hb, which is hash32 of the item with a salt prepended. Kirsch and Mitzenmacher proved this costs nothing in the asymptotic false-positive rate. One caution: if h2 shares a factor with m, the positions repeat, so implementations pick a prime m or force h2 to be odd.
Why can you not delete an item from a standard Bloom filter?
Because bits are shared. Each bit may have been set by several different items, and the filter keeps no record of which ones. Clearing an item’s k bits to delete it would also clear bits that other inserted items depend on, and those items would then fail their lookups, which is a false negative, the one error a Bloom filter promises never to make. If you need deletion, use a counting Bloom filter, which keeps a small counter per position, or a cuckoo filter, which stores short fingerprints that can be removed individually.
This idea in other formats
Explore the topic
See this alongside everything else on the same subject — handbooks, system designs, challenges and tools, in one place.
More Challenges
- LRU CacheThe eviction policy behind every size-limited cache: when you run out of room, throw out whatever was used least recently. The trick is O(1) get and put — an ordered hash map (Python dict / JS Map) gives you exactly that. Solve it in Python or TypeScript.Read →
- Streaming MedianLatency dashboards do this every second: maintain the median of a stream without re-sorting per event. The classic two-heap trick — a max-heap for the low half, a min-heap for the high half, the median always at the boundary. Solve it in Python or TypeScript.Read →
- Token Bucket Rate LimiterThe algorithm inside most production rate limiters — and it never runs a timer. Refill the bucket lazily from the time elapsed since the last request, cap at capacity, spend one token or reject. Two numbers of state per client, exactly like the Redis version. Solve it in Python or TypeScript.Read →
- Sliding-Window Rate LimiterAllow at most N requests per rolling window — the rate limiter that guards real APIs. A sliding log of accepted timestamps gives exact limits without fixed-window bursts. Decide accept/reject for a stream. Solve it in Python or TypeScript, with hidden tests.Read →
- LFU CacheThe cache that evicts what you use least often — and, on ties, least recently. Harder than LRU: track frequency and recency together, still O(1) per op. Replay get/put operations. Solve it in Python or TypeScript, with hidden tests.Read →
- HyperLogLog Cardinality EstimatorCount how many distinct items a stream held — billions of them — using a few kilobytes, not a giant set. Turn "the longest run of leading zeros" into an estimate. Powers COUNT(DISTINCT) in Redis and BigQuery. Solve it in Python or TypeScript, with hidden tests.Read →