What is a Bloom Filter?
A Bloom Filter is a space-efficient, probabilistic data structure used to test whether an element is a member of a set.
- False Positives: Possible. The filter might claim an element is in the set when it is not.
- False Negatives: Impossible. If the filter claims an element is not in the set, it is definitely not.
- Deletions: Standard Bloom Filters do not support deleting elements.
Explanation
- When storing massive sets of data (e.g., millions of malicious URLs or username registrations), storing the actual keys in memory becomes too expensive. A Bloom Filter uses a compact bit array to represent membership, discarding the keys entirely.
Real-World Analogy
- Physical Mailroom Checklist: A mailroom clerk keeps a checklist of families who have packages. Instead of writing full names, they write initials. If someone asks for a package for “John Doe” (initials JD), and the initials “JD” are on the list, there might be a package (or it could be for “Jane Davis” — a false positive). But if “JD” is not on the list, there is definitely no package (no false negative).
- Database Cache Filtering (LSM Trees / RocksDB): Before reading a heavy block file from disk to look up a key, the database queries an in-memory Bloom Filter. If it returns False, the database skips the disk read entirely.
How It Works
Core Mechanics
- A Bloom Filter consists of:
- A bit array of size , initially all set to
0. - different hash functions, each mapping a key to one of the bit positions with a uniform random distribution.
- A bit array of size , initially all set to
1. Insertion
- To insert an element, feed it to each of the hash functions to get indices.
- Set the bits at all these indices to
1.
2. Query (Lookup)
- To query an element, hash it using the hash functions to get indices.
- Check if the bits at all these indices are
1. - If any bit is
0, the element is definitely not in the set. - If all bits are
1, the element is probably in the set.
Mathematical Formulas and Tuning
- Given:
- = expected number of elements to be inserted.
- = acceptable false positive probability (e.g.
0.01for 1%).
- The optimal size of the bit array is:
- The optimal number of hash functions is:
Visual Walkthrough
Initial empty bit array (size , hash functions )
Index: 0 1 2 3 4 5 6 7
Bit: 0 0 0 0 0 0 0 0
Insert “apple” (hashes to 1 and 4)
Index: 0 1 2 3 4 5 6 7
Bit: 0 1 0 0 1 0 0 0
Insert “banana” (hashes to 4 and 6)
Index: 0 1 2 3 4 5 6 7
Bit: 0 1 0 0 1 0 1 0
Query “apple” (hashes to 1 and 4)
- Bit at 1 is
1, Bit at 4 is1. Returns True (correct).
Query “cherry” (hashes to 1 and 6)
- Bit at 1 is
1, Bit at 6 is1. Returns True (False Positive! “cherry” was never inserted, but both bits were set by others).
Time & Space Complexity
| Operation | Time Complexity | Space Complexity |
|---|---|---|
| Insertion | bits | |
| Query | auxiliary | |
| Deletions | Not Supported | - |
- Note: is the number of hash functions. Since is a small constant (typically between 3 and 10), insertion and query are effectively operations.
Implementation
import math
class BloomFilter:
def __init__(self, expected_elements, false_positive_rate):
"""
Initializes the Bloom Filter with optimal bit-array size (m)
and optimal number of hash functions (k).
"""
self.n = expected_elements
self.p = false_positive_rate
# Calculate optimal size m and optimal hash count k
self.m = int(- (self.n * math.log(self.p)) / (math.log(2) ** 2))
self.k = max(1, int((self.m / self.n) * math.log(2)))
self.bit_array = [0] * self.m
def _fnv1a_hash(self, key, seed):
"""Computes an FNV-1a hash value for a key with a seed offset."""
# FNV parameters for 64-bit hashing
fnv_prime = 1099511628211
fnv_offset_basis = 14695981039346656037
# Standard hashing representation
hash_val = fnv_offset_basis ^ seed
for char in str(key):
hash_val ^= ord(char)
hash_val = (hash_val * fnv_prime) & 0xFFFFFFFFFFFFFFFF
return hash_val % self.m
def insert(self, item):
"""Inserts an item by setting bits at hashed indices."""
for i in range(self.k):
idx = self._fnv1a_hash(item, i)
self.bit_array[idx] = 1
def query(self, item):
"""
Queries membership.
Returns False if definitely not in set.
Returns True if probably in set.
"""
for i in range(self.k):
idx = self._fnv1a_hash(item, i)
if self.bit_array[idx] == 0:
return False
return True
# Example Usage
if __name__ == "__main__":
bf = BloomFilter(expected_elements=1000, false_positive_rate=0.01)
print(f"Optimal array size: {bf.m} bits, hash count: {bf.k}")
bf.insert("apple")
bf.insert("banana")
print("Query apple:", bf.query("apple")) # True
print("Query banana:", bf.query("banana")) # True
print("Query cherry:", bf.query("cherry")) # False (most likely)#include <iostream>
#include <vector>
#include <string>
#include <cmath>
#include <algorithm>
class BloomFilter {
private:
int n;
double p;
int m;
int k;
std::vector<bool> bitArray;
// Computes an FNV-1a hash value for a key with a seed offset
int fnv1aHash(const std::string& key, int seed) const {
unsigned long long fnv_prime = 1099511628211ULL;
unsigned long long hash_val = 14695981039346656037ULL ^ seed;
for (char char_byte : key) {
hash_val ^= static_cast<unsigned char>(char_byte);
hash_val *= fnv_prime;
}
return static_cast<int>(hash_val % m);
}
public:
BloomFilter(int expectedElements, double falsePositiveRate)
: n(expectedElements), p(falsePositiveRate) {
// Calculate optimal size m and optimal hash count k
m = static_cast<int>(- (n * std::log(p)) / (std::log(2) * std::log(2)));
k = std::max(1, static_cast<int>((m / static_cast<double>(n)) * std::log(2)));
bitArray.assign(m, false);
}
int getBitSize() const { return m; }
int getHashCount() const { return k; }
// Inserts an item by setting bits at hashed indices
void insert(const std::string& item) {
for (int i = 0; i < k; ++i) {
int idx = fnv1aHash(item, i);
bitArray[idx] = true;
}
}
// Queries membership
bool query(const std::string& item) const {
for (int i = 0; i < k; ++i) {
int idx = fnv1aHash(item, i);
if (!bitArray[idx]) {
return false;
}
}
return true;
}
};
int main() {
BloomFilter bf(1000, 0.01);
std::cout << "Optimal array size: " << bf.getBitSize() << " bits, hash count: " << bf.getHashCount() << "\n";
bf.insert("apple");
bf.insert("banana");
std::cout << std::boolalpha;
std::cout << "Query apple: " << bf.query("apple") << "\n"; // true
std::cout << "Query banana: " << bf.query("banana") << "\n"; // true
std::cout << "Query cherry: " << bf.query("cherry") << "\n"; // false
return 0;
}
When to Use
✅ Use Bloom Filters When:
- You need to reduce expensive disk lookups, network requests, or database scans by failing fast for non-existent keys (e.g. CDNs checking for cached content).
- Memory is highly constrained, and storing the keys themselves is impossible or impractical.
- False positives are acceptable, and you have a reliable secondary source to confirm true membership (e.g. a database search).
❌ Do NOT Use Bloom Filters When:
- Deleting elements is a strict requirement (use a Counting Bloom Filter instead).
- False positives are absolutely unacceptable (e.g. bank account balance verification or authentication systems).
- You need to list or iterate over all items currently stored in the set.
Variations & Related Concepts
- Counting Bloom Filter: Replaces each bit in the array with a small counter. Insertion increments the counter, and deletion decrements it, allowing dynamic item removal.
- Cuckoo Filter: A newer alternative using cuckoo hashing tables to store fingerprints. It supports deletions and achieves better space efficiency for low false positive rates.
- Quotient Filter: A cache-friendly alternative that stores fingerprints in a hash table using quotienting, supporting resizing.
Key Takeaways
- Bloom Filters provide space-efficient membership testing with zero false negatives but possible false positives.
- Optimal sizing () and hashing count () depend on the expected element count () and desired error rate ().
- Built using simple, fast non-cryptographic hashing (like FNV-1a or MurmurHash).
- Crucial component in databases, CDNs, network routers, and caching layers.