Hash Tables are a fundamental data structure in computer science, offering an incredibly efficient way to store and retrieve data. Many applications, like compilers maintaining symbol tables, rely on dynamic sets that support dictionary operations: INSERT, SEARCH, and DELETE. While a worst-case search can take O(n) time, in practice, hash tables achieve an average search time of O(1) under reasonable assumptions, making them extremely powerful. The core idea is to compute an array index directly from a key, but this can lead to collisions where different keys map to the same index. This article will break down how hash tables work and how they handle these conflicts.
Understanding Direct Addressing: The Foundation of Hash Tables
Before diving into hash tables, it's helpful to understand a simpler, related concept: direct addressing. This technique works well when the universe of possible keys (U) is relatively small. Imagine you have keys from 0 to m-1. A direct-address table, denoted as T[0...m-1], uses an array where each position, or slot, directly corresponds to a key. Slot k either points to an element with key k or contains NIL if no such element exists.
Operations like DIRECT-ADDRESS-SEARCH(T, k), DIRECT-ADDRESS-INSERT(T, x), and DIRECT-ADDRESS-DELETE(T, x) are incredibly fast, each taking only O(1) time. This efficiency stems from the ability to access any array position directly. Sometimes, the table itself can hold the elements, saving space by not needing external objects and pointers.
Limitations of Direct Addressing
The main drawback of direct addressing becomes apparent when the universe U of possible keys is very large. Allocating an array of size |U| might be impractical or impossible due to memory constraints. Furthermore, if the set of actual keys (K) being stored is tiny compared to U, most of the allocated space in T would be wasted. This is where hash tables provide a more memory-efficient solution.
How Hash Tables Work: Reducing Storage and Handling Collisions
Hash tables overcome the storage limitations of direct addressing by using an array (the hash table T) whose size m is typically much smaller than |U|. Instead of directly using the key k as an array index, a hash function h computes the slot from the key: h: U → {0, 1,..., m-1}. An element with key k hashes to slot h(k), and h(k) is called the hash value of key k.
This reduction in the range of array indices allows the hash table to be sized proportionally to the number of keys actually stored (Θ(|K|)). However, a significant challenge arises because |U| > m. This means there must be at least two different keys that map to the same slot, a situation called a collision. Effective collision resolution techniques are crucial for hash tables to perform well.
Collision Resolution by Chaining
Chaining is the simplest and most common technique for resolving collisions. When multiple elements hash to the same slot j, they are all placed into a linked list associated with that slot T[j]. Each slot T[j] contains a pointer to the head of its list. If no elements hash to j, the slot contains NIL.
Dictionary operations with chaining are straightforward:
CHAINED-HASH-INSERT(T, x): Insertsxat the head of the listT[h(x.key)]. This takes O(1) time (assumingxisn't already present). For faster deletion, lists should be doubly linked.CHAINED-HASH-SEARCH(T, k): Searches for an element with keykwithin the listT[h(k)].CHAINED-HASH-DELETE(T, x): Deletesxfrom its listT[h(x.key)]. With doubly linked lists, this is O(1).
Performance Analysis of Chaining
The performance of hashing with chaining is analyzed using the load factor α = n/m, where n is the number of elements and m is the number of slots. α represents the average number of elements per chain and can be less than, equal to, or greater than 1.
The worst-case performance is poor: if all n keys hash to the same slot, searching takes Θ(n) time, similar to a single linked list. However, hashing's strength lies in its average-case performance, which relies on a good hash function distributing keys well (the simple uniform hashing assumption).
- Unsuccessful Search: Under simple uniform hashing, an unsuccessful search takes
Θ(1 + α)average-case time. This includes computing the hash function and searching the expected length of the list, which isα. - Successful Search: A successful search also takes
Θ(1 + α)average-case time. This is because the expected number of elements examined is1 + (α/2) - (α/2n), which simplifies toΘ(1 + α).
If n = O(m) (meaning α = O(1)), then searching takes constant time on average. Combined with O(1) insertion and deletion, all dictionary operations can be supported in O(1) average time.
Designing Effective Hash Functions
A good hash function is crucial for distributing keys evenly across the hash table, closely approximating simple uniform hashing. This minimizes collisions and improves performance. Hash functions typically interpret keys as natural numbers. For instance, a character string can be converted to an integer using radix notation (e.g., "pt" as 112 * 128 + 116 = 14452 in ASCII radix-128).
The Division Method
The division method is straightforward: h(k) = k mod m. It computes the hash value as the remainder of k divided by m. It's very fast, requiring only a single division operation.
- Choosing
m: Avoidmbeing a power of 2 (e.g.,m = 2^p), ash(k)would only use theplowest-order bits ofk. A prime number not close to an exact power of 2 is often a good choice. For example, for 2000 strings, a primem = 701near2000/3is suitable.
The Multiplication Method
The multiplication method involves two steps: h(k) = floor(m * (kA mod 1)). Here, kA mod 1 refers to the fractional part of kA.
- Choosing
m: The value ofmis less critical here; it's often chosen as a power of 2 (m = 2^p) for efficient implementation. Knuth suggestsA ≈ (sqrt(5) - 1)/2 ≈ 0.618forA.
Universal Hashing: Guaranteeing Average-Case Performance
To prevent a malicious adversary from choosing keys that always result in worst-case collision behavior for a fixed hash function, universal hashing is employed. Here, the hash function h is chosen randomly from a carefully designed family of functions H at the beginning of execution.
A family H is universal if, for any pair of distinct keys k, l ∈ U, the number of hash functions h ∈ H for which h(k) = h(l) is at most |H|/m. This means the probability of a collision between k and l is no more than 1/m.
One common universal family H_pm is defined as h_ab(k) = ((ak + b) mod p) mod m, where p is a prime greater than any key, a ∈ Z_p* (1 to p-1), and b ∈ Z_p (0 to p-1). This guarantees average-case O(1) performance for dictionary operations using chaining, regardless of the input key distribution.
Flashcards
Tap to flip · Swipe to navigate
Open Addressing: Storing Elements Directly in the Table
Open addressing is an alternative collision resolution technique where all elements are stored directly within the hash table itself, without using linked lists. Each table entry contains either an element or NIL (empty). This implies that the load factor α can never exceed 1 (n ≤ m). The advantage is saving memory by avoiding pointers.
To insert a key k, we systematically probe table slots until an empty one is found. The probe sequence ⟨h(k, 0), h(k, 1),..., h(k, m-1)⟩ is computed by extending the hash function to include a probe number i. This sequence must be a permutation of ⟨0, 1,..., m-1⟩, ensuring every slot is eventually considered.
HASH-INSERT(T, k): Probes slotsj = h(k, i)fori = 0, 1,...untilT[j]isNIL, then insertskand returnsj. Ifireachesm, the table is full.HASH-SEARCH(T, k): Follows the same probe sequence asHASH-INSERT. It returnsjifT[j] == k, orNILif an empty slot is found (meaningkisn't in the table) ormprobes are exhausted.
Deletion Challenges in Open Addressing
Deleting from an open-address hash table is complex. Simply marking a slot as NIL can break subsequent search paths for keys that probed through that slot. To fix this, a special DELETED value is used instead of NIL. HASH-INSERT treats DELETED slots as empty, while HASH-SEARCH passes over them. However, using DELETED values can degrade performance over time, as search times no longer depend solely on α. Because of this, chaining is often preferred when deletions are frequent.
Open Addressing Probe Techniques
Practical approximations to uniform hashing (where each key's probe sequence is equally likely to be any permutation of ⟨0,..., m-1⟩) are used:
- Linear Probing: Uses
h(k, i) = (h_0(k) + i) mod m, whereh_0is an auxiliary hash function. This probesT[h_0(k)], thenT[h_0(k)+1], and so on. It's easy to implement but suffers from primary clustering, where long runs of occupied slots form, increasing average search time. - Quadratic Probing: Uses
h(k, i) = (h_0(k) + c_1*i + c_2*i^2) mod m. This method reduces primary clustering but can lead to secondary clustering, where keys with the same initial hash value (h_0(k)) follow identical probe sequences. The constantsc1,c2, andmmust be chosen carefully for full table utilization. - Double Hashing: Offers one of the best methods:
h(k, i) = (h_1(k) + i*h_2(k)) mod m. Bothh_1andh_2are auxiliary hash functions. The probe sequence depends on two aspects of the key, resulting inΘ(m^2)distinct probe sequences (compared toΘ(m)for linear/quadratic). For the entire table to be searched,h_2(k)must be relatively prime tom. This can be achieved by makingmprime andh_2(k)return a positive integer less thanm(e.g.,h_2(k) = 1 + (k mod (m-1))). Double hashing closely approximates uniform hashing performance.
Performance Analysis of Open Addressing
Under the assumption of uniform hashing:
- Unsuccessful Search: The expected number of probes is at most
1/(1 - α). Forα = 0.5, it's≤ 2probes; forα = 0.9, it's≤ 10probes. - Insertion: Since insertion is an unsuccessful search followed by placing the key, it also takes at most
1/(1 - α)probes on average. - Successful Search: The expected number of probes is at most
(1/α) * ln(1 / (1 - α)). Forα = 0.5, it's< 1.387probes; forα = 0.9, it's< 2.559probes.
Perfect Hashing: Worst-Case O(1) Search for Static Sets
For static sets of keys (where keys never change once stored, like reserved words in a programming language), perfect hashing guarantees O(1) memory accesses in the worst case for searches. This is achieved using a two-level hashing scheme with universal hashing at each level.
- First Level:
nkeys are hashed intomslots (typicallym = n) using a hash functionhchosen from a universal familyH_pm. - Secondary Level: Instead of a linked list, each slot
jgets its own smaller, secondary hash tableS_j. Ifn_jkeys hash to slotj,S_jhas sizem_j = n_j^2and uses its own hash functionh_jchosen from a universal familyH_p,m_j.
The quadratic sizing m_j = n_j^2 guarantees no collisions within S_j, ensuring O(1) lookup. Despite this, by choosing the first-level hash function appropriately, the expected total storage for all secondary tables is O(n). This means that with a few random trials, a suitable first-level hash function can be found that minimizes overall storage and ensures perfect hashing.
Frequently Asked Questions about Hash Tables
What is the main purpose of a hash table?
A hash table's main purpose is to implement dynamic sets or dictionaries, providing highly efficient (average O(1) time) operations for inserting, searching, and deleting elements based on their keys. It's ideal for scenarios where quick lookup is essential.
Why are collisions unavoidable in hash tables?
Collisions are unavoidable because the universe of possible keys (U) is typically much larger than the number of available slots (m) in the hash table. By the Pigeonhole Principle, if you have more items than containers, at least one container must hold more than one item. Similarly, more keys than slots means some keys must map to the same slot.
What's the difference between chaining and open addressing?
Chaining resolves collisions by storing all elements that hash to the same slot in a linked list attached to that slot. Open addressing, conversely, stores all elements directly within the hash table itself. When a collision occurs in open addressing, the system probes for the next available empty slot based on a predefined sequence.