Hash Tables: Efficient Dictionary Data Structures

Master hash tables and their efficient dictionary operations. Learn collision resolution (chaining, open addressing) and hash functions. Understand average-case O(1) performance and perfect hashing for students.

Podcast

Unlocking Hash Tables0:00 / 21:25
0:001:00 remaining

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 element x at the head of the list T[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 key k within the list T[h(k)]. The running time depends on the length of the list.
  • CHAINED-HASH-DELETE(T, x): Deletes element x from the list T[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 n keys hash to the same slot, creating a single list of length n. 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 m slots, independently of other elements. Under this assumption, the expected length of any list T[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) = 5
  • h(28) = 1 (28 mod 9 = 1)
  • h(19) = 1 (19 mod 9 = 1) -> Collision with 28. Insert 19 at head of list T[1].
  • h(15) = 6
  • h(20) = 2
  • h(33) = 6 (33 mod 9 = 6) -> Collision with 15. Insert 33 at head of list T[6].
  • h(12) = 3
  • h(17) = 8
  • h(10) = 1 (10 mod 9 = 1) -> Collision with 19, 28. Insert 10 at head of list T[1].

Resulting Hash Table (simplified view, assuming insertions prepend):

  • T[0]: NIL
  • T[1]: 10 -> 19 -> 28 -> NIL
  • T[2]: 20 -> NIL
  • T[3]: 12 -> NIL
  • T[4]: NIL
  • T[5]: 5 -> NIL
  • T[6]: 33 -> 15 -> NIL
  • T[7]: NIL
  • T[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

  1. 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 m as a power of 2 (e.g., m = 2^p), because then h(k) would simply be the p lowest-order bits of k. A prime number not too close to a power of 2 is often a good choice for m. For example, if you expect n = 2000 items and want an average of 3 elements per chain (α ≈ 3), you might choose m = 701 (a prime near 2000/3).
  1. The Multiplication Method: This method operates in two steps: h(k) = floor(m * (kA mod 1)), where kA mod 1 is the fractional part of kA. A is a constant in the range 0 < A < 1. A common suggestion for A is (sqrt(5) - 1)/2 ≈ 0.6180339887....
  • Advantages: The value of m is less critical, often chosen as a power of 2 (m = 2^p). This allows for efficient implementation using bitwise operations on computers with a w-bit word size. The p most significant bits of the product's lower w-bit half form the hash value.
  1. 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 H of hash functions is universal if, for any pair of distinct keys k, l, the number of hash functions h in 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, just as if h(k) and h(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 p greater than any possible key. The hash function h_ab(k) = ((a*k + b) mod p) mod m is used, where a is from Z_p* (1 to p-1) and b is from Z_p (0 to p-1). The family H_pm of all such functions h_ab is universal.

Flashcards

1 / 20

What is the definition of perfect hashing in terms of memory accesses for search in the worst case?

A hashing technique is called perfect hashing if O(1) memory accesses are required to perform a search in the worst case.

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 slots h(k, i) until an empty slot T[j] is found, then places k into T[j]. If m probes are made without finding an empty slot, the table is full.
  • HASH-SEARCH(T, k): Probes the same sequence of slots as HASH-INSERT. It terminates successfully if k is found, or unsuccessfully if an empty slot is encountered (meaning k is not in the table).
  • Deletion: Deleting from an open-address table is challenging. Simply marking a slot as NIL can break subsequent searches for other keys that might have probed through that slot. A common solution is to mark deleted slots with a special DELETED value. HASH-INSERT then treats DELETED slots as empty for insertion, while HASH-SEARCH continues probing past DELETED slots.

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:

  1. Linear Probing: Uses the hash function h(k, i) = (h_0(k) + i) mod m, where h_0(k) is an auxiliary hash function. It probes T[h_0(k)], then T[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.

  2. Quadratic Probing: Uses h(k, i) = (h_0(k) + c_1*i + c_2*i^2) mod m, with auxiliary constants c_1 and c_2. This reduces primary clustering but can lead to secondary clustering, where keys with the same initial hash value follow the same probe sequence.

  3. Double Hashing: Considered one of the best open addressing methods: h(k, i) = (h_1(k) + i*h_2(k)) mod m. Both h_1 and h_2 are auxiliary hash functions. The probe sequence depends on k in two ways, generating Θ(m^2) distinct probe sequences, which is closer to ideal uniform hashing. h_2(k) must be relatively prime to m (often achieved by choosing m as a prime and h_2(k) to return a value less than m but 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 taking 1 / (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:

  1. First Level: n keys are hashed into m = n slots using a universal hash function h. Instead of linked lists, each slot j points to a small secondary hash table S_j.
  2. Second Level: Each secondary table S_j has a size m_j = n_j^2, where n_j is the number of keys that hashed to slot j at the first level. A separate universal hash function h_j is chosen for each S_j to 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-free h_j for each S_j with 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 of n_j^2 values. The probability that total secondary storage exceeds 4n is 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.

What is a

Sign up to access full content

Create a free account to unlock all study materials, take interactive tests, listen to podcasts and more.

Create free account

Related topics