Hash Tables: Concepts, Operations, and Performance

Understand hash table concepts, operations, and performance. Learn about chaining, open addressing, hash functions, and perfect hashing. Master this core data structure for better algorithms!

Podcast

Hash Tables: The Ultimate Guide0:00 / 22:52
0:001:00 remaining

Hash tables are fundamental data structures that play a crucial role in computer science, particularly for implementing dictionaries and symbol tables. They offer an efficient way to store and retrieve data, making operations like insertion, searching, and deletion incredibly fast on average. While their worst-case performance can sometimes be slow, hash tables are highly practical and perform exceptionally well in most real-world scenarios, often achieving average-case O(1) time complexity.

Hash Tables: Core Concepts and Why They Matter

A hash table generalizes the simple idea of an ordinary array. In an ordinary array, if you know the key, you can access its corresponding data directly in O(1) time. This is called direct addressing. However, direct addressing only works well when the universe of possible keys is small enough to allocate an array position for every single key. Many applications deal with a vast universe of possible keys, but only a relatively small number of them are actually stored at any given time.

This is where hash tables become an effective alternative. Instead of using the key directly as an array index, a hash function computes the array index from the key. This allows the hash table to use an array size proportional to the number of keys actually stored, rather than the entire key universe, saving significant memory.

The Problem of Collisions

The core challenge with hash tables is that multiple keys might map to the same array index. This situation is known as a collision. Since the universe of keys (U) is typically much larger than the hash table's size (m), collisions are unavoidable. A well-designed hash function aims to minimize collisions, but a strategy is always needed to resolve them when they occur.

Direct-Address Tables: The Simple Precursor

Direct addressing is the simplest technique for dynamic sets, ideal when the universe of keys U = {0, 1,..., m-1} is reasonably small. Imagine an array T where each slot k directly corresponds to key k.

How Direct Addressing Works:

  • You have a direct-address table T of size m.
  • Each slot T[k] points to an element with key k.
  • If no element with key k is present, T[k] contains NIL.

Operations:

  • DIRECT-ADDRESS-SEARCH(T, k): Returns T[k].
  • DIRECT-ADDRESS-INSERT(T, x): Sets T[x.key] = x.
  • DIRECT-ADDRESS-DELETE(T, x): Sets T[x.key] = NIL.

Each of these operations takes O(1) time in the worst case, making direct addressing incredibly fast. However, its primary drawback is its impracticality when the universe U is very large, leading to massive memory waste if only a few keys are stored.

Collision Resolution Strategy: Chaining

Chaining is a widely used technique to handle collisions in hash tables. When multiple keys hash to the same slot, these elements are stored together in a linked list at that slot.

Mechanism:

  • Each slot T[j] in the hash table contains a pointer to the head of a linked list.
  • This list holds all elements whose hash value is j.
  • If a slot is empty, it contains NIL.

Dictionary Operations with Chaining:

  • CHAINED-HASH-INSERT(T, x): Inserts x at the head of the list T[h(x.key)]. This is typically an O(1) worst-case operation.
  • CHAINED-HASH-SEARCH(T, k): Searches for an element with key k within the list T[h(k)]. The worst-case time is proportional to the length of the list.
  • CHAINED-HASH-DELETE(T, x): Deletes x from the list T[h(x.key)]. If lists are doubly linked, this takes O(1) worst-case time.

Performance Analysis of Chaining:

To analyze performance, we use 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.

  • Worst-Case: All n keys hash to the same slot, creating a list of length n. Search time becomes O(n), no better than a single linked list.
  • Average-Case (under simple uniform hashing):
  • Unsuccessful Search: Takes O(1 + α) time. This is because we expect to search to the end of a list with length α.
  • Successful Search: Also takes O(1 + α) time. This assumes the element being searched for is equally likely to be any of the n elements.

If n = O(m) (meaning the number of elements is proportional to the number of slots), then α is a constant. In this common scenario, all dictionary operations—insertion, search, and deletion—take O(1) time on average.

Hash Functions: Mapping Keys to Slots

A good hash function is crucial for efficient hash table performance. It should distribute keys as uniformly as possible across the m slots, ideally satisfying the assumption of simple uniform hashing: each key is equally likely to hash to any slot, independently of other keys. Hash functions typically interpret keys as natural numbers.

The Division Method

This method is straightforward: h(k) = k mod m.

  • The hash value is the remainder of key k divided by hash table size m.
  • It's generally very fast, requiring a single division operation.
  • Important Considerations:
  • Avoid m being a power of 2 (e.g., m = 2^p), as h(k) would only depend on the p lowest-order bits of k.
  • Avoid m = 2^p - 1 when keys are character strings interpreted in radix 2^p, as permuting characters might not change the hash value.
  • A prime number not too close to a power of 2 is often a good choice for m.

The Multiplication Method

This method involves two steps: h(k) = floor(m * (kA mod 1)).

  1. Multiply the key k by a constant A (where 0 < A < 1).
  2. Extract the fractional part of kA (i.e., kA - floor(kA)).
  3. Multiply this fractional part by m and take the floor.
  • An advantage is that the value of m is not as critical; it's often chosen as a power of 2 (m = 2^p).
  • Knuth suggests A ≈ (sqrt(5) - 1) / 2 ≈ 0.6180339887 often works well.

Universal Hashing: Against Adversaries

To prevent an adversary from choosing keys that all hash to the same slot (leading to O(n) worst-case performance for any fixed hash function), universal hashing is used. Here, the hash function is selected randomly at the beginning of execution from a carefully designed family of functions.

A collection H of hash functions (mapping U to {0,..., m-1}) is universal if, for every 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 for any two distinct keys is at most 1/m, similar to truly random hashing.

A Common Universal Class:

  • Choose a prime p greater than any possible key k.
  • Define h_ab(k) = ((ak + b) mod p) mod m for a ∈ {1,..., p-1} and b ∈ {0,..., p-1}.
  • The family H_pm contains p(p-1) hash functions and is provably universal.

Collision Resolution Strategy: Open Addressing

In open addressing, all elements are stored directly within the hash table itself, without using linked lists. Each table entry holds either an element or NIL (or DELETED for removed elements).

When searching for an element, the algorithm systematically probes table slots until either the desired element is found, an empty slot is encountered (meaning the element is not present), or all slots have been examined.

Key Characteristics:

  • No pointers are stored, potentially saving memory.
  • The load factor α cannot exceed 1 (n <= m).
  • The probe sequence (the order of slots examined) depends on the key being inserted or searched for.
  • A hash function h(k, i) is used, where i is the probe number (starting from 0).
  • For every key k, the sequence h(k, 0), h(k, 1),..., h(k, m-1) must be a permutation of 0,..., m-1.

Open Addressing Operations:

  • HASH-INSERT(T, k):
  1. Starts with i = 0.
  2. Computes j = h(k, i).
  3. If T[j] is NIL (or DELETED), k is inserted there, and the slot j is returned.
  4. Otherwise, i increments, and step 2 repeats until an empty slot is found or m probes have been made (indicating table overflow).
  • HASH-SEARCH(T, k):
  1. Starts with i = 0.
  2. Computes j = h(k, i).
  3. If T[j] equals k, the element is found, and j is returned.
  4. If T[j] is NIL, k is not in the table, and NIL is returned.
  5. Otherwise (T[j] is occupied by a different key or DELETED), i increments, and step 2 repeats until k is found, an empty slot is hit, or m probes are made.
  • HASH-DELETE(T, x): Difficult with open addressing. Simply marking a slot NIL can break subsequent searches. Instead, slots are often marked DELETED. This complicates insertion logic but allows searches to proceed. Due to this complexity, chaining is often preferred when deletions are frequent.

Performance Analysis of Open Addressing (under uniform hashing):

Uniform hashing assumes that each key's probe sequence is equally likely to be any of the m! permutations of 0,..., m-1.

  • Expected Probes in Unsuccessful Search: At most 1 / (1 - α).
  • Expected Probes for Insertion: At most 1 / (1 - α).
  • Expected Probes in Successful Search: At most (1/α) * ln(1 / (1 - α)).

For example, if α = 0.5 (half full), an unsuccessful search averages at most 2 probes, and a successful search averages less than 1.387 probes. If α = 0.9, an unsuccessful search averages at most 10 probes, and a successful search averages less than 2.559 probes.

Open Addressing Techniques:

These techniques define how the probe sequence h(k, i) is generated:

  1. Linear Probing: h(k, i) = (h'(k) + i) mod m.
  • h'(k) is an auxiliary hash function.
  • Probes T[h'(k)], then T[h'(k) + 1], and so on, wrapping around the table.
  • Problem: Suffers from primary clustering, where long runs of occupied slots form, increasing search times as new keys are likely to extend existing clusters.
  1. Quadratic Probing: h(k, i) = (h'(k) + c1*i + c2*i^2) mod m.
  • h'(k) is an auxiliary hash function; c1, c2 are positive constants.
  • Probes are offset quadratically based on i.
  • Works better than linear probing but still suffers from secondary clustering: if two keys have the same h'(k), their probe sequences will be identical.
  1. Double Hashing: h(k, i) = (h1(k) + i*h2(k)) mod m.
  • Uses two auxiliary hash functions, h1(k) and h2(k).
  • The initial probe is T[h1(k)], and subsequent probes are offset by h2(k), modulo m.
  • Offers one of the best practical methods for open addressing.
  • h2(k) must be relatively prime to m to ensure all slots are searched. This is often achieved by making m prime and h2(k) a positive integer less than m.
  • Reduces clustering significantly because h1(k) and h2(k) combine to create m^2 distinct probe sequences, providing performance very close to ideal uniform hashing.

Flashcards

1 / 9

What is the form of the probe function used in linear probing (given an auxiliary hash h0) and how is the probe sequence generated for a key k?

h(k,i) = (h0(k) + i) mod m for i = 0,1,...,m-1. Start at T[h0(k)], then T[h0(k)+1], ... up to T[m-1], wrap to T[0], continuing until T[h0(k)-1]. The i

Tap to flip · Swipe to navigate

Perfect Hashing: Worst-Case O(1) Search for Static Sets

For static sets of keys (where keys are stored once and never change, like reserved words in a programming language), perfect hashing can guarantee O(1) memory accesses for searches in the worst case.

This scheme uses a two-level hashing approach:

  1. First Level: n keys are hashed into m = n slots using a hash function h from a universal family.
  2. Second Level: Instead of a linked list, each slot j has its own small secondary hash table Sj of size mj = nj^2 (where nj is the number of keys hashing to slot j). A secondary hash function hj is chosen such that there are no collisions within Sj.

While the nj^2 size for secondary tables might seem excessive, by carefully choosing the first-level hash function, the expected total memory used for all secondary hash tables is O(n). This ensures constant-time lookup in the worst case without excessive memory overhead.

FAQ: Hash Tables for Students

What is a hash table used for in programming?

Hash tables are primarily used for implementing dictionaries, which are data structures that support quick insertion, deletion, and searching of key-value pairs. They are common in compilers for symbol tables, database indexing, and caching mechanisms where fast lookups are essential.

How does a hash table achieve O(1) average time complexity?

A hash table achieves O(1) average time complexity by using a hash function to directly compute an array index for a given key. When collisions are handled efficiently (e.g., with chaining or well-designed open addressing), the average length of the lists (or probe sequences) remains small and constant, allowing most operations to complete in constant time.

What is the difference between chaining and open addressing for collision resolution?

Chaining resolves collisions by storing all elements that hash to the same slot in a linked list at that slot. Open addressing stores all elements directly within the hash table array itself. When a collision occurs in open addressing, the algorithm probes other slots in a systematic sequence until an empty one is found. Chaining typically handles deletions more easily, while open addressing can save memory by avoiding pointers.

Using a prime number for the hash table size (m) in the division method (k mod m) helps distribute keys more uniformly. If m is a power of 2, the hash function only considers the lowest-order bits of the key, which can lead to more collisions if key patterns exist. A prime m helps ensure that the hash value depends on all bits of the key, making the distribution more "random" and reducing clustering.

When would you use perfect hashing, and what are its main benefits?

Perfect hashing is used when the set of keys to be stored is static, meaning it never changes after initial storage (e.g., a list of reserved keywords in a programming language). Its main benefit is guaranteeing O(1) time for searches in the worst case, which is a significant improvement over the average-case O(1) of other hashing methods. This certainty is achieved through a two-level hashing scheme that eliminates all collisions in the secondary tables. For a deeper understanding of its implications in complexity theory, you can refer to the Wikipedia article on Perfect Hashing.

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