diff --git a/src/main/java/com/thealgorithms/datastructures/hashmap/hashing/HashMap.java b/src/main/java/com/thealgorithms/datastructures/hashmap/hashing/HashMap.java index e0b394b1..1aae122b 100644 --- a/src/main/java/com/thealgorithms/datastructures/hashmap/hashing/HashMap.java +++ b/src/main/java/com/thealgorithms/datastructures/hashmap/hashing/HashMap.java @@ -1,146 +1,275 @@ package com.thealgorithms.datastructures.hashmap.hashing; -public class HashMap { +/** + * A generic HashMap implementation that uses separate chaining with linked lists + * to handle collisions. The class supports basic operations such as insert, delete, + * and search, as well as displaying the contents of the hash map. + * + * @param the type of keys maintained by this map + * @param the type of mapped values + */ +public class HashMap { + private final int hashSize; + private final LinkedList[] buckets; - private final int hsize; - private final LinkedList[] buckets; - - public HashMap(int hsize) { - buckets = new LinkedList[hsize]; - for (int i = 0; i < hsize; i++) { - buckets[i] = new LinkedList(); - // Java requires explicit initialization of each object + /** + * Constructs a HashMap with the specified hash size. + * + * @param hashSize the number of buckets in the hash map + */ + @SuppressWarnings("unchecked") + public HashMap(int hashSize) { + this.hashSize = hashSize; + // Safe to suppress warning because we are creating an array of generic type + this.buckets = new LinkedList[hashSize]; + for (int i = 0; i < hashSize; i++) { + buckets[i] = new LinkedList<>(); } - this.hsize = hsize; } - public int hashing(int key) { - int hash = key % hsize; - if (hash < 0) { - hash += hsize; + /** + * Computes the hash code for the specified key. + * Null keys are hashed to bucket 0. + * + * @param key the key for which the hash code is to be computed + * @return the hash code corresponding to the key + */ + private int computeHash(K key) { + if (key == null) { + return 0; // Use a special bucket (e.g., bucket 0) for null keys } - return hash; + int hash = key.hashCode() % hashSize; + return hash < 0 ? hash + hashSize : hash; } - public void insertHash(int key) { - int hash = hashing(key); - buckets[hash].insert(key); + /** + * Inserts the specified key-value pair into the hash map. + * If the key already exists, the value is updated. + * + * @param key the key to be inserted + * @param value the value to be associated with the key + */ + public void insert(K key, V value) { + int hash = computeHash(key); + buckets[hash].insert(key, value); } - public void deleteHash(int key) { - int hash = hashing(key); - + /** + * Deletes the key-value pair associated with the specified key from the hash map. + * + * @param key the key whose key-value pair is to be deleted + */ + public void delete(K key) { + int hash = computeHash(key); buckets[hash].delete(key); } - public void displayHashtable() { - for (int i = 0; i < hsize; i++) { - System.out.printf("Bucket %d :", i); - System.out.println(buckets[i].display()); + /** + * Searches for the value associated with the specified key in the hash map. + * + * @param key the key whose associated value is to be returned + * @return the value associated with the specified key, or null if the key does not exist + */ + public V search(K key) { + int hash = computeHash(key); + Node node = buckets[hash].findKey(key); + return node != null ? node.getValue() : null; + } + + /** + * Displays the contents of the hash map, showing each bucket and its key-value pairs. + */ + public void display() { + for (int i = 0; i < hashSize; i++) { + System.out.printf("Bucket %d: %s%n", i, buckets[i].display()); } } - public static class LinkedList { + /** + * A nested static class that represents a linked list used for separate chaining in the hash map. + * + * @param the type of keys maintained by this linked list + * @param the type of mapped values + */ + public static class LinkedList { + private Node head; - private Node first; - - public LinkedList() { - first = null; - } - - public void insert(int key) { - if (isEmpty()) { - first = new Node(key); - return; + /** + * Inserts the specified key-value pair into the linked list. + * If the linked list is empty, the pair becomes the head. + * Otherwise, the pair is added to the end of the list. + * + * @param key the key to be inserted + * @param value the value to be associated with the key + */ + public void insert(K key, V value) { + Node existingNode = findKey(key); + if (existingNode != null) { + existingNode.setValue(value); // Update the value, even if it's null + } else { + if (isEmpty()) { + head = new Node<>(key, value); + } else { + Node temp = findEnd(head); + temp.setNext(new Node<>(key, value)); + } } - - Node temp = findEnd(first); - temp.setNext(new Node(key)); } - private Node findEnd(Node n) { - while (n.getNext() != null) { - n = n.getNext(); + /** + * Finds the last node in the linked list. + * + * @param node the starting node + * @return the last node in the linked list + */ + private Node findEnd(Node node) { + while (node.getNext() != null) { + node = node.getNext(); } - return n; + return node; } - public Node findKey(int key) { - if (!isEmpty()) { - Node temp = first; - if (temp.getKey() == key) { + /** + * Finds the node associated with the specified key in the linked list. + * + * @param key the key to search for + * @return the node associated with the specified key, or null if not found + */ + public Node findKey(K key) { + Node temp = head; + while (temp != null) { + if ((key == null && temp.getKey() == null) || (temp.getKey() != null && temp.getKey().equals(key))) { return temp; } - - while ((temp = temp.getNext()) != null) { - if (temp.getKey() == key) { - return temp; - } - } + temp = temp.getNext(); } return null; } - public void delete(int key) { - if (!isEmpty()) { - if (first.getKey() == key) { - Node next = first.next; - first.next = null; // help GC - first = next; - } else { - delete(first, key); + /** + * Deletes the node associated with the specified key from the linked list. + * Handles the case where the key could be null. + * + * @param key the key whose associated node is to be deleted + */ + public void delete(K key) { + if (isEmpty()) { + return; + } + + // Handle the case where the head node has the key to delete + if ((key == null && head.getKey() == null) || (head.getKey() != null && head.getKey().equals(key))) { + head = head.getNext(); + return; + } + + // Traverse the list to find and delete the node + Node current = head; + while (current.getNext() != null) { + if ((key == null && current.getNext().getKey() == null) || (current.getNext().getKey() != null && current.getNext().getKey().equals(key))) { + current.setNext(current.getNext().getNext()); + return; } + current = current.getNext(); } } - private void delete(Node n, int key) { - if (n.getNext().getKey() == key) { - if (n.getNext().getNext() == null) { - n.setNext(null); - } else { - n.setNext(n.getNext().getNext()); - } - } else { - delete(n.getNext(), key); - } - } - + /** + * Displays the contents of the linked list as a string. + * + * @return a string representation of the linked list + */ public String display() { - return display(first); + return display(head); } - private String display(Node n) { - if (n == null) { - return "null"; - } else { - return n.getKey() + "->" + display(n.getNext()); + /** + * Constructs a string representation of the linked list non-recursively. + * + * @param node the starting node + * @return a string representation of the linked list starting from the given node + */ + private String display(Node node) { + StringBuilder sb = new StringBuilder(); + while (node != null) { + sb.append(node.getKey()).append("=").append(node.getValue()); + node = node.getNext(); + if (node != null) { + sb.append(" -> "); + } } + return sb.toString().isEmpty() ? "null" : sb.toString(); } + /** + * Checks if the linked list is empty. + * + * @return true if the linked list is empty, false otherwise + */ public boolean isEmpty() { - return first == null; + return head == null; } } - public static class Node { + /** + * A nested static class representing a node in the linked list. + * + * @param the type of key maintained by this node + * @param the type of value maintained by this node + */ + public static class Node { + private final K key; + private V value; + private Node next; - private Node next; - private final int key; - - public Node(int key) { - next = null; + /** + * Constructs a Node with the specified key and value. + * + * @param key the key associated with this node + * @param value the value associated with this node + */ + public Node(K key, V value) { this.key = key; + this.value = value; } - public Node getNext() { - return next; - } - - public int getKey() { + /** + * Gets the key associated with this node. + * + * @return the key associated with this node + */ + public K getKey() { return key; } - public void setNext(Node next) { + /** + * Gets the value associated with this node. + * + * @return the value associated with this node + */ + public V getValue() { + return value; + } + + public void setValue(V value) { // This method allows updating the value + this.value = value; + } + + /** + * Gets the next node in the linked list. + * + * @return the next node in the linked list + */ + public Node getNext() { + return next; + } + + /** + * Sets the next node in the linked list. + * + * @param next the next node to be linked + */ + public void setNext(Node next) { this.next = next; } } diff --git a/src/main/java/com/thealgorithms/datastructures/hashmap/hashing/Main.java b/src/main/java/com/thealgorithms/datastructures/hashmap/hashing/Main.java deleted file mode 100644 index 4d9b33b1..00000000 --- a/src/main/java/com/thealgorithms/datastructures/hashmap/hashing/Main.java +++ /dev/null @@ -1,52 +0,0 @@ -package com.thealgorithms.datastructures.hashmap.hashing; - -import java.util.Scanner; - -public final class Main { - private Main() { - } - - public static void main(String[] args) { - int choice; - int key; - - HashMap h = new HashMap(7); - Scanner scan = new Scanner(System.in); - - while (true) { - System.out.println("Enter your Choice :"); - System.out.println("1. Add Key"); - System.out.println("2. Delete Key"); - System.out.println("3. Print Table"); - System.out.println("4. Exit"); - - choice = scan.nextInt(); - - switch (choice) { - case 1: - System.out.println("Enter the Key: "); - key = scan.nextInt(); - h.insertHash(key); - break; - - case 2: - System.out.println("Enter the Key delete: "); - key = scan.nextInt(); - h.deleteHash(key); - break; - - case 3: - System.out.println("Print table"); - h.displayHashtable(); - break; - - case 4: - scan.close(); - return; - - default: - throw new IllegalArgumentException("Unexpected value: " + choice); - } - } - } -} diff --git a/src/test/java/com/thealgorithms/datastructures/hashmap/hashing/HashMapTest.java b/src/test/java/com/thealgorithms/datastructures/hashmap/hashing/HashMapTest.java new file mode 100644 index 00000000..3552bc1a --- /dev/null +++ b/src/test/java/com/thealgorithms/datastructures/hashmap/hashing/HashMapTest.java @@ -0,0 +1,146 @@ +package com.thealgorithms.datastructures.hashmap.hashing; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; + +import org.junit.jupiter.api.Test; + +public class HashMapTest { + + @Test + public void testInsertAndSearch() { + HashMap hashMap = new HashMap<>(10); + hashMap.insert(15, "Value15"); + hashMap.insert(25, "Value25"); + hashMap.insert(35, "Value35"); + + assertEquals("Value15", hashMap.search(15)); + assertEquals("Value25", hashMap.search(25)); + assertEquals("Value35", hashMap.search(35)); + assertNull(hashMap.search(45)); + } + + @Test + public void testDelete() { + HashMap hashMap = new HashMap<>(10); + hashMap.insert(15, "Value15"); + hashMap.insert(25, "Value25"); + hashMap.insert(35, "Value35"); + + assertEquals("Value25", hashMap.search(25)); + hashMap.delete(25); + assertNull(hashMap.search(25)); + } + + @Test + public void testDisplay() { + HashMap hashMap = new HashMap<>(5); + hashMap.insert(15, "Value15"); + hashMap.insert(25, "Value25"); + hashMap.insert(35, "Value35"); + hashMap.display(); + } + + @Test + public void testInsertNullKey() { + HashMap hashMap = new HashMap<>(10); + hashMap.insert(null, "NullValue"); + assertEquals("NullValue", hashMap.search(null)); + } + + @Test + public void testInsertNullValue() { + HashMap hashMap = new HashMap<>(10); + hashMap.insert(15, null); + assertNull(hashMap.search(15)); + } + + @Test + public void testUpdateExistingKey() { + HashMap hashMap = new HashMap<>(10); + hashMap.insert(15, "Value15"); + hashMap.insert(15, "UpdatedValue15"); + + assertEquals("UpdatedValue15", hashMap.search(15)); + } + + @Test + public void testHandleCollisions() { + HashMap hashMap = new HashMap<>(3); + // These keys should collide if the hash function is modulo 3 + hashMap.insert(1, "Value1"); + hashMap.insert(4, "Value4"); + hashMap.insert(7, "Value7"); + + assertEquals("Value1", hashMap.search(1)); + assertEquals("Value4", hashMap.search(4)); + assertEquals("Value7", hashMap.search(7)); + } + + @Test + public void testSearchInEmptyHashMap() { + HashMap hashMap = new HashMap<>(10); + assertNull(hashMap.search(10)); + } + + @Test + public void testDeleteNonExistentKey() { + HashMap hashMap = new HashMap<>(10); + hashMap.insert(15, "Value15"); + hashMap.delete(25); + + assertEquals("Value15", hashMap.search(15)); + assertNull(hashMap.search(25)); + } + + @Test + public void testInsertLargeNumberOfElements() { + HashMap hashMap = new HashMap<>(10); + for (int i = 0; i < 100; i++) { + hashMap.insert(i, "Value" + i); + } + + for (int i = 0; i < 100; i++) { + assertEquals("Value" + i, hashMap.search(i)); + } + } + + @Test + public void testDeleteHeadOfBucket() { + HashMap hashMap = new HashMap<>(3); + hashMap.insert(1, "Value1"); + hashMap.insert(4, "Value4"); + hashMap.insert(7, "Value7"); + + hashMap.delete(1); + assertNull(hashMap.search(1)); + assertEquals("Value4", hashMap.search(4)); + assertEquals("Value7", hashMap.search(7)); + } + + @Test + public void testDeleteTailOfBucket() { + HashMap hashMap = new HashMap<>(3); + hashMap.insert(1, "Value1"); + hashMap.insert(4, "Value4"); + hashMap.insert(7, "Value7"); + + hashMap.delete(7); + assertNull(hashMap.search(7)); + assertEquals("Value1", hashMap.search(1)); + assertEquals("Value4", hashMap.search(4)); + } + + @Test + public void testDeleteMiddleElementOfBucket() { + HashMap hashMap = new HashMap<>(3); + hashMap.insert(1, "Value1"); + hashMap.insert(4, "Value4"); + hashMap.insert(7, "Value7"); + + hashMap.delete(4); + assertNull(hashMap.search(4)); + assertEquals("Value1", hashMap.search(1)); + assertEquals("Value7", hashMap.search(7)); + } +}