1+ /* eslint-disable no-bitwise, no-iterator, no-restricted-syntax */
12const LinkedList = require ( '../linked-lists/linked-list' ) ;
23const { 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+ */
415class 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
162250module . exports = HashMap ;
0 commit comments