Hash tables, Collision handling techniques and applications in Testing

Share with friends
โฑ๏ธ ๐‘น๐’†๐’‚๐’…๐’Š๐’๐’ˆ ๐‘ป๐’Š๐’Ž๐’†: 7 ๐˜ฎ๐˜ช๐˜ฏ๐˜ถ๐˜ต๐˜ฆ๐˜ด โšก๏ธ
Save Story for Later (0)
Please login to bookmark Close

Hashing is a powerful technique used in computer science to map data of arbitrary size to fixed-size values, making data storage and retrieval efficient. This guide will delve into the concepts of hash tables, collision handling techniques, and the applications of hashing in testing, providing a comprehensive understanding for beginners.

What is Hashing?

Hashing involves transforming input data (keys) into a fixed-size value called a hash code, typically using a hash function. The primary goal of hashing is to enable fast data retrieval and storage.

Key Characteristics of Hashing

  1. Hash Function: A function that takes an input and produces a hash code.
  2. Hash Code: The output of the hash function, typically a fixed-size integer.
  3. Hash Table: A data structure that stores key-value pairs, using the hash code to index the data for quick access.

Executable Hashing Examples in Java & Python

import java.util.Hashtable;

public class HashTableExample {
    public static void main(String[] args) {
        Hashtable<Integer, String> hashtable = new Hashtable<>();

        // Adding key-value pairs to the hash table
        hashtable.put(1, "Test Case 1");
        hashtable.put(2, "Test Case 2");
        hashtable.put(3, "Test Case 3");

        // Display the hash table
        System.out.println("HashTable: " + hashtable);

        // Retrieve value using a key
        String value = hashtable.get(2);
        System.out.println("Value for key 2: " + value);

        // Remove a key-value pair
        hashtable.remove(3);
        System.out.println("HashTable after removing key 3: " + hashtable);
    }
}

// Output:
// HashTable: {3=Test Case 3, 2=Test Case 2, 1=Test Case 1}
// Value for key 2: Test Case 2
// HashTable after removing key 3: {2=Test Case 2, 1=Test Case 1}
class HashTable:
    def __init__(self):
        self.table = [None] * 10
    
    def hash_function(self, key):
        return key % len(self.table)
    
    def insert(self, key, value):
        hash_key = self.hash_function(key)
        self.table[hash_key] = value
    
    def retrieve(self, key):
        hash_key = self.hash_function(key)
        return self.table[hash_key]
    
    def remove(self, key):
        hash_key = self.hash_function(key)
        self.table[hash_key] = None
    
    def display(self):
        print("HashTable:", self.table)

# Example usage
hash_table = HashTable()
hash_table.insert(1, "Test Case 1")
hash_table.insert(2, "Test Case 2")
hash_table.insert(3, "Test Case 3")
hash_table.display()

value = hash_table.retrieve(2)
print("Value for key 2:", value)

hash_table.remove(3)
hash_table.display()

# Output:
# HashTable: [None, 'Test Case 1', 'Test Case 2', 'Test Case 3', None, None, None, None, None, None]
# Value for key 2: Test Case 2
# HashTable: [None, 'Test Case 1', 'Test Case 2', None, None, None, None, None, None, None]

Collision Handling Techniques

Collisions occur when two different keys hash to the same index in the hash table. Several techniques can handle collisions effectively:

1. Chaining

Chaining involves storing multiple elements at the same index using a linked list. If a collision occurs, the new element is added to the list at that index.

Executable Chaining Examples in Java & Python

import java.util.LinkedList;

public class ChainingHashTable {
    private LinkedList<Entry>[] table;
    
    public ChainingHashTable(int size) {
        table = new LinkedList[size];
        for (int i = 0; i < size; i++) {
            table[i] = new LinkedList<>();
        }
    }
    
    private int hashFunction(int key) {
        return key % table.length;
    }
    
    public void insert(int key, String value) {
        int hashKey = hashFunction(key);
        table[hashKey].add(new Entry(key, value));
    }
    
    public String retrieve(int key) {
        int hashKey = hashFunction(key);
        for (Entry entry : table[hashKey]) {
            if (entry.key == key) {
                return entry.value;
            }
        }
        return null;
    }
    
    public void remove(int key) {
        int hashKey = hashFunction(key);
        table[hashKey].removeIf(entry -> entry.key == key);
    }
    
    public void display() {
        for (int i = 0; i < table.length; i++) {
            System.out.print(i + ": ");
            for (Entry entry : table[i]) {
                System.out.print(entry.value + " -> ");
            }
            System.out.println();
        }
    }
    
    private static class Entry {
        int key;
        String value;
        
        Entry(int key, String value) {
            this.key = key;
            this.value = value;
        }
    }
    
    public static void main(String[] args) {
        ChainingHashTable hashTable = new ChainingHashTable(10);
        hashTable.insert(1, "Test Case 1");
        hashTable.insert(11, "Test Case 11"); // Collision with key 1
        hashTable.insert(2, "Test Case 2");
        hashTable.display();

        String value = hashTable.retrieve(11);
        System.out.println("Value for key 11: " + value);

        hashTable.remove(11);
        hashTable.display();
    }
}

// Output:
// 0: 
// 1: Test Case 1 -> Test Case 11 -> 
// 2: Test Case 2 -> 
// 3: 
// 4: 
// 5: 
// 6: 
// 7: 
// 8: 
// 9: 
// Value for key 11: Test Case 11
// 0: 
// 1: Test Case 1 -> 
// 2: Test Case 2 -> 
// 3: 
// 4: 
// 5: 
// 6: 
// 7: 
// 8: 
// 9: 
class ChainingHashTable:
    def __init__(self):
        self.table = [[] for _ in range(10)]
    
    def hash_function(self, key):
        return key % len(self.table)
    
    def insert(self, key, value):
        hash_key = self.hash_function(key)
        self.table[hash_key].append((key, value))
    
    def retrieve(self, key):
        hash_key = self.hash_function(key)
        for k, v in self.table[hash_key]:
            if k == key:
                return v
        return None
    
    def remove(self, key):
        hash_key = self.hash_function(key)
        self.table[hash_key] = [(k, v) for k, v in self.table[hash_key] if k != key]
    
    def display(self):
        for i, bucket in enumerate(self.table):
            print(f"{i}: ", end="")
            for k, v in bucket:
                print(f"{v} -> ", end="")
            print()

# Example usage
hash_table = ChainingHashTable()
hash_table.insert(1, "Test Case 1")
hash_table.insert(11, "Test Case 11")  # Collision with key 1
hash_table.insert(2, "Test Case 2")
hash_table.display()

value = hash_table.retrieve(11)
print("Value for key 11:", value)

hash_table.remove(11)
hash_table.display()

# Output:
# 0: 
# 1: Test Case 1 -> Test Case 11 -> 
# 2: Test Case 2 -> 
# 3: 
# 4: 
# 5: 
# 6: 
# 7: 
# 8: 
# 9: 
# Value for key 11: Test Case 11
# 0: 
# 1: Test Case 1 -> 
# 2: Test Case 2 -> 
# 3: 
# 4: 
# 5: 
# 6: 
# 7: 
# 8: 
# 9:

2. Open Addressing

Open addressing involves finding the next available slot in the hash table when a collision occurs. There are several open addressing techniques, such as linear probing, quadratic probing, and double hashing.

Linear Probing

Linear probing handles collisions by checking the next slot in the array. If it’s occupied, it continues checking subsequent slots until an empty one is found.

Executable Chaining Examples in Java & Python
public class LinearProbingHashTable {
    private Entry[] table;
    private int size;
    
    public LinearProbingHashTable(int size) {
        this.size = size;
        table = new Entry[size];
    }
    
    private int hashFunction(int key) {
        return key % size;
    }
    
    public void insert(int key, String value) {
        int hashKey = hashFunction(key);
        while (table[hashKey] != null && table[hashKey].key != key) {
            hashKey = (hashKey + 1) % size;
        }
        table[hashKey] = new Entry(key, value);
    }
    
    public String retrieve(int key) {
        int hashKey = hashFunction(key);
        while (table[hashKey] != null && table[hashKey].key != key) {
            hashKey = (hashKey + 1) % size;
        }
        return table[hashKey] != null ? table[hashKey].value : null;
    }
    
    public void remove(int key) {
        int hashKey = hashFunction(key);
        while (table[hashKey] != null && table[hashKey].key != key) {
            hashKey = (hashKey + 1) % size;
        }
        table[hashKey] = null;
    }
    
    public void display() {
        for (int i = 0; i < size; i++) {
            if (table[i] != null) {
                System.out.println(i + ": " + table[i].value);
            } else {
                System.out.println(i + ": null");
            }
        }
    }
    
    private static class Entry {
        int key;
        String value;
        
        Entry(int key, String value) {
            this.key = key;
            this.value = value;
        }
    }
    
    public static void main(String[] args) {
        LinearProbingHashTable hashTable = new LinearProbingHashTable(10);
        hashTable.insert(1, "Test Case 1");
        hashTable.insert(11, "Test Case 11"); // Collision with key 1
        hashTable.insert(2, "Test Case 2");
        hashTable.display();

        String value = hashTable.retrieve(11);
        System.out.println("Value for key 11: " + value);

        hashTable.remove(11);
        hashTable.display();
    }
}

// Output:
// 0: null
// 1: Test Case 1
// 2: Test Case 2
// 3: null
// 4: null
// 5: null
// 6: null
// 7: null
// 8: null
// 9: null
// Value for key 11: Test Case 11
// 0: null
// 1: Test Case 1
// 2: Test Case 2
// 3: null
// 4: null
// 5: null
// 6: null
// 7: null
// 8: null
// 9: null
class LinearProbingHashTable:
    def __init__(self, size=10):
        self.table = [None] * size
        self.size = size
    
    def hash_function(self, key):
        return key % self.size
    
    def insert(self, key, value):
        hash_key = self.hash_function(key)
        while self.table[hash_key] is not None and self.table[hash_key][0] != key:
            hash_key = (hash_key + 1) % self.size
        self.table[hash_key] = (key, value)
    
    def retrieve(self, key):
        hash_key = self.hash_function(key)
        while self.table[hash_key] is not None and self.table[hash_key][0] != key:
            hash_key = (hash_key + 1) % self.size
        return self.table[hash_key][1] if self.table[hash_key] is not None else None
    
    def remove(self, key):
        hash_key = self.hash_function(key)
        while self.table[hash_key] is not None and self.table[hash_key][0] != key:
            hash_key = (hash_key + 1) % self.size
        self.table[hash_key] = None
    
    def display(self):
        for i, entry in enumerate(self.table):
            if entry is not None:
                print(f"{i}: {entry[1]}")
            else:
                print(f"{i}: null")

# Example usage
hash_table = LinearProbingHashTable()
hash_table.insert(1, "Test Case 1")
hash_table.insert(11, "Test Case 11")  # Collision with key 1
hash_table.insert(2, "Test Case 2")
hash_table.display()

value = hash_table.retrieve(11)
print("Value for key 11:", value)

hash_table.remove(11)
hash_table.display()

# Output:
# 0: null
# 1: Test Case 1
# 2: Test Case 2
# 3: null
# 4: null
# 5: null
# 6: null
# 7: null
# 8: null
# 9: null
# Value for key 11: Test Case 11
# 0: null
# 1: Test Case 1
# 2: Test Case 2
# 3: null
# 4: null
# 5: null
# 6: null
# 7: null
# 8: null
# 9: null

Applications of Hashing in Testing

Efficient Data Storage

Hash tables provide efficient data storage, making it easy to store and retrieve test cases based on unique identifiers. This is particularly useful in large test suites where quick access to specific test cases is essential.

Managing Test Execution States

Hashing can be used to manage the state of test executions, storing results and statuses of individual test cases. This allows for quick updates and retrieval of test case states during testing cycles.

Detecting Duplicate Test Cases

Hashing can help detect duplicate test cases by hashing the content of each test case and comparing hash values. This ensures that only unique test cases are executed, reducing redundancy.

Executable Examples in Java & Python for Managing Test Cases

import java.util.Hashtable;

public class TestCaseManager {
    private Hashtable<Integer, String> testCases;

    public TestCaseManager() {
        testCases = new Hashtable<>();
    }

    public void addTestCase(int id, String testCase) {
        testCases.put(id, testCase);
    }

    public String getTestCase(int id) {
        return testCases.get(id);
    }

    public void removeTestCase(int id) {
        testCases.remove(id);
    }

    public void displayTestCases() {
        System.out.println("Test Cases: " + testCases);
    }

    public static void main(String[] args) {
        TestCaseManager manager = new TestCaseManager();
        manager.addTestCase(1, "Test Case 1");
        manager.addTestCase(2, "Test Case 2");
        manager.displayTestCases();

        String testCase = manager.getTestCase(1);
        System.out.println("Retrieved Test Case 1: " + testCase);

        manager.removeTestCase(1);
        manager.displayTestCases();
    }
}

// Output:
// Test Cases: {2=Test Case 2, 1=Test Case 1}
// Retrieved Test Case 1: Test Case 1
// Test Cases: {2=Test Case 2}
class TestCaseManager:
    def __init__(self):
        self.test_cases = {}
    
    def add_test_case(self, id, test_case):
        self.test_cases[id] = test_case
    
    def get_test_case(self, id):
        return self.test_cases.get(id)
    
    def remove_test_case(self, id):
        if id in self.test_cases:
            del self.test_cases[id]
    
    def display_test_cases(self):
        print("Test Cases:", self.test_cases)

# Example usage
manager = TestCaseManager()
manager.add_test_case(1, "Test Case 1")
manager.add_test_case(2, "Test Case 2")
manager.display_test_cases()

test_case = manager.get_test_case(1)
print("Retrieved Test Case 1:", test_case)

manager.remove_test_case(1)
manager.display_test_cases()

# Output:
# Test Cases: {1: 'Test Case 1', 2: 'Test Case 2'}
# Retrieved Test Case 1: Test Case 1
# Test Cases: {2: 'Test Case 2'}

Conclusion

Hashing is a crucial technique in computer science, providing efficient data storage and retrieval. Understanding hash tables, collision handling techniques, and their applications in testing will enhance your ability to manage test cases effectively.

Article Contributors

  • QABash.ai
    (Author)
    Director - Research & Innovation, QABash

    Scientist Testbot, endlessly experimenting with testing frameworks, automation tools, and wild test cases in search of the most elusive bugs. Whether it's poking at flaky pipelines, dissecting Selenium scripts, or running clever Lambda-powered tests โ€” QAbash.ai is always in the lab, always learning. โš™๏ธ Built for testers. Tuned for automation. Obsessed with quality.

  • Ishan Dev Shukl
    (Reviewer)
    SDET Manager, Nykaa

    With 13+ years in SDET leadership, I drive quality and innovation through Test Strategies and Automation. I lead Testing Center of Excellence, ensuring high-quality products across Frontend, Backend, and App Testing. "Quality is in the details" defines my approachโ€”creating seamless, impactful user experiences. I embrace challenges, learn from failure, and take risks to drive success.

Subscribe to QABash Weekly ๐Ÿ’ฅ

Dominate โ€“ Stay Ahead of 99% Testers!

Leave a Reply

Scroll to Top
×