Hash tables are fundamental data structures that play a crucial role in efficient data management, particularly for implementing dictionary data structures. They allow us to store and retrieve data quickly, generalizing the concept of a simple array. While direct-address tables offer O(1) worst-case performance, they are often impractical for large key universes due to excessive memory requirements. Hash tables address this by using a special function to map keys to a smaller, more manageable array, achieving excellent average-case performance.
Understanding Hash Tables: An Efficient Dictionary Data Structure
A dictionary data structure needs to support three core operations: INSERT, SEARCH, and DELETE. In a direct-address table, an element with key k is stored directly in slot k. This is incredibly fast, but if the universe of possible keys is very large, allocating an array for every possible key becomes impossible. Most of the memory would be wasted if only a small subset of keys is actually stored.
Hash tables solve this space issue. Instead of directly using the key as an index, we use a hash function, h, to compute the slot for a given key k. This function maps the vast universe U of keys to a much smaller range of array indices: h: U -> {0, 1,..., m-1}, where m is the size of the hash table. We say that an element with key k hashes to slot h(k). The value h(k) is also known as the hash value of key k.
The Challenge of Collisions
Since the universe of keys U is typically much larger than the hash table size m (|U| > m), it's inevitable that multiple distinct keys will map to the same slot. This situation is called a collision. While a well-designed hash function aims to minimize collisions, they cannot be entirely avoided. Therefore, effective techniques are needed to resolve these conflicts.
Collision Resolution by Chaining: A Practical Approach
One of the simplest and most common techniques for resolving collisions is chaining. In this method, all elements that hash to the same slot are placed into a linked list. Each slot j in the hash table T contains a pointer to the head of the list of all stored elements whose hash value is j. If no elements hash to a particular slot, that slot contains NIL.
Dictionary Operations with Chaining
Implementing dictionary operations with chaining is straightforward:
CHAINED-HASH-INSERT(T, x): Inserts elementxat the head of the listT[h(x.key)]. This operation takes O(1) worst-case time, assuming the element isn't already present.CHAINED-HASH-SEARCH(T, k): Searches for an element with keykwithin the listT[h(k)]. The running time depends on the length of the list.CHAINED-HASH-DELETE(T, x): Deletes elementxfrom the listT[h(x.key)]. If the linked lists are doubly linked, deletion also takes O(1) time, as we don't need to search for the element's predecessor to update pointers.
Analyzing Performance with Chaining
To understand the performance of hashing with chaining, we use the load factor, denoted by α. For a hash table T with m slots storing n elements, α = n/m. This represents the average number of elements stored in a chain. The load factor can be less than, equal to, or greater than 1.
- Worst-Case Performance: The worst-case scenario occurs when all
nkeys hash to the same slot, creating a single list of lengthn. In this case, searching takes Θ(n) time, which is no better than a single linked list. Hash tables are not typically chosen for their worst-case performance. - Average-Case Performance: The average-case performance heavily relies on how well the hash function distributes keys. The ideal scenario is simple uniform hashing, where any given element is equally likely to hash into any of the
mslots, independently of other elements. Under this assumption, the expected length of any listT[j]isα. - Unsuccessful Search: An unsuccessful search takes Θ(1 + α) average-case time. This includes the O(1) time to compute the hash and access the slot, plus the expected
αtime to traverse the list. - Successful Search: A successful search also takes Θ(1 + α) average-case time. While the probability of searching each list isn't equal (it's proportional to the number of elements it contains), the mathematical expectation still works out to
Θ(1 + α).
Key takeaway: If n = O(m) (meaning α = O(1)), then searching, insertion, and deletion (with doubly linked lists) all take O(1) time on average. This is the significant benefit of hash tables.
Example: Chaining in action
Let's insert the keys 5, 28, 19, 15, 20, 33, 12, 17, 10 into a hash table with m = 9 slots, using the hash function h(k) = k mod 9:
h(5) = 5h(28) = 1(28 mod 9 = 1)h(19) = 1(19 mod 9 = 1) -> Collision with 28. Insert 19 at head of listT[1].h(15) = 6h(20) = 2h(33) = 6(33 mod 9 = 6) -> Collision with 15. Insert 33 at head of listT[6].h(12) = 3h(17) = 8h(10) = 1(10 mod 9 = 1) -> Collision with 19, 28. Insert 10 at head of listT[1].
Resulting Hash Table (simplified view, assuming insertions prepend):
T[0]: NILT[1]: 10 -> 19 -> 28 -> NILT[2]: 20 -> NILT[3]: 12 -> NILT[4]: NILT[5]: 5 -> NILT[6]: 33 -> 15 -> NILT[7]: NILT[8]: 17 -> NIL
Designing Effective Hash Functions
The performance of a hash table critically depends on a good hash function. A good hash function aims to satisfy the assumption of simple uniform hashing, distributing keys evenly across all m slots. In practice, we rarely know the exact probability distribution of keys, so heuristic techniques are often employed.
Interpreting Keys as Natural Numbers
Most hash functions assume keys are natural numbers. If keys are not numerical (e.g., character strings), they must first be interpreted as natural numbers. For instance, a character string can be viewed as an integer in a suitable radix notation (e.g., pt might become (112 * 128) + 116 = 14452 in ASCII radix-128).
Common Hash Function Schemes
- The Division Method: This is perhaps the simplest method:
h(k) = k mod m. It's very fast, requiring only a single division operation.
- Considerations: Avoid choosing
mas a power of 2 (e.g.,m = 2^p), because thenh(k)would simply be theplowest-order bits ofk. A prime number not too close to a power of 2 is often a good choice form. For example, if you expectn = 2000items and want an average of 3 elements per chain (α ≈ 3), you might choosem = 701(a prime near2000/3).
- The Multiplication Method: This method operates in two steps:
h(k) = floor(m * (kA mod 1)), wherekA mod 1is the fractional part ofkA.Ais a constant in the range0 < A < 1. A common suggestion forAis(sqrt(5) - 1)/2 ≈ 0.6180339887....
- Advantages: The value of
mis less critical, often chosen as a power of 2 (m = 2^p). This allows for efficient implementation using bitwise operations on computers with aw-bit word size. Thepmost significant bits of the product's lowerw-bit half form the hash value.
- Universal Hashing: To prevent a malicious adversary from choosing keys that always result in worst-case behavior (e.g., all keys hashing to the same slot), universal hashing employs randomization. Instead of a fixed hash function, one is chosen at random from a carefully designed family of hash functions at the beginning of execution.
- A collection
Hof hash functions is universal if, for any pair of distinct keysk, l, the number of hash functionshinHfor whichh(k) = h(l)is at most|H|/m. This means the probability of a collision betweenkandlis no more than1/m, just as ifh(k)andh(l)were chosen randomly and independently. - Benefits: Universal hashing guarantees good average-case performance (
O(1 + α)) for any sequence of operations, even if keys are adversarially chosen. - Designing a Universal Class: One common construction involves choosing a prime
pgreater than any possible key. The hash functionh_ab(k) = ((a*k + b) mod p) mod mis used, whereais fromZ_p*(1 top-1) andbis fromZ_p(0 top-1). The familyH_pmof all such functionsh_abis universal.
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 reside directly within the hash table itself. Each table entry contains either an element or NIL. Unlike chaining, no linked lists are used, potentially saving memory by avoiding pointers.
How Open Addressing Works
When inserting an element, if the initial slot h(k) is occupied, we systematically probe other slots until an empty one is found. The sequence of slots probed depends on the key being inserted. A hash function for open addressing takes the key k and a probe number i (starting from 0) as input: h(k, i) -> {0, 1,..., m-1}. For every key, the sequence h(k, 0), h(k, 1),..., h(k, m-1) must be a permutation of 0, 1,..., m-1 to ensure all slots are eventually considered.
HASH-INSERT(T, k): Iteratively probes slotsh(k, i)until an empty slotT[j]is found, then placeskintoT[j]. Ifmprobes are made without finding an empty slot, the table is full.HASH-SEARCH(T, k): Probes the same sequence of slots asHASH-INSERT. It terminates successfully ifkis found, or unsuccessfully if an empty slot is encountered (meaningkis not in the table).- Deletion: Deleting from an open-address table is challenging. Simply marking a slot as
NILcan break subsequent searches for other keys that might have probed through that slot. A common solution is to mark deleted slots with a specialDELETEDvalue.HASH-INSERTthen treatsDELETEDslots as empty for insertion, whileHASH-SEARCHcontinues probing pastDELETEDslots.
Probe Sequence Techniques
Open addressing methods strive to approximate uniform hashing, where each key's probe sequence is equally likely to be any of the m! permutations of table slots. True uniform hashing is difficult, so practical approximations are used:
-
Linear Probing: Uses the hash function
h(k, i) = (h_0(k) + i) mod m, whereh_0(k)is an auxiliary hash function. It probesT[h_0(k)], thenT[h_0(k)+1],T[h_0(k)+2], and so on, wrapping around the table. While easy to implement, it suffers from primary clustering, where long runs of occupied slots tend to grow longer, increasing search times. -
Quadratic Probing: Uses
h(k, i) = (h_0(k) + c_1*i + c_2*i^2) mod m, with auxiliary constantsc_1andc_2. This reduces primary clustering but can lead to secondary clustering, where keys with the same initial hash value follow the same probe sequence. -
Double Hashing: Considered one of the best open addressing methods:
h(k, i) = (h_1(k) + i*h_2(k)) mod m. Bothh_1andh_2are auxiliary hash functions. The probe sequence depends onkin two ways, generatingΘ(m^2)distinct probe sequences, which is closer to ideal uniform hashing.h_2(k)must be relatively prime tom(often achieved by choosingmas a prime andh_2(k)to return a value less thanmbut not 0).
Analysis of Open Addressing
Similar to chaining, open addressing performance is analyzed using the load factor α = n/m, where α <= 1 (since each slot holds at most one element).
- Unsuccessful Search: Under uniform hashing, the expected number of probes in an unsuccessful search is at most
1 / (1 - α). Forα = 0.5, it's 2 probes; forα = 0.9, it's 10 probes. Insertion performance is directly related to unsuccessful search, also taking1 / (1 - α)probes on average. - Successful Search: The expected number of probes in a successful search is at most
(1/α) * ln(1 / (1 - α)). Forα = 0.5, it's less than 1.387 probes; forα = 0.9, it's less than 2.559 probes. This demonstrates excellent average-case performance as long as the table isn't too full.
Perfect Hashing: Worst-Case O(1) Search for Static Sets
While hashing generally provides excellent average-case performance, some applications, especially those with static sets of keys (keys that never change once stored), require guaranteed worst-case performance. Perfect hashing achieves O(1) memory accesses for search in the worst case.
Perfect hashing uses a two-level approach:
- First Level:
nkeys are hashed intom = nslots using a universal hash functionh. Instead of linked lists, each slotjpoints to a small secondary hash tableS_j. - Second Level: Each secondary table
S_jhas a sizem_j = n_j^2, wheren_jis the number of keys that hashed to slotjat the first level. A separate universal hash functionh_jis chosen for eachS_jto ensure no collisions within that secondary table.
Key Guarantees:
- Collision-Free Secondary Tables: If a hash function is chosen randomly from a universal class for a table of size
m = n^2, the probability of any collisions is less than 1/2. This makes it easy to find a collision-freeh_jfor eachS_jwith a few random trials. - Overall O(n) Space: Despite the quadratic size of secondary tables, the expected total storage for all secondary hash tables combined is less than
2n. This is proven using linearity of expectation, bounding the sum ofn_j^2values. The probability that total secondary storage exceeds4nis less than 1/2.
Perfect hashing guarantees constant-time lookups in the worst case, making it ideal for fixed key sets like reserved words in programming languages.
Frequently Asked Questions about Hash Tables
What is the primary advantage of hash tables over direct-address tables?
Hash tables significantly reduce the memory required when the universe of possible keys is very large but the actual number of stored keys is relatively small. They offer efficient average-case O(1) time for dictionary operations, which is comparable to direct addressing but with much less space.
How does the load factor (α) impact hash table performance?
The load factor α = n/m is crucial. For chaining, a smaller α means shorter chains and faster average search times. If α is a constant (e.g., O(1)), operations are O(1) on average. For open addressing, α must be less than 1. As α approaches 1, the number of probes increases significantly, making the table slower and potentially leading to a full table.
Why is deletion difficult in open-address hash tables?
When a key is deleted, simply marking its slot as empty (NIL) can break the probe sequences of other keys that were inserted later and might have passed through that now-empty slot. If that slot becomes NIL, a subsequent search for those keys would prematurely terminate, failing to find them. This problem is usually mitigated by marking deleted slots with a special DELETED value, which insertion can overwrite but search will skip.