public class HashMapSC<K, V> {
private class Node<K, V> {
K key;
V val;
Node<K, V> next;
public Node(K key, V val) {
this.key = key;
this.val = val;
}
}
private Node[] table;
// size of table
private int capacity;
// total items in hashmap
private int size;
public HashMapSC() { this(997); }
public HashMapSC(int capacity) {
this.capacity = capacity;
table = new Node[capacity];
size = 0;
}
public int size() { return size; }
public boolean isEmpty() { return size == 0; }
public void put(K key, V val) {
if (key == null || val == null) throw new IllegalArgumentException();
// double table capacity if necessary
if (size >= capacity / 2) resize(capacity * 2);
int index = hash(key);
Node nextNode = new Node(key, val);
Node curr = table[index];
// insert new node at head of linked list
if (curr == null) {
nextNode.next = curr;
table[index] = nextNode;
size++;
return;
}
// if linked list has nodes, replace val if key in list
while (curr != null) {
if (curr.key.equals(key)) {
// replace update val
curr.val = val;
}
curr = curr.next;
}
}
public V get(K key) {
if (key == null) throw new IllegalArgumentException();
int index = hash(key);
Node node = table[index];
while (node != null) {
if (node.key.equals(key)) return (V) node.val;
node = node.next;
}
return null;
}
private K remove(K key) {
if (key == null) throw new IllegalArgumentException();
int index = hash(key);
Node curr = table[index];
Node prev = null;
while (curr != null) {
if (curr.key.equals(key)) {
size--;
// halve the table capacity if necessary
if (size > 0 && size < (capacity / 8)) resize(capacity / 2);
if (prev == null) {
// removing first item in linked list
table[index] = curr.next;
} else {
prev.next = curr.next;
table[index] = prev;
}
return key;
}
prev = curr;
curr = curr.next;
}
return null;
}
private int hash(K key) {
return (key.hashCode() & 0x7fffffff) % capacity;
}
private void resize(int size) {
Node[] copy = new Node[size];
for (int i = 0; i < size; i++) {
copy[i] = table[i];
}
table = copy;
}
public static void main(String[] args) {
HashMapSC hm = new HashMapSC<String, Integer>();
hm.put("homer", 123);
hm.put("marge", 456);
assert hm.size() == 2;
assert hm.get("homer").equals(123);
assert hm.get("marge").equals(456);
hm.put("marge", 654);
assert hm.get("marge").equals(654);
assert hm.remove("marge").equals("marge");
assert hm.size() == 1;
}
}
Another common method for collision resolution is linear probing.
While seperate chaining uses linked lists at indices to resolve collisions, a linear probing strategy stores the values (or references to values) in the buckets. When the hash function hashes to a table index that’s already occupied, it probes for the next empty position in the array (by incrementing a pointer).
Retrieval works similarly; if the hash function hashes to an index that does not match the given key, the index increments by 1 and checks again. This is repeated until there’s a key match. Otherwise null
is returned.
Parallel arrays, one for keys and one for values.
public class HashMapLP<K, V> {
private int size;
private int capacity;
private K[]keys;
private V[]vals;
public HashMapLP() {
this(4);
}
public HashMapLP(int capacity) {
this.capacity = capacity;
keys = (K[]) new Object[capacity];
vals = (V[]) new Object[capacity];
}
public int size() { return size; }
public boolean isEmpty() { return size == 0; }
public void put(K key, V val) {
if (key == null || val == null) throw new IllegalArgumentException();
// double the size of the table when it's at half capacity
if (size >= capacity / 2) resize(capacity * 2);
int i = hash(key);
// replace existing val
while(keys[i] != null) {
if(keys[i].equals(key)){
vals[i] = val;
return;
}
i = (i + 1) % capacity;
}
// insert new k:v pair
keys[i] = key;
vals[i] = val;
size++;
}
public V get(K key) {
if (key == null) throw new IllegalArgumentException();
int i = hash(key);
while (keys[i] != null) {
if (keys[i].equals(key)) return vals[i];
i = (i+1) % capacity;
}
return null;
}
public K remove(K key) {
if (key == null) throw new IllegalArgumentException();
int i = hash(key);
while (keys[i] != null) {
if (keys[i].equals(key)) {
key = keys[i];
keys[i] = null;
vals[i] = null;
size--;
if (size > 0 && size <= (capacity / 8)) resize(capacity / 2);
return key;
}
i = (i + 1) % capacity;
}
return null;
}
private void resize(int capacity) {
K[] keysCopy = (K[]) new Object[capacity];
V[] valsCopy = (V[]) new Object[capacity];
for (int i = 0; i < this.capacity; i++) {
if (keys[i] != null) {
keysCopy[i] = keys[i];
valsCopy[i] = vals[i];
}
}
this.capacity = capacity;
keys = keysCopy;
vals = valsCopy;
}
private int hash(K key) {
return (key.hashCode() & 0x7fffffff) % capacity;
}
}
private int hash(K key) {
return (key.hashCode() & 0x7fffffff) % capacity;
}
The above hashing algorithm is a simple in its design.
The hashing algorithm needs to ensure it does not provide a negative number. Without ensuring this, we know it’s a possibility because hashCode()
can provide negative values
We can ensure a positive number is provided by performing a shift mask using bitwise ANDing; it extracts the common bits between the result of key.hashcode()
and the hexadecimal value for 214,7483,647, which uncoincidentally is the max integer value a Java-32 bit integer can hold. The max value is used to take advantage of the largest number of bits possible. This value is divided by the array length and remainder, in principle, should represent an array index value that is of uniform distribution for all possible array index values.