DSA Notes
Explore hash function design — division method, multiplication method, universal hashing, polynomial rolling hash, and properties that make a hash function effective.
What Makes a Good Hash Function?
A hash function maps keys from a large universe (all possible strings, integers, etc.) to a small range [0, m-1] where m is the table size. A good hash function should:
- Deterministic: Same key always produces same hash
- Uniform distribution: Keys spread evenly across buckets (minimizes collisions)
- Efficient to compute: O(1) or O(key_length) at most
- Avalanche effect: Small changes in key produce vastly different hashes
- Minimize clustering: Similar keys should not map to adjacent slots
Division Method
The simplest hash function: h(k) = k mod m
def hash_division(key, table_size):
return key % table_sizeChoosing m: The table size matters greatly.
- Bad: m = power of 2 (e.g., 1024) — only the lowest bits of the key matter. Keys with similar low bits cluster.
- Bad: m = even number — odd and even keys separate into distinct halves.
- Good: m = prime not close to a power of 2 — distributes keys more uniformly.
Multiplication Method
h(k) = floor(m × (k × A mod 1)) where A is a constant 0 < A < 1.
Knuth recommends A = (√5 - 1)/2 ≈ 0.6180339887 (the golden ratio conjugate).
import math
def hash_multiplication(key, table_size):
A = 0.6180339887 # golden ratio conjugate
fractional_part = (key * A) % 1 # extract fractional part
return int(table_size * fractional_part)
# Examples with table_size = 16:
# h(1) = floor(16 × 0.618) = 9
# h(2) = floor(16 × 0.236) = 3
# h(3) = floor(16 × 0.854) = 13
# h(4) = floor(16 × 0.472) = 7
# Beautiful spread! No clustering.Advantage: The value of m does not matter — works well even with powers of 2. This is why multiplication method is preferred in practice when table size is a power of 2.
Polynomial Rolling Hash (For Strings)
For string keys, treat each character as a coefficient in a polynomial:
h(s) = (s[0]×p^(n-1) + s[1]×p^(n-2) + ... + s[n-1]×p^0) mod m
def hash_polynomial(key, table_size, p=31):
"""
p = small prime (31 or 37 for lowercase letters, 53 for mixed case)
"""
hash_value = 0
p_power = 1
for char in key:
hash_value = (hash_value + (ord(char) - ord('a') + 1) * p_power) % table_size
p_power = (p_power * p) % table_size
return hash_value
# Example: hash("abc") with p=31, m=1000000007
# = 1×1 + 2×31 + 3×961 = 1 + 62 + 2883 = 2946Rolling property: When computing hashes of all substrings (e.g., Rabin-Karp pattern matching), you can compute the next hash from the previous one in O(1) by subtracting the outgoing character and adding the incoming one.
Java's String hashCode (Real-World Example)
Java uses: h = s[0]×31^(n-1) + s[1]×31^(n-2) + ... + s[n-1]
Why 31? It is an odd prime, and 31 × i can be optimized as (i << 5) - i by the compiler.
Universal Hashing
No single hash function is safe against adversarial inputs. Universal hashing randomly selects a hash function from a family at runtime:
h_{a,b}(k) = ((a×k + b) mod p) mod m
where:
- p is a prime larger than the key universe
- a is random in [1, p-1]
- b is random in [0, p-1]
Why it matters: Prevents hash-flooding DoS attacks. Even if an attacker knows the algorithm, they cannot predict which function was chosen.
Tabulation Hashing
Split the key into bytes, look up each byte in a random table, and XOR the results:
Fast (4 lookups + 3 XORs), high quality, and theoretically 3-wise independent.
Comparing Hash Functions
| Method | Speed | Distribution Quality | Table Size Restriction |
|---|---|---|---|
| Division | Very fast | Depends on m choice | m should be prime |
| Multiplication | Fast | Very good | None (works with 2^k) |
| Polynomial | O(key length) | Good for strings | None |
| Universal | Fast | Guaranteed uniform (probabilistic) | None |
| Tabulation | Very fast | Excellent | None |
Cryptographic vs Non-Cryptographic Hashes
| Property | Hash Table Hash | Cryptographic Hash (SHA-256) |
|---|---|---|
| Speed | Very fast | Deliberately slow |
| Output size | Small (table index) | Fixed large (256 bits) |
| Reversible? | Not a concern | Must be infeasible |
| Collision resistance | Best effort | Cryptographically guaranteed |
| Use case | Data structures | Security, integrity |
For hash tables, we do NOT need cryptographic hashes — they are too slow. We need fast functions with good distribution.
Practical Guidelines
- Integer keys: Use multiplication method with golden ratio, or
(key * 2654435769) >> (32 - log2(m))for power-of-2 table sizes - String keys: Polynomial hash with p=31 (lowercase) or p=53 (mixed case)
- Composite keys: Combine component hashes:
hash = h1 * 31 + h2or XOR with rotation - Security-sensitive: Use universal or SipHash (Python 3.4+ default for strings)
Real-World Applications
- HashMap/dict implementations: Every standard library needs an efficient hash function
- Bloom filters: Use multiple hash functions for probabilistic set membership
- Consistent hashing: Load balancers distribute requests using hash ring
- Rabin-Karp string matching: Rolling polynomial hash enables O(n) pattern search
- Checksums: CRC32, xxHash for fast data integrity verification
Key Takeaways
The choice of hash function profoundly affects hash table performance. Division method is simple but table-size-sensitive. Multiplication method handles power-of-2 sizes elegantly. Polynomial hashing is the standard for strings. Universal hashing provides worst-case guarantees against adversarial inputs. Always match your hash function to your key type and security requirements.
Exam Focus
Revise definitions, diagrams, examples, and short-answer points for Hash Functions.
Interview Use
Prepare one clear explanation, one practical example, and one common mistake for this Data Structures & Algorithms topic.
Search Terms
data-structures-algorithms, data structures & algorithms, data, structures, algorithms, hashing, hash, functions
Related Data Structures & Algorithms Topics