Skip to content

refactor: Enhance docs, add tests in GenericHashMapUsingArray #5972

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 5 commits into from
Oct 29, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -2,67 +2,112 @@

import java.util.LinkedList;

// implementation of generic hashmaps using array of Linked Lists

/**
* A generic implementation of a hash map using an array of linked lists for collision resolution.
* This class provides a way to store key-value pairs efficiently, allowing for average-case
* constant time complexity for insertion, deletion, and retrieval operations.
*
* <p>
* The hash map uses separate chaining for collision resolution. Each bucket in the hash map is a
* linked list that stores nodes containing key-value pairs. When a collision occurs (i.e., when
* two keys hash to the same index), the new key-value pair is simply added to the corresponding
* linked list.
* </p>
*
* <p>
* The hash map automatically resizes itself when the load factor exceeds 0.75. The load factor is
* defined as the ratio of the number of entries to the number of buckets. When resizing occurs,
* all existing entries are rehashed and inserted into the new buckets.
* </p>
*
* @param <K> the type of keys maintained by this hash map
* @param <V> the type of mapped values
*/
public class GenericHashMapUsingArray<K, V> {

private int size; // n (total number of key-value pairs)
private LinkedList<Node>[] buckets; // N = buckets.length
private float lf = 0.75f;
private int size; // Total number of key-value pairs
private LinkedList<Node>[] buckets; // Array of linked lists (buckets) for storing entries

/**
* Constructs a new empty hash map with an initial capacity of 16.
*/
public GenericHashMapUsingArray() {
initBuckets(16);
size = 0;
}

// load factor = 0.75 means if we need to add 100 items and we have added
// 75, then adding 76th item it will double the size, copy all elements
// & then add 76th item.

/**
* Initializes the buckets for the hash map with the specified number of buckets.
*
* @param n the number of buckets to initialize
*/
private void initBuckets(int n) {
buckets = new LinkedList[n];
for (int i = 0; i < buckets.length; i++) {
buckets[i] = new LinkedList<>();
}
}

/**
* Associates the specified value with the specified key in this map.
* If the map previously contained a mapping for the key, the old value is replaced.
*
* @param key the key with which the specified value is to be associated
* @param value the value to be associated with the specified key
*/
public void put(K key, V value) {
int bucketIndex = hashFunction(key);
LinkedList<Node> nodes = buckets[bucketIndex];
for (Node node : nodes) { // if key present => update
// Update existing key's value if present
for (Node node : nodes) {
if (node.key.equals(key)) {
node.value = value;
return;
}
}

// key is not present => insert
// Insert new key-value pair
nodes.add(new Node(key, value));
size++;

if ((float) size / buckets.length > lf) {
// Check if rehashing is needed
// Load factor threshold for resizing
float loadFactorThreshold = 0.75f;
if ((float) size / buckets.length > loadFactorThreshold) {
reHash();
}
}

// tells which bucket to go to
/**
* Returns the index of the bucket in which the key would be stored.
*
* @param key the key whose bucket index is to be computed
* @return the bucket index
*/
private int hashFunction(K key) {
return Math.floorMod(key.hashCode(), buckets.length);
}

/**
* Rehashes the map by doubling the number of buckets and re-inserting all entries.
*/
private void reHash() {
System.out.println("Rehashing!");
LinkedList<Node>[] old = buckets;
initBuckets(old.length * 2);
LinkedList<Node>[] oldBuckets = buckets;
initBuckets(oldBuckets.length * 2);
this.size = 0;

for (LinkedList<Node> nodes : old) {
for (LinkedList<Node> nodes : oldBuckets) {
for (Node node : nodes) {
put(node.key, node.value);
}
}
}

/**
* Removes the mapping for the specified key from this map if present.
*
* @param key the key whose mapping is to be removed from the map
*/
public void remove(K key) {
int bucketIndex = hashFunction(key);
LinkedList<Node> nodes = buckets[bucketIndex];
Expand All @@ -74,14 +119,28 @@ public void remove(K key) {
break;
}
}
nodes.remove(target);
size--;

if (target != null) {
nodes.remove(target);
size--;
}
}

/**
* Returns the number of key-value pairs in this map.
*
* @return the number of key-value pairs
*/
public int size() {
return this.size;
}

/**
* Returns the value to which the specified key is mapped, or null if this map contains no mapping for the key.
*
* @param key the key whose associated value is to be returned
* @return the value associated with the specified key, or null if no mapping exists
*/
public V get(K key) {
int bucketIndex = hashFunction(key);
LinkedList<Node> nodes = buckets[bucketIndex];
Expand All @@ -96,7 +155,6 @@ public V get(K key) {
@Override
public String toString() {
StringBuilder builder = new StringBuilder();

builder.append("{");
for (LinkedList<Node> nodes : buckets) {
for (Node node : nodes) {
Expand All @@ -106,19 +164,37 @@ public String toString() {
builder.append(", ");
}
}
// Remove trailing comma and space
if (builder.length() > 1) {
builder.setLength(builder.length() - 2);
}
builder.append("}");
return builder.toString();
}

/**
* Returns true if this map contains a mapping for the specified key.
*
* @param key the key whose presence in this map is to be tested
* @return true if this map contains a mapping for the specified key
*/
public boolean containsKey(K key) {
return get(key) != null;
}

/**
* A private class representing a key-value pair (node) in the hash map.
*/
public class Node {

K key;
V value;

/**
* Constructs a new Node with the specified key and value.
*
* @param key the key of the key-value pair
* @param value the value of the key-value pair
*/
public Node(K key, V value) {
this.key = key;
this.value = value;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,4 +50,47 @@ void testGenericHashmapWhichUsesArrayAndKeyIsIntegerValueIsString() {
assertEquals("Washington DC", map.get(101));
assertTrue(map.containsKey(46));
}

@Test
void testRemoveNonExistentKey() {
GenericHashMapUsingArray<String, String> map = new GenericHashMapUsingArray<>();
map.put("USA", "Washington DC");
map.remove("Nepal"); // Attempting to remove a non-existent key
assertEquals(1, map.size()); // Size should remain the same
}

@Test
void testRehashing() {
GenericHashMapUsingArray<String, String> map = new GenericHashMapUsingArray<>();
for (int i = 0; i < 20; i++) {
map.put("Key" + i, "Value" + i);
}
assertEquals(20, map.size()); // Ensure all items were added
assertEquals("Value5", map.get("Key5")); // Check retrieval after rehash
}

@Test
void testUpdateValueForExistingKey() {
GenericHashMapUsingArray<String, String> map = new GenericHashMapUsingArray<>();
map.put("USA", "Washington DC");
map.put("USA", "New Washington DC"); // Updating value for existing key
assertEquals("New Washington DC", map.get("USA"));
}

@Test
void testToStringMethod() {
GenericHashMapUsingArray<String, String> map = new GenericHashMapUsingArray<>();
map.put("USA", "Washington DC");
map.put("Nepal", "Kathmandu");
String expected = "{USA : Washington DC, Nepal : Kathmandu}";
assertEquals(expected, map.toString());
}

@Test
void testContainsKey() {
GenericHashMapUsingArray<String, String> map = new GenericHashMapUsingArray<>();
map.put("USA", "Washington DC");
assertTrue(map.containsKey("USA"));
assertFalse(map.containsKey("Nepal"));
}
}