Hash Table
Data structure for efficient key-value mapping.
Hash Table
Definition
A hash table is a data structure that maps keys to values for efficient data retrieval. It uses a hash function to compute an index (hash code) into an array of buckets or slots, from which the desired value can be found.
Denote as the universe set, as the hash table set, and as the set of keys to be stored. A hash function . (.)
Collision Resolution
Collisions occur when two keys hash to the same index. Common collision resolution techniques include:
- Chaining: Each bucket contains a linked list of all entries that hash to the same index.
- Open Addressing: If a collision occurs, the algorithm searches for the next available slot using a probing sequence.
Load
Denote as the load of the -th bucket, i.e., the number of elements hashed to that bucket.
Load Factor
The load factor is a measure of how full the hash table is, defined as , where is the number of elements and is the number of buckets. A higher load factor increases the likelihood of collisions and can degrade performance.
When the load factor exceeds a certain threshold (e.g., 0.7), the hash table may be resized (usually doubled) to maintain efficient operations. This involves rehashing all existing keys into the new table.
Hash Function
A good hash function has the following properties:
- Deterministic: The same input always produces the same output.
- Uniform Distribution: Hash values are evenly distributed across the hash table to minimize collisions.
- is mutually independent of for .
- Efficient: The hash function should be quick to compute.
Every hash function has a bad input where all elements map to the same bucket.
Naive hash functions include:
- Division Method:
Random Hash Function
Theorem: The max load in chaining is about with high probability.
Let be the event that the -th bucket has load at least . Then .
, .
Two Hash Functions
Theorem: Using two hash functions and placing each element in the less loaded bucket, the maximum load is about with high probability.
For hash functions, the maximum load is about with high probability.
Pairwise Independent Hash Function
(ideally mutually independent)
For any two distinct keys and , the hash values and are uniformly distributed over the output range and are independent of each other. for any in the range of the hash function.
Universal Hash Function
- choose a prime (Merseenne prime preferred, )
- choose and uniformly at random
- define the hash function as
is the number of buckets in the hash table. For any two distinct keys , .
Cuckoo Hashing
Cuckoo hashing is a collision resolution technique for hash tables that uses two or more hash functions and two or more hash tables. Each key can be stored in one of the tables based on its hash value. If a collision occurs, the existing key is "kicked out" and reinserted into its alternative location, potentially causing further displacements.
(one table, two hash functions in this case)
Operations:
- Insertion: Insert the key based on one of its hash values. If all positions are occupied, remove the existing key and reinsert it into its alternative location, potentially causing further displacements. If a cycle exists, rehash the entire table with new hash functions. amortized in expectation.
- Query: An element exists if either of its hashed positions contains the key. worst-case.
- Deletion: Remove the key from the table if it exists.
Analysis: If , then
- The expected insertion is .
- The probability that a rehash is needed is .
- The expected number of rehashes is .
- The expected total time for rehashing is .
class CuckooHashTable:
TABLE_SIZE = 11
MAX_LOOP = 20
def __init__(self):
self.EMPTY = None
self.table = [self.EMPTY] * self.TABLE_SIZE
# store both hash functions in a list
self.hash_funcs = [
lambda key: key % self.TABLE_SIZE,
lambda key: (key // self.TABLE_SIZE) % self.TABLE_SIZE,
]
def search(self, key):
return any(self.table[h(key)] == key for h in self.hash_funcs)
def insert(self, key):
if self.search(key):
return
for h in self.hash_funcs: # try both positions first
if self.table[h(key)] is self.EMPTY:
self.table[h(key)] = key
return
cur_key, pos = key, self.hash_funcs[0](key)
for _ in range(self.MAX_LOOP):
cur_key, self.table[pos] = self.table[pos], cur_key # kick out the occupant
p0, p1 = (h(cur_key) for h in self.hash_funcs)
pos = p1 if pos == p0 else p0 # the evicted key goes to its other position
if self.table[pos] is self.EMPTY:
self.table[pos] = cur_key
return
print(f"Rehash needed! Insertion failed for {cur_key}")
def remove(self, key):
for h in self.hash_funcs:
pos = h(key)
if self.table[pos] == key:
self.table[pos] = self.EMPTY
return
def display(self):
print("Table:", ["_" if x is None else x for x in self.table])
if __name__ == "__main__":
ht = CuckooHashTable()
keys = [20, 50, 53, 75, 100, 67, 105, 3, 36, 39]
for k in keys:
ht.insert(k)
ht.display()
print("Search 75:", ht.search(75))
ht.remove(75)
ht.display()
Bloom Filter
A Bloom filter is a space-efficient probabilistic data structure used to test whether an element is a member of a set.
Structure
A Bloom filter consists of:
- A bit array of size , initialized to all 0s.
- independent hash functions, each of which maps an element to one of the array positions uniformly at random. (ideally mutually independent)
Operations
- Insertion: To add an element, compute the hash values using the hash functions. Set the bits at the resulting positions in the bit array to 1.
- Query: To check if an element is in the set, compute the hash values.
- indicates that the element is possibly in the set.
- indicates that the element is definitely not in the set.
- No deletion.
Advantages and Disadvantages
- Advantages:
- Space-efficient: Requires significantly less memory than traditional data structures for set membership.
- Fast operations: Both insertion and query operations are performed in constant time, , where is the number of hash functions.
- Simple to implement.
- Disadvantages:
- False positives but no false negatives: May indicate that an element is in the set when it is not, but will never indicate that an element is not in the set when it actually is.
- No deletion: Standard Bloom filters do not support deletion of elements.
Analysis
Given elements inserted into a Bloom filter with bits and hash functions, for a specific bit : (using the approximation )
To minimize the false positive rate is to minimize , and is equivalent to minimizing .
The trivial solution is when .
Thus, .
Let ,
Notice that, .
Therefore, the optimal is:
Algorithm
class BloomFilter:
def __init__(self, m, k, hash_class):
self.m = m
self.k = k
self.bits = [0] * m
self.hash_functions = [hash_class(m) for _ in range(k)]
def add(self, x):
for hf in self.hash_functions:
self.bits[hf.hash(x)] = 1
def contains(self, x):
for hf in self.hash_functions:
if self.bits[hf.hash(x)] == 0:
return False
return True
Applications
Reference: Network applications of Bloom filters (2003). Bloom filters are widely used in various applications, including:
- Database Systems: To quickly check if a record exists before performing a more expensive database query.
- Web Caching: To determine if a URL is in the cache before fetching it from the web.
- Distributed Systems: To reduce the amount of data transferred between nodes by checking membership before sending data.
- Spell Checking: To quickly check if a word is in a dictionary.
- Network Security: To filter out known malicious URLs or IP addresses.
- Blockchain and Cryptocurrencies: To efficiently verify transactions and blocks without downloading the entire blockchain.
Perfect Hashing
Birthday paradox
Define as the probability that people have distinct birthdays. Let . .
Since for all , .
For example, ; . For at least 23 people, the probability that at least two share a birthday is at least 50%.
In general, for possible values and items, if , then the probability of a collision is at least 0.5.
Static Perfect Hashing
Static set of keys from a large universe . Want worst-case lookup time with space.
Choose . Use a universal hash function to map keys to slots. Expected number of collisions is . If there are collisions, rehash with a new until no collisions. Expected number of trials is 2. Total space is .
Two-Level Scheme
- First level: choose a universal hash function . Let be the number of keys hashed to slot .
- check if . If not, rehash with a new until it holds. Expected number of trials is 2.
- Second level: for each slot with keys, create a secondary hash table of size using a new universal hash function . The expected number of collisions in this secondary table is . Rehash until no collisions. Expected number of trials is 2.
- Total space: . We have . Thus total space is .
- Lookup: compute first-level hash to find slot , then compute second-level hash to find the key in time.
Proof that with probability at least 0.5:
.
.
.
.
Thus .
So .
By Markov's inequality, .
Therefore, .