Skip to content

Commit 208bad7

Browse files
authored
Merge pull request #85 from jaebradley/hashmap-implementation
Hashmap implementation
2 parents 46fc3d8 + cb2f497 commit 208bad7

File tree

3 files changed

+522
-0
lines changed

3 files changed

+522
-0
lines changed

codereview/hashMap.md

Lines changed: 208 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,208 @@
1+
## Purpose
2+
3+
I've never implemented a `HashMap` and thought it would be a good data structure exercise. I tried looking at the Java
4+
source code as little as possible.
5+
6+
## Discussion
7+
8+
The `HashMap` was made up of an array of `Entry`s. Each `Entry` is points to another `Entry` (or `null` if it's the
9+
last `Entry` in the linked list).
10+
11+
Since you'd want `Entry`s distributed as equally as possible across the different index values of the internal `Entry`
12+
array, each key's hashcode is hashed again to combat a bad hashcode (this code is from the Java 8 source code) and
13+
eventually an index value is calculated - this index value represents the index of the internal array that the
14+
key-value pair will exist under.
15+
16+
Thus, the logic for getting a value for a particular key would be to translate the key into an index value, and then
17+
to get the first `Entry` for that index value from the internal array. And then to get the `Entry` in the linked list
18+
with the same key (or return `null` if the linked list has been exhausted).
19+
20+
Another thing that was interesting to implement was array resizing. Every time `put` is called, it checks to see if array
21+
resizing needs to occur. I resize the array if the number of objects that have been added to the array is greater than
22+
75% of the allocated array capacity. I don't know if this is the best way to implement a resize check. When actually
23+
resizing the array, I needed to iterate through every `Entry` and reindex the `Entry`.
24+
25+
Things I don't like:
26+
* My implementation seems really messy.
27+
1. I don't like the `addEntry` method. I don't like it has a `boolean` return. However, I did this so that when `put`ting
28+
I wouldn't update the `size` when I updated a key-value pair vs. added one.
29+
2. I don't like how the `setEntry` method keeps the `next` value in memory before overwriting it (this was done so
30+
that no `Entry` in the linked list did not get reindexed).
31+
* Is the way I'm resizing logical? I read [this article](http://coding-geek.com/how-does-a-hashmap-work-in-java/) and
32+
that's where I got my ideas for how to resize the internal array.
33+
34+
## Implementation
35+
36+
<!-- language:lang-java --!>
37+
38+
public class HashMap<K, V> {
39+
private int size = 0;
40+
private int capacity = 16;
41+
private Entry<K, V>[] entries = new Entry[capacity];
42+
private double loadFactor = 0.75;
43+
44+
private static class Entry<K, V> {
45+
private final K key;
46+
private V value;
47+
private Entry<K, V> next = null;
48+
49+
public Entry(K key, V value) {
50+
this.key = key;
51+
this.value = value;
52+
}
53+
}
54+
55+
public HashMap() {
56+
}
57+
58+
public boolean isEmpty() {
59+
return this.size == 0;
60+
}
61+
62+
public int getSize() {
63+
return this.size;
64+
}
65+
66+
public boolean containsKey(K key) {
67+
Entry<K, V> matchingEntry = getMatchingEntry(key);
68+
69+
return matchingEntry != null && matchingEntry.key == key;
70+
}
71+
72+
public boolean containsValue(V value) {
73+
for (Entry<K, V> entry : this.entries) {
74+
while (entry != null && !matches(value, entry.value)) {
75+
entry = entry.next;
76+
}
77+
78+
if (entry != null) {
79+
return true;
80+
}
81+
}
82+
83+
return false;
84+
}
85+
86+
public V get(K key) {
87+
Entry<K, V> matchingEntry = getMatchingEntry(key);
88+
89+
return matchingEntry == null ? null : matchingEntry.value;
90+
}
91+
92+
public void put(K key, V value) {
93+
if (this.shouldResize()) {
94+
this.resize();
95+
}
96+
97+
if (addEntry(new Entry<>(key, value), this.entries)) {
98+
this.size++;
99+
}
100+
101+
}
102+
103+
public void remove(K key) {
104+
int index = indexOf(key);
105+
Entry<K, V> currentEntry = this.entries[index];
106+
107+
while (currentEntry != null && currentEntry.next != null && !matches(key, currentEntry.next.key)) {
108+
currentEntry = currentEntry.next;
109+
}
110+
111+
if (currentEntry != null) {
112+
// this case can only occur if there is only one non-null entry at the index
113+
if (matches(key, currentEntry.key)) {
114+
this.entries[index] = null;
115+
// this case can only occur because the next entry's key matched
116+
} else if (currentEntry.next != null) {
117+
currentEntry.next = currentEntry.next.next;
118+
}
119+
120+
this.size--;
121+
}
122+
}
123+
124+
private boolean shouldResize() {
125+
return this.size > Math.ceil((double) this.capacity * this.loadFactor);
126+
}
127+
128+
private void resize() {
129+
this.capacity = this.size * 2;
130+
131+
Entry<K, V>[] newEntries = new Entry[this.capacity];
132+
for (Entry<K, V> entry : this.entries) {
133+
if (entry != null) {
134+
this.setEntry(entry, newEntries);
135+
}
136+
}
137+
138+
this.entries = newEntries;
139+
}
140+
141+
private void setEntry(Entry<K, V> entry, Entry<K, V>[] entries){
142+
Entry<K, V> nextEntry = entry.next;
143+
entry.next = null;
144+
145+
this.addEntry(entry, entries);
146+
147+
if (nextEntry != null) {
148+
this.setEntry(nextEntry, entries);
149+
}
150+
}
151+
152+
private boolean addEntry(Entry<K, V> entry, Entry<K, V>[] entries) {
153+
int index = indexOf(entry.key);
154+
Entry<K, V> existingEntry = entries[index];
155+
156+
if (existingEntry == null) {
157+
entries[index] = entry;
158+
return true;
159+
} else {
160+
while (!this.matches(entry.key, existingEntry.key) && existingEntry.next != null) {
161+
existingEntry = existingEntry.next;
162+
}
163+
164+
if (this.matches(entry.key, existingEntry.key)) {
165+
existingEntry.value = entry.value;
166+
return false;
167+
}
168+
169+
existingEntry.next = entry;
170+
return true;
171+
172+
}
173+
}
174+
175+
private Entry<K, V> getMatchingEntry(K key) {
176+
Entry<K, V> existingEntry = this.entries[indexOf(key)];
177+
178+
while (existingEntry != null && !matches(key, existingEntry.key)) {
179+
existingEntry = existingEntry.next;
180+
}
181+
182+
return existingEntry;
183+
}
184+
185+
private int indexOf(K object) {
186+
return object == null ? 0 : hash(object) & (this.capacity - 1);
187+
}
188+
189+
private boolean matches(Object o1, Object o2) {
190+
return (o1 == null && o2 == null) ||
191+
(o1 != null && o2 != null && o1.equals(o2));
192+
}
193+
194+
/**
195+
* Applies a supplemental hash function to a given hashCode, which
196+
* defends against poor quality hash functions. This is critical
197+
* because HashMap uses power-of-two length hash tables, that
198+
* otherwise encounter collisions for hashCodes that do not differ
199+
* in lower bits. Note: Null keys always map to hash 0, thus index 0.
200+
*/
201+
private static int hash(Object key) {
202+
// This function ensures that hashCodes that differ only by
203+
// constant multiples at each bit position have a bounded
204+
// number of collisions (approximately 8 at default load factor).
205+
int h;
206+
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
207+
}
208+
}
Lines changed: 178 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,178 @@
1+
package problems.impl;
2+
3+
import java.util.Objects;
4+
5+
/**
6+
* https://codereview.stackexchange.com/questions/171650/hashmap-implementation/171662#171662
7+
* @param <K> Key Object
8+
* @param <V> Value Object
9+
*/
10+
11+
public class SimpleHashMap<K, V> {
12+
private int size = 0;
13+
private int capacity = 16;
14+
private double loadFactor = 0.75;
15+
16+
@SuppressWarnings("unchecked")
17+
private Entry<K, V>[] entries = new Entry[capacity];
18+
19+
private static class Entry<K, V> {
20+
private final K key;
21+
private V value;
22+
private Entry<K, V> next;
23+
24+
Entry(K key, V value) {
25+
this.key = key;
26+
this.value = value;
27+
}
28+
}
29+
30+
public boolean isEmpty() {
31+
return this.size == 0;
32+
}
33+
34+
public int getSize() {
35+
return this.size;
36+
}
37+
38+
public boolean containsKey(K key) {
39+
return getMatchingEntry(key) != null;
40+
}
41+
42+
public boolean containsValue(V value) {
43+
for (Entry<K, V> entry : this.entries) {
44+
while (entry != null && !matches(value, entry.value)) {
45+
entry = entry.next;
46+
}
47+
48+
if (entry != null) {
49+
return true;
50+
}
51+
}
52+
53+
return false;
54+
}
55+
56+
public V get(K key) {
57+
Entry<K, V> matchingEntry = getMatchingEntry(key);
58+
59+
return matchingEntry == null ? null : matchingEntry.value;
60+
}
61+
62+
public void put(K key, V value) {
63+
if (this.shouldResize()) {
64+
this.resize();
65+
}
66+
67+
if (addEntry(new Entry<>(key, value), this.entries)) {
68+
this.size++;
69+
}
70+
71+
}
72+
73+
public void remove(K key) {
74+
int index = indexOf(key);
75+
Entry<K, V> currentEntry = this.entries[index];
76+
77+
while (currentEntry != null && currentEntry.next != null && !matches(key, currentEntry.next.key)) {
78+
currentEntry = currentEntry.next;
79+
}
80+
81+
if (currentEntry != null) {
82+
// this case can only occur if there is only one non-null entry at the index
83+
if (matches(key, currentEntry.key)) {
84+
this.entries[index] = null;
85+
// this case can only occur because the next entry's key matched
86+
} else if (currentEntry.next != null) {
87+
currentEntry.next = currentEntry.next.next;
88+
}
89+
90+
this.size--;
91+
}
92+
}
93+
94+
private boolean shouldResize() {
95+
return this.size > Math.ceil((double) this.capacity * this.loadFactor);
96+
}
97+
98+
private void resize() {
99+
this.capacity = this.size * 2;
100+
101+
@SuppressWarnings("unchecked")
102+
Entry<K, V>[] newEntries = new Entry[this.capacity];
103+
for (Entry<K, V> entry : this.entries) {
104+
if (entry != null) {
105+
this.setEntry(entry, newEntries);
106+
}
107+
}
108+
109+
this.entries = newEntries;
110+
}
111+
112+
private void setEntry(Entry<K, V> entry, Entry<K, V>[] entries){
113+
Entry<K, V> nextEntry = entry.next;
114+
entry.next = null;
115+
116+
this.addEntry(entry, entries);
117+
118+
if (nextEntry != null) {
119+
this.setEntry(nextEntry, entries);
120+
}
121+
}
122+
123+
private boolean addEntry(Entry<K, V> entry, Entry<K, V>[] entries) {
124+
int index = indexOf(entry.key);
125+
Entry<K, V> existingEntry = entries[index];
126+
127+
if (existingEntry == null) {
128+
entries[index] = entry;
129+
return true;
130+
} else {
131+
while (!this.matches(entry.key, existingEntry.key) && existingEntry.next != null) {
132+
existingEntry = existingEntry.next;
133+
}
134+
135+
if (this.matches(entry.key, existingEntry.key)) {
136+
existingEntry.value = entry.value;
137+
return false;
138+
}
139+
140+
existingEntry.next = entry;
141+
return true;
142+
143+
}
144+
}
145+
146+
private Entry<K, V> getMatchingEntry(K key) {
147+
Entry<K, V> existingEntry = this.entries[indexOf(key)];
148+
149+
while (existingEntry != null && !matches(key, existingEntry.key)) {
150+
existingEntry = existingEntry.next;
151+
}
152+
153+
return existingEntry;
154+
}
155+
156+
private int indexOf(K object) {
157+
return object == null ? 0 : hash(object) & (this.capacity - 1);
158+
}
159+
160+
private boolean matches(Object o1, Object o2) {
161+
return Objects.equals(o1, o2);
162+
}
163+
164+
/**
165+
* Applies a supplemental hash function to a given hashCode, which
166+
* defends against poor quality hash functions. This is critical
167+
* because HashMap uses power-of-two length hash tables, that
168+
* otherwise encounter collisions for hashCodes that do not differ
169+
* in lower bits. Note: Null keys always map to hash 0, thus index 0.
170+
*/
171+
private static int hash(Object key) {
172+
// This function ensures that hashCodes that differ only by
173+
// constant multiples at each bit position have a bounded
174+
// number of collisions (approximately 8 at default load factor).
175+
int h;
176+
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
177+
}
178+
}

0 commit comments

Comments
 (0)