codingstuff.io
ExploreTutorialsProblemsCS Subjects
Get Started
ExploreTutorialsProblemsCS Subjects
Get Started
codingstuff.io

Master the art of building software through interactive tutorials, real-world problems, and guided projects.

Pune, Maharashtra, India

codingstuffmail@gmail.com

Product

  • Explore
  • Tutorials
  • Problems
  • CS Subjects

Company

  • About
  • Contact
  • Privacy Policy
  • Terms & Conditions
  • Sitemap

© 2026 codingstuff.io. All rights reserved.

Built with ❤️ for developers everywhere

/
/
All Tutorials
🧮

Data Structures & Algorithms

12 / 65 topics
11Hash Tables12Hash Collision Handling
Tutorials/Data Structures & Algorithms/Hash Collision Handling
🧮Data Structures & Algorithms

Hash Collision Handling

Updated 2026-04-20
3 min read

Hash Collision Handling

Introduction

Hashing is a fundamental concept in computer science and data structures, widely used for efficient data retrieval. A hash function maps input data of arbitrary size to fixed-size values, typically integers or strings. However, due to the pigeonhole principle, it's inevitable that different inputs can produce the same hash value, known as a hash collision. This tutorial will explore various techniques to handle hash collisions effectively.

Understanding Hash Collisions

Definition

A hash collision occurs when two distinct keys generate the same hash code. In a perfect world, a hash function would map each unique input to a unique output, but this is practically impossible due to the finite range of possible hash values and the potentially infinite number of inputs.

Types of Collisions

  1. Chained Hashing: Each bucket in the hash table contains a linked list (or other data structure) that stores all keys hashing to the same index.
  2. Open Addressing: When a collision occurs, the algorithm searches for the next available slot in the hash table using a probing sequence.

Chained Hashing

Overview

Chained hashing is one of the most common methods to handle collisions. Each bucket in the hash table contains a linked list (or any other data structure like a balanced tree) that stores all keys hashing to the same index.

Implementation

Here's a simple implementation of chained hashing using Python:

class Node:
    def __init__(self, key):
        self.key = key
        self.next = None

class ChainedHashTable:
    def __init__(self, size=10):
        self.size = size
        self.table = [None] * self.size

    def _hash(self, key):
        return hash(key) % self.size

    def insert(self, key):
        index = self._hash(key)
        if not self.table[index]:
            self.table[index] = Node(key)
        else:
            current = self.table[index]
            while current.next:
                current = current.next
            current.next = Node(key)

    def search(self, key):
        index = self._hash(key)
        current = self.table[index]
        while current:
            if current.key == key:
                return True
            current = current.next
        return False

    def delete(self, key):
        index = self._hash(key)
        current = self.table[index]
        previous = None
        while current:
            if current.key == key:
                if previous:
                    previous.next = current.next
                else:
                    self.table[index] = current.next
                return True
            previous = current
            current = current.next
        return False

# Example usage
hash_table = ChainedHashTable()
hash_table.insert(10)
hash_table.insert(20)
print(hash_table.search(10))  # Output: True
print(hash_table.delete(10))
print(hash_table.search(10))  # Output: False

Advantages

  • Simplicity: Easy to implement and understand.
  • Dynamic Size: Can handle a large number of keys without resizing the table.

Disadvantages

  • Memory Overhead: Requires additional memory for pointers in linked lists.
  • Performance Variability: Performance can degrade if many keys hash to the same index, leading to long chains.

Open Addressing

Overview

Open addressing is another method to handle collisions where the algorithm searches for the next available slot in the hash table using a probing sequence. Common probing techniques include linear probing, quadratic probing, and double hashing.

Linear Probing

In linear probing, if a collision occurs at index i, the algorithm checks the next consecutive indices (i+1) % size, (i+2) % size, and so on, until an empty slot is found.

Implementation

class OpenAddressHashTable:
    def __init__(self, size=10):
        self.size = size
        self.table = [None] * self.size

    def _hash(self, key):
        return hash(key) % self.size

    def insert(self, key):
        index = self._hash(key)
        original_index = index
        while self.table[index] is not None:
            if self.table[index] == key:  # Key already exists
                return False
            index = (index + 1) % self.size
            if index == original_index:  # Table is full
                return False
        self.table[index] = key
        return True

    def search(self, key):
        index = self._hash(key)
        original_index = index
        while self.table[index] is not None:
            if self.table[index] == key:
                return True
            index = (index + 1) % self.size
            if index == original_index:
                break
        return False

    def delete(self, key):
        index = self._hash(key)
        original_index = index
        while self.table[index] is not None:
            if self.table[index] == key:
                self.table[index] = None  # Mark as deleted or use a special value
                return True
            index = (index + 1) % self.size
            if index == original_index:
                break
        return False

# Example usage
hash_table = OpenAddressHashTable()
hash_table.insert(10)
hash_table.insert(20)
print(hash_table.search(10))  # Output: True
print(hash_table.delete(10))
print(hash_table.search(10))  # Output: False

Quadratic Probing

Quadratic probing uses a quadratic function to determine the next slot, reducing clustering compared to linear probing.

Implementation

class QuadraticProbingHashTable:
    def __init__(self, size=10):
        self.size = size
        self.table = [None] * self.size

    def _hash(self, key):
        return hash(key) % self.size

    def insert(self, key):
        index = self._hash(key)
        original_index = index
        i = 0
        while self.table[index] is not None:
            if self.table[index] == key:  # Key already exists
                return False
            i += 1
            index = (original_index + i * i) % self.size
            if index == original_index:  # Table is full
                return False
        self.table[index] = key
        return True

    def search(self, key):
        index = self._hash(key)
        original_index = index
        i = 0
        while self.table[index] is not None:
            if self.table[index] == key:
                return True
            i += 1
            index = (original_index + i * i) % self.size
            if index == original_index:
                break
        return False

    def delete(self, key):
        index = self._hash(key)
        original_index = index
        i = 0
        while self.table[index] is not None:
            if self.table[index] == key:
                self.table[index] = None  # Mark as deleted or use a special value
                return True
            i += 1
            index = (original_index + i * i) % self.size
            if index == original_index:
                break
        return False

# Example usage
hash_table = QuadraticProbingHashTable()
hash_table.insert(10)
hash_table.insert(20)
print(hash_table.search(10))  # Output: True
print(hash_table.delete(10))
print(hash_table.search(10))  # Output: False

Double Hashing

Double hashing uses two hash functions to determine the next slot, providing a more uniform distribution compared to linear and quadratic probing.

Implementation

class DoubleHashingHashTable:
    def __init__(self, size=10):
        self.size = size
        self.table = [None] * self.size

    def _hash1(self, key):
        return hash(key) % self.size

    def _hash2(self, key):
        return 1 + (hash(key) % (self.size - 1))

    def insert(self, key):
        index = self._hash1(key)
        original_index = index
        step = self._hash2(key)
        while self.table[index] is not None:
            if self.table[index] == key:  # Key already exists
                return False
            index = (index + step) % self.size
            if index == original_index:  # Table is full
                return False
        self.table[index] = key
        return True

    def search(self, key):
        index = self._hash1(key)
        original_index = index
        step = self._hash2(key)
        while self.table[index] is not None:
            if self.table[index] == key:
                return True
            index = (index + step) % self.size
            if index == original_index:
                break
        return False

    def delete(self, key):
        index = self._hash1(key)
        original_index = index
        step = self._hash2(key)
        while self.table[index] is not None:
            if self.table[index] == key:
                self.table[index] = None  # Mark as deleted or use a special value
                return True
            index = (index + step) % self.size
            if index == original_index:
                break
        return False

# Example usage
hash_table = DoubleHashingHashTable()
hash_table.insert(10)
hash_table.insert(20)
print(hash_table.search(10))  # Output: True
print(hash_table.delete(10))
print(hash_table.search(10))  # Output: False

Advantages

  • Less Clustering: Reduces the tendency of keys to cluster together.
  • Efficient Search and Insertion: Can be more efficient than chained hashing for certain probing sequences.

Disadvantages

  • Complexity: More complex to implement compared to chained hashing.
  • Performance Degradation: Performance can degrade if the table becomes too full, leading to long probing sequences.

Best Practices

  1. Choose a Good Hash Function: A good hash function should distribute keys uniformly across the hash table to minimize collisions.
  2. Load Factor Management: Keep the load factor (number of elements divided by table size) below a certain threshold to maintain performance.
  3. Resize the Table: Implement dynamic resizing to handle an increasing number of keys efficiently.
  4. Use Separate Chaining for High Collisions: If collisions are frequent, consider using chained hashing with linked lists or balanced trees.

Conclusion

Hash collision handling is crucial for maintaining efficient hash table operations. Both chained hashing and open addressing have their strengths and weaknesses, and the choice between them depends on specific requirements such as memory usage, performance considerations, and implementation complexity. By understanding these techniques and best practices, you can effectively manage hash collisions and optimize your data structures for better performance.


PreviousHash TablesNext Binary Search Trees (BSTs)

Recommended Gear

Hash TablesBinary Search Trees (BSTs)