Day 4: Hash Tables
What You'll Learn Today
- How hash tables work
- The role of hash functions
- Collision resolution strategies
- JavaScript's Map and Set
What Is a Hash Table?
A hash table stores key-value pairs and provides O(1) average-time lookups by key.
flowchart LR
K1["key: 'name'"] --> HF["Hash Function"]
HF --> I["index: 3"]
I --> B["Bucket[3]"]
B --> V["value: 'Alice'"]
style HF fill:#3b82f6,color:#fff
style B fill:#22c55e,color:#fff
How It Works
- Pass the key to a hash function
- The hash function produces an index (number)
- Store the data at that index position
Hash Functions
A hash function converts arbitrary data into a fixed-size number. A good hash function must be:
- Deterministic: Same input always produces the same output
- Uniformly distributed: Outputs spread evenly across buckets
- Fast: Quick to compute
function hash(key, tableSize) {
let total = 0;
for (let i = 0; i < key.length; i++) {
total += key.charCodeAt(i);
}
return total % tableSize;
}
console.log(hash("name", 10));
console.log(hash("age", 10));
A Better Hash Function
function betterHash(key, tableSize) {
const PRIME = 31;
let hash = 0;
for (let i = 0; i < key.length; i++) {
hash = (hash * PRIME + key.charCodeAt(i)) % tableSize;
}
return hash;
}
Hash Table Implementation
class HashTable {
constructor(size = 53) {
this.table = new Array(size);
this.size = size;
this.count = 0;
}
_hash(key) {
const PRIME = 31;
let hash = 0;
for (let i = 0; i < Math.min(key.length, 100); i++) {
hash = (hash * PRIME + key.charCodeAt(i)) % this.size;
}
return hash;
}
set(key, value) {
const index = this._hash(key);
if (!this.table[index]) {
this.table[index] = [];
}
const existing = this.table[index].find(pair => pair[0] === key);
if (existing) {
existing[1] = value;
} else {
this.table[index].push([key, value]);
this.count++;
}
}
get(key) {
const index = this._hash(key);
if (!this.table[index]) return undefined;
const pair = this.table[index].find(pair => pair[0] === key);
return pair ? pair[1] : undefined;
}
delete(key) {
const index = this._hash(key);
if (!this.table[index]) return false;
const pairIndex = this.table[index].findIndex(pair => pair[0] === key);
if (pairIndex === -1) return false;
this.table[index].splice(pairIndex, 1);
this.count--;
return true;
}
has(key) {
return this.get(key) !== undefined;
}
keys() {
const result = [];
for (const bucket of this.table) {
if (bucket) {
for (const [key] of bucket) {
result.push(key);
}
}
}
return result;
}
}
const ht = new HashTable();
ht.set("name", "Alice");
ht.set("age", 30);
console.log(ht.get("name")); // "Alice"
console.log(ht.has("age")); // true
Collisions
A collision occurs when different keys map to the same index.
flowchart TB
subgraph Collision["Collision Example"]
K1["'abc'"] --> H["hash()"] --> I3["index: 3"]
K2["'cab'"] --> H2["hash()"] --> I3
end
style Collision fill:#ef4444,color:#fff
Solution 1: Separate Chaining
Store multiple entries at the same index using a list (used in the implementation above).
flowchart TB
subgraph Table["Hash Table"]
B0["[0]"] --> L0["null"]
B1["[1]"] --> P1["('name','Alice')"] --> P2["('email','alice@example.com')"]
B2["[2]"] --> L2["null"]
B3["[3]"] --> P3["('age', 30)"]
end
style Table fill:#3b82f6,color:#fff
Solution 2: Open Addressing
When a collision occurs, probe for the next available slot.
class OpenAddressingHashTable {
constructor(size = 53) {
this.table = new Array(size);
this.size = size;
}
_hash(key) {
const PRIME = 31;
let hash = 0;
for (let i = 0; i < Math.min(key.length, 100); i++) {
hash = (hash * PRIME + key.charCodeAt(i)) % this.size;
}
return hash;
}
set(key, value) {
let index = this._hash(key);
while (this.table[index] !== undefined && this.table[index][0] !== key) {
index = (index + 1) % this.size;
}
this.table[index] = [key, value];
}
get(key) {
let index = this._hash(key);
while (this.table[index] !== undefined) {
if (this.table[index][0] === key) {
return this.table[index][1];
}
index = (index + 1) % this.size;
}
return undefined;
}
}
Load Factor and Resizing
Load Factor = number of entries / table size
As the load factor increases, collisions become more frequent and performance degrades. Typically, resize when the load factor exceeds 0.7β0.75.
| Load Factor | Performance |
|---|---|
| < 0.5 | Fast |
| 0.5 β 0.75 | Good |
| > 0.75 | Collisions increase, resize recommended |
JavaScript's Map and Set
Map
const map = new Map();
map.set("name", "Alice");
map.set(42, "answer");
map.set(true, "boolean key");
console.log(map.get("name")); // "Alice"
console.log(map.has(42)); // true
map.delete(true);
console.log(map.size); // 2
for (const [key, value] of map) {
console.log(`${key}: ${value}`);
}
Set
const set = new Set();
set.add(1);
set.add(2);
set.add(2); // Duplicate ignored
set.add(3);
console.log(set.has(2)); // true
console.log(set.size); // 3
set.delete(2);
const unique = [...new Set([1, 2, 2, 3, 3, 4])];
console.log(unique); // [1, 2, 3, 4]
Object vs. Map
| Feature | Object | Map |
|---|---|---|
| Key types | String/Symbol | Any type |
| Order | Insertion order (limited) | Insertion order |
| Size | Object.keys(o).length |
map.size |
| Iteration | Indirect | Direct (for...of) |
| Perf (frequent add/remove) | Lower | Higher |
Practical Examples
1. Frequency Count
function countFrequency(arr) {
const freq = new Map();
for (const item of arr) {
freq.set(item, (freq.get(item) || 0) + 1);
}
return freq;
}
const fruits = ["apple", "banana", "apple", "cherry", "banana", "apple"];
console.log(countFrequency(fruits));
// Map { 'apple' => 3, 'banana' => 2, 'cherry' => 1 }
2. Two Sum
function twoSum(nums, target) {
const map = new Map();
for (let i = 0; i < nums.length; i++) {
const complement = target - nums[i];
if (map.has(complement)) {
return [map.get(complement), i];
}
map.set(nums[i], i);
}
return [];
}
console.log(twoSum([2, 7, 11, 15], 9)); // [0, 1]
3. Anagram Check
function isAnagram(s, t) {
if (s.length !== t.length) return false;
const count = new Map();
for (const char of s) {
count.set(char, (count.get(char) || 0) + 1);
}
for (const char of t) {
const c = count.get(char);
if (!c) return false;
count.set(char, c - 1);
}
return true;
}
console.log(isAnagram("listen", "silent")); // true
console.log(isAnagram("hello", "world")); // false
Hash Table Time Complexities
| Operation | Average | Worst |
|---|---|---|
| Lookup | O(1) | O(n) |
| Insert | O(1) | O(n) |
| Delete | O(1) | O(n) |
The worst case occurs when all elements hash to the same bucket. A good hash function and proper resizing keep operations effectively O(1).
Summary
| Concept | Description |
|---|---|
| Hash Table | Data structure providing O(1) key-value lookups |
| Hash Function | Converts keys into numeric indices |
| Collision | Different keys mapping to the same index |
| Chaining | Resolves collisions using linked lists |
| Load Factor | Performance metric; resize above 0.75 |
Key Takeaways
- Hash tables provide O(1) average-time lookups, inserts, and deletes
- A good hash function minimizes collisions
- JavaScript provides built-in
MapandSet - Prefer
MapoverObjectfor frequent additions and deletions
Practice Problems
Problem 1: Basic
Implement a function that finds the first non-repeating character in a string.
Problem 2: Intermediate
Implement a function that returns the intersection of two arrays using a Set.
Challenge
Find the length of the longest substring without repeating characters (hint: sliding window + hash map).
References
Next up: In Day 5, we'll explore "Recursion & Divide and Conquer" β a powerful technique for breaking problems into smaller pieces.