Skip to content

Commit 0707ece

Browse files
committedSep 23, 2018
hashmap iterators
·
v1.0.01.0.0
1 parent 93ea854 commit 0707ece

File tree

4 files changed

+281
-91
lines changed

4 files changed

+281
-91
lines changed
 

‎README.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,4 +37,5 @@ alert('foo');
3737
/* eslint-disable no-alert, no-console */
3838
alert('foo');
3939
console.log('bar');
40-
/* e
40+
/* eslint-enable no-alert */
41+
```

‎benchmarks/hashmap.perf.js

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -130,12 +130,18 @@ function useBenchmark() {
130130
suite
131131

132132
/*
133-
======== Results ========
134-
490.84 ops/s with HashMap
135-
293.756 ops/s with HashMap3
136-
64.091 ops/s with HashMap4
133+
======== Results ========
134+
2,653.472 ops/s with Map (built-in)
135+
469.016 ops/s with HashMap
136+
355.064 ops/s with HashMap3
137+
66.808 ops/s with HashMap4
137138
*/
138139

140+
suite.add('Map (built-in)', function() {
141+
const map = new Map();
142+
testMapOperations(map);
143+
})
144+
139145
// HashMap3 x 543 ops/sec ±1.53% (84 runs sampled)
140146
suite.add('HashMap3', function() {
141147
map = new HashMap3();
@@ -156,7 +162,7 @@ function useBenchmark() {
156162
// add listeners
157163
.on('cycle', function(event) {
158164
console.log(String(event.target));
159-
if (map.collisions) {
165+
if (map && map.collisions) {
160166
console.log('\tcollisions', map.collisions);
161167
}
162168
})
Lines changed: 158 additions & 70 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,21 @@
1+
/* eslint-disable no-bitwise, no-iterator, no-restricted-syntax */
12
const LinkedList = require('../linked-lists/linked-list');
23
const { nextPrime } = require('./primes');
34

5+
/**
6+
* The Map object holds key-value pairs.
7+
* Any value (both objects and primitive values) may be used as either a key or a value.
8+
*
9+
* Features:
10+
* - HashMap offers avg. 0(1) lookup, insertion and deletion.
11+
* - Keys and values are ordered by their insertion order (like Java's LinkedHashMap)
12+
* - It contains only unique key.
13+
* - It may have one null key and multiple null values.
14+
*/
415
class HashMap {
516
/**
617
* Initialize array that holds the values.
7-
* @param {number} initialCapacity initial size of the array
18+
* @param {number} initialCapacity initial size of the array (should be a prime)
819
* @param {number} loadFactor if set, the Map will automatically
920
* rehash when the load factor threshold is met
1021
*/
@@ -28,88 +39,109 @@ class HashMap {
2839
this.keysTrackerIndex = keysTrackerIndex;
2940
}
3041

31-
set(key, value) {
32-
const index = this.hashFunction(key);
33-
this.setBucket(index, key, value);
34-
return this;
35-
}
36-
37-
get(key) {
38-
const index = this.hashFunction(key);
39-
const bucket = this.getBucket(index, key);
40-
return bucket && bucket.value;
41-
}
42-
43-
has(key) {
44-
const index = this.hashFunction(key);
45-
const bucket = this.getBucket(index, key);
46-
return bucket !== undefined;
47-
}
48-
49-
delete(key) {
50-
const index = this.hashFunction(key);
51-
return this.removeBucket(index, key);
52-
}
53-
5442
/**
55-
* Uses FVN-1a hashing algorithm for 32 bits
43+
* Polynomial hash codes are used to hash String typed keys.
44+
*
45+
* It uses FVN-1a hashing algorithm for 32 bits
5646
* @see https://en.wikipedia.org/wiki/Fowler%E2%80%93Noll%E2%80%93Vo_hash_function
5747
* @param {any} key
5848
* @return {integer} bucket index
5949
*/
6050
hashFunction(key) {
61-
const str = key.toString();
62-
let hash = 2166136261;
51+
const str = String(key);
52+
let hash = 2166136261; // FNV_offset_basis (32 bit)
6353
for (let i = 0; i < str.length; i += 1) {
64-
hash ^= str.codePointAt(i); // eslint-disable-line no-bitwise
65-
hash *= 16777619;
54+
hash ^= str.codePointAt(i);
55+
hash *= 16777619; // 32 bit FNV_prime
6656
}
67-
return (hash >>> 0) % this.buckets.length; // eslint-disable-line no-bitwise
57+
return (hash >>> 0) % this.buckets.length;
6858
}
6959

70-
setBucket(index, key, value) {
71-
this.buckets[index] = this.buckets[index] || new LinkedList();
72-
const bucket = this.buckets[index];
73-
// update value if key already exists
74-
const hasKey = bucket.find(({ value: data }) => {
75-
if (key === data.key) {
76-
data.value = value; // update value
77-
return true;
60+
/**
61+
* Find an entry inside a bucket.
62+
*
63+
* The bucket is an array of LinkedList.
64+
* Entries are each of the nodes in the linked list.
65+
*
66+
* Avg. Runtime: O(1)
67+
* If there are many collisions it could be O(n).
68+
*
69+
* @param {any} key
70+
* @param {function} callback (optional) operation to
71+
* perform once the entry has been found
72+
* @returns {any} entry (LinkedList's node's value)
73+
*/
74+
getEntry(key, callback = () => {}) {
75+
const index = this.hashFunction(key);
76+
const bucket = this.buckets[index] || new LinkedList();
77+
return bucket.find(({ value: entry }) => {
78+
if (key === entry.key) {
79+
callback(entry);
80+
return entry;
7881
}
7982
return undefined;
8083
});
81-
// add key/value if it doesn't find the key
82-
if (!hasKey) {
83-
bucket.push({
84-
key,
85-
value,
86-
order: this.keysTrackerIndex,
87-
});
84+
}
85+
86+
/**
87+
* Insert a key/value pair into the hash map.
88+
* If the key is already there replaces its content.
89+
* Avg. Runtime: O(1)
90+
* In the case a rehash is needed O(n).
91+
* @param {any} key
92+
* @param {any} value
93+
* @returns {HashMap} Return the Map object to allow chaining
94+
*/
95+
set(key, value) {
96+
const index = this.hashFunction(key);
97+
this.buckets[index] = this.buckets[index] || new LinkedList();
98+
const bucket = this.buckets[index];
99+
100+
const found = this.getEntry(key, (entry) => {
101+
entry.value = value; // update value if key already exists
102+
});
103+
104+
if (!found) { // add key/value if it doesn't find the key
105+
bucket.push({ key, value, order: this.keysTrackerIndex });
88106
this.keysTrackerArray[this.keysTrackerIndex] = key;
89107
this.keysTrackerIndex += 1;
90108
this.size += 1;
91-
// count collisions
92-
if (bucket.size > 1) {
93-
this.collisions += 1;
94-
}
95-
96-
if (this.isBeyondloadFactor()) {
97-
this.rehash();
98-
}
109+
if (bucket.size > 1) { this.collisions += 1; }
110+
if (this.isBeyondloadFactor()) { this.rehash(); }
99111
}
112+
return this;
100113
}
101114

102-
getBucket(index, key) {
103-
const bucket = this.buckets[index] || new LinkedList();
104-
return bucket.find(({ value: data }) => {
105-
if (key === data.key) {
106-
return data;
107-
}
108-
return undefined;
109-
});
115+
/**
116+
* Gets the value out of the hash map
117+
*
118+
* @param {any} key
119+
* @returns {any} value associated to the key, or undefined if there is none.
120+
*/
121+
get(key) {
122+
const bucket = this.getEntry(key);
123+
return bucket && bucket.value;
124+
}
125+
126+
/**
127+
* Search for key and return true if it was found
128+
* @param {any} key
129+
* @returns {boolean} indicating whether an element
130+
* with the specified key exists or not.
131+
*/
132+
has(key) {
133+
const bucket = this.getEntry(key);
134+
return bucket !== undefined;
110135
}
111136

112-
removeBucket(index, key) {
137+
/**
138+
* Removes the specified element from a Map object.
139+
* @param {*} key
140+
* @returns {boolean} true if an element in the Map object existed
141+
* and has been removed, or false if the element did not exist.
142+
*/
143+
delete(key) {
144+
const index = this.hashFunction(key);
113145
const bucket = this.buckets[index];
114146

115147
if (!bucket || bucket.size === 0) {
@@ -126,28 +158,36 @@ class HashMap {
126158
});
127159
}
128160

161+
/**
162+
* Load factor - measure how full the Map is.
163+
* It's ratio between items on the map and total size of buckets
164+
*/
129165
getLoadFactor() {
130166
return this.size / this.buckets.length;
131167
}
132168

169+
/**
170+
* Check if a rehash is due
171+
*/
133172
isBeyondloadFactor() {
134173
return this.getLoadFactor() > this.loadFactor;
135174
}
136175

137176
/**
138-
* @returns keys without holes (empty spaces of deleted keys)
177+
* Rehash means to create a new Map with a new (higher)
178+
* capacity with the purpose of outgrow collisions.
179+
* @param {integer} newBucketSize new bucket size by default
180+
* is the 2x the amount of data or bucket size.
139181
*/
140-
keys() {
141-
return Object.values(this.keysTrackerArray);
142-
}
143-
144182
rehash(newBucketSize = Math.max(this.size, this.buckets.length) * 2) {
145183
const newCapacity = nextPrime(newBucketSize);
146184
const newMap = new HashMap(newCapacity);
147-
this.keys().forEach((key) => {
185+
186+
for (const key of this.keys()) {
148187
newMap.set(key, this.get(key));
149-
});
150-
const newArrayKeys = newMap.keys();
188+
}
189+
190+
const newArrayKeys = Array.from(newMap.keys());
151191

152192
this.reset(
153193
newMap.buckets,
@@ -157,6 +197,54 @@ class HashMap {
157197
newArrayKeys.length,
158198
);
159199
}
200+
201+
/**
202+
* Keys for each element in the Map object in insertion order.
203+
* @returns {Iterator} keys without holes (empty spaces of deleted keys)
204+
*/
205+
* keys() {
206+
for (let index = 0; index < this.keysTrackerArray.length; index++) {
207+
const key = this.keysTrackerArray[index];
208+
if (key !== undefined) {
209+
yield key;
210+
}
211+
}
212+
}
213+
214+
/**
215+
* Values for each element in the Map object in insertion order.
216+
* @returns {Iterator} values without holes (empty spaces of deleted values)
217+
*/
218+
* values() {
219+
for (const key of this.keys()) {
220+
yield this.get(key);
221+
}
222+
}
223+
224+
/**
225+
* Contains the [key, value] pairs for each element in the Map object in insertion order.
226+
* @returns {Iterator}
227+
*/
228+
* entries() {
229+
for (const key of this.keys()) {
230+
yield [key, this.get(key)];
231+
}
232+
}
233+
234+
/**
235+
* the same function object as the initial value of the `entries` method.
236+
* Contains the [key, value] pairs for each element in the Map.
237+
*/
238+
* [Symbol.iterator]() {
239+
yield* this.entries();
240+
}
241+
242+
/**
243+
* @returns {integer} number of elements in the hashmap
244+
*/
245+
get length() {
246+
return this.size;
247+
}
160248
}
161249

162250
module.exports = HashMap;

‎src/data-structures/hash-maps/hashmap.spec.js

Lines changed: 110 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,112 @@ const HashMap = require('./hashmap');
33

44

55
describe('HashMap Tests', () => {
6-
describe('without collisions', () => {
7-
let hashMap;
6+
let hashMap;
87

9-
beforeEach(() => {
10-
hashMap = new HashMap();
8+
beforeEach(() => {
9+
hashMap = new HashMap();
10+
});
11+
12+
describe('set and get basics', () => {
13+
it('should hold one null key', () => {
14+
hashMap.set(null, 1);
15+
hashMap.set(null, 2);
16+
expect(hashMap.get(null)).toBe(2);
17+
});
18+
19+
it('should hold multiple null values', () => {
20+
hashMap.set(1, null);
21+
hashMap.set(2, null);
22+
expect(hashMap.get(1)).toBe(null);
23+
expect(hashMap.get(2)).toBe(null);
24+
});
25+
});
26+
27+
describe('#keys', () => {
28+
it('should get keys', () => {
29+
hashMap.set(0, 'foo');
30+
hashMap.set(null, 'fox');
31+
hashMap.set('a', 'bar');
32+
hashMap.set({}, 'baz');
33+
34+
const mapIter = hashMap.keys();
35+
36+
expect(mapIter.next().value).toBe(0);
37+
expect(mapIter.next().value).toBe(null);
38+
expect(mapIter.next().value).toBe('a');
39+
expect(mapIter.next().value).toEqual({});
40+
});
41+
42+
it('should not have holes', () => {
43+
hashMap.set('0', 'foo');
44+
hashMap.set(1, 'bar');
45+
hashMap.set({}, 'baz');
46+
47+
hashMap.delete(1);
48+
49+
expect([...hashMap.keys()]).toEqual(['0', {}]);
50+
});
51+
});
52+
53+
describe('#values', () => {
54+
it('should get values', () => {
55+
hashMap.set('0', 'foo');
56+
hashMap.set(1, 'bar');
57+
hashMap.set({}, 'baz');
58+
59+
const mapIter = hashMap.values();
60+
61+
expect(mapIter.next().value).toBe('foo');
62+
expect(mapIter.next().value).toBe('bar');
63+
expect(mapIter.next().value).toBe('baz');
1164
});
1265

66+
it('should not have holes', () => {
67+
hashMap.set('0', 'foo');
68+
hashMap.set(1, 'bar');
69+
hashMap.set({}, 'baz');
70+
71+
hashMap.delete(1);
72+
73+
expect(Array.from(hashMap.values())).toEqual(['foo', 'baz']);
74+
});
75+
});
76+
77+
describe('#entries', () => {
78+
it('should get values', () => {
79+
hashMap.set('0', 'foo');
80+
hashMap.set(1, 'bar');
81+
hashMap.set({}, 'baz');
82+
83+
const mapIter = hashMap.entries();
84+
85+
expect(mapIter.next().value).toEqual(['0', 'foo']);
86+
expect(mapIter.next().value).toEqual([1, 'bar']);
87+
expect(mapIter.next().value).toEqual([{}, 'baz']);
88+
});
89+
90+
it('should not have holes', () => {
91+
hashMap.set('0', 'foo');
92+
hashMap.set(1, 'bar');
93+
hashMap.set({}, 'baz');
94+
95+
hashMap.delete(1);
96+
expect(hashMap.length).toBe(2);
97+
expect(hashMap.size).toBe(2);
98+
99+
expect(Array.from(hashMap.entries())).toEqual([
100+
['0', 'foo'],
101+
[{}, 'baz'],
102+
]);
103+
104+
expect(Array.from(hashMap)).toEqual([
105+
['0', 'foo'],
106+
[{}, 'baz'],
107+
]);
108+
});
109+
});
110+
111+
describe('without collisions', () => {
13112
it('set and get values', () => {
14113
hashMap.set('test', 'one');
15114
expect(hashMap.get('test')).toBe('one');
@@ -65,8 +164,6 @@ describe('HashMap Tests', () => {
65164
});
66165

67166
describe('with many values (and collisions)', () => {
68-
let hashMap;
69-
70167
beforeEach(() => {
71168
hashMap = new HashMap(1, Number.MAX_SAFE_INTEGER);
72169

@@ -132,20 +229,18 @@ describe('HashMap Tests', () => {
132229
expect(hashMap.has('This Is What You Came For')).toBe(false);
133230
expect(hashMap.size).toBe(3);
134231

135-
expect(hashMap.keys()).toEqual(['Despacito', 'Bailando', 'Dura']);
232+
expect(Array.from(hashMap.keys())).toEqual(['Despacito', 'Bailando', 'Dura']);
136233

137234
expect(hashMap.delete('Bailando')).toBe(true);
138235
expect(hashMap.delete('Bailando')).toBe(false);
139236
expect(hashMap.get('Bailando')).toBe(undefined);
140237

141238
expect(hashMap.size).toBe(2);
142-
expect(hashMap.keys()).toEqual(['Despacito', 'Dura']);
239+
expect([...hashMap.keys()]).toEqual(['Despacito', 'Dura']);
143240
});
144241
});
145242

146243
describe('#rehash', () => {
147-
let hashMap;
148-
149244
beforeEach(() => {
150245
hashMap = new HashMap(1, 11);
151246

@@ -191,14 +286,14 @@ describe('HashMap Tests', () => {
191286
expect(hashMap.get('Bailando')).toBe('Enrique Iglesias');
192287
expect(hashMap.get('Alone')).toBe('Alan Walker');
193288

194-
expect(hashMap.keys().length).toBe(13);
289+
expect(Array.from(hashMap.keys()).length).toBe(13);
195290
expect(hashMap.size).toBe(13);
196291
expect(hashMap.keysTrackerIndex).toBe(13);
197292
// after the rehash the hole should have been removed
198-
expect(hashMap.keysTrackerArray).toEqual(["Pineapple", "Despacito",
199-
"Bailando", "Dura", "Lean On", "Hello", "Wake Me Up", "Brother",
200-
"Faded", "The Spectre", "All About That Bass", "Alone",
201-
"Rolling in the Deep"]);
293+
expect(hashMap.keysTrackerArray).toEqual(['Pineapple', 'Despacito',
294+
'Bailando', 'Dura', 'Lean On', 'Hello', 'Wake Me Up', 'Brother',
295+
'Faded', 'The Spectre', 'All About That Bass', 'Alone',
296+
'Rolling in the Deep']);
202297
});
203298
});
204299
});

0 commit comments

Comments
 (0)
Please sign in to comment.