diff --git a/src/data-structures/hash-table.js b/src/data-structures/hash-table.js new file mode 100644 index 00000000..2b7dff44 --- /dev/null +++ b/src/data-structures/hash-table.js @@ -0,0 +1,213 @@ +/** + * Hash Table + * + * An associative array, that can map keys + * (strings and numbers) to values in O(1). + * + * @example + * var hash = require('path-to-algorithms/src/data-structures'+ + * '/hash-table'); + * var hashTable = new hash.Hashtable(); + * + * hashTable.put(10, 'value'); + * hashTable.put('key', 10); + * + * console.log(hashTable.get(10)); // 'value' + * console.log(hashTable.get('key')); // 10 + * + * hashTable.remove(10); + * hashTable.remove('key'); + * + * console.log(hashTable.get(10)); // undefined + * console.log(hashTable.get('key')); // undefined + * + * @module data-structures/hash-table +*/ +(function (exports) { + 'use strict'; + + exports.Node = function (key, data) { + this.key = key; + this.data = data; + this.next = undefined; + this.prev = undefined; + }; + + exports.Hashtable = function () { + this.buckets = []; + // The higher the bucket count; less likely for collisions. + this.maxBucketCount = 100; + }; + + /* + Using simple non-crypto x->integer based hash. + */ + exports.Hashtable.prototype.hashCode = function (val) { + var i; + var hashCode = 0; + var character; + + // If value to be hashed is already an integer, return it. + if (val.length === 0 || val.length === undefined) { + return val; + } + + for (i = 0; i < val.length; i += 1) { + character = val.charCodeAt(i); + /*jshint -W016 */ + hashCode = ((hashCode << 5) - hashCode) + character; + hashCode = hashCode & hashCode; + /*jshint -W016 */ + } + + return hashCode; + }; + + exports.Hashtable.prototype.put = function (key, data, hashCode) { + /* + Make collision testing easy with optional hashCode parameter. + That should not be used! Only by spec/tests. + */ + if (hashCode === undefined) { + // Typical use + hashCode = this.hashCode(key); + } else if (hashCode.length > 0) { + // Testing/Spec - String hash passed, convert to int based hash. + hashCode = this.hashCode(hashCode); + } + // Adjust hash to fit within buckets. + hashCode = hashCode % this.maxBucketCount; + + var newNode = new exports.Node(key, data); + + // No element exists at hash/index for given key -> put in table. + if (this.buckets[hashCode] === undefined) { + this.buckets[hashCode] = newNode; + return; + } + + // Element exists at hash/index for given key, but, same key -> overwrite. + if (this.buckets[hashCode].key === key) { + this.buckets[hashCode].data = data; + return; + } + + /* + Item exists at hash/index for key, but different key. + Handle collision. + */ + var first = this.buckets[hashCode]; + while (first.next !== undefined) { + first = first.next; + } + first.next = newNode; + newNode.prev = first; + }; + + exports.Hashtable.prototype.get = function (key, hashCode) { + /* + Make collision testing easy with optional hashCode parameter. + That should not be used! Only by spec/tests. + */ + if (hashCode === undefined) { + // Typical use + hashCode = this.hashCode(key); + } else if (hashCode.length > 0) { + // Testing/Spec - String hash passed, convert to int based hash. + hashCode = this.hashCode(hashCode); + } + hashCode = hashCode % this.maxBucketCount; + + if (this.buckets[hashCode] === undefined) { + return undefined; + } else if ( + this.buckets[hashCode].next === undefined && + this.buckets[hashCode].key === key + ) { + return this.buckets[hashCode].data; + } else { + var first = this.buckets[hashCode]; + while ( + first !== undefined && + first.next !== undefined && + first.key !== key + ) { + first = first.next; + } + + if (first.key === key) { + return first.data; + } else { + return undefined; + } + } + }; + + exports.Hashtable.prototype.remove = function (key, hashCode) { + /* + Make collision testing easy with optional hashCode parameter. + That should not be used! Only by spec/tests. + */ + if (hashCode === undefined) { + // Typical use + hashCode = this.hashCode(key); + } else if (hashCode.length > 0) { + // Testing/Spec - String hash passed, convert to int based hash. + hashCode = this.hashCode(hashCode); + } + hashCode = hashCode % this.maxBucketCount; + + if (this.buckets[hashCode] === undefined) { + return undefined; + } else if (this.buckets[hashCode].next === undefined) { + this.buckets[hashCode] = undefined; + } else { + var first = this.buckets[hashCode]; + + while ( + first !== undefined && + first.next !== undefined && + first.key !== key + ) { + first = first.next; + } + + var removedValue = first.data; + + // Removing (B) + // (B) : only item in bucket + if (first.prev === undefined && first.next === undefined) { + first = undefined; + return removedValue; + } + + // (B) - A - C: start link in bucket + if (first.prev === undefined && first.next !== undefined) { + first.data = first.next.data; + first.key = first.next.key; + if (first.next.next !== undefined) { + first.next = first.next.next; + } else { + first.next = undefined; + } + return removedValue; + } + + // A - (B) : end link in bucket + if (first.prev !== undefined && first.next === undefined) { + first.prev.next = undefined; + first = undefined; + return removedValue; + } + + // A - (B) - C : middle link in bucket + if (first.prev !== undefined && first.next !== undefined) { + first.prev.next = first.next; + first.next.prev = first.prev; + first = undefined; + return removedValue; + } + + } + }; +})(typeof window === 'undefined' ? module.exports : window); diff --git a/test/data-structures/hash-table.spec.js b/test/data-structures/hash-table.spec.js new file mode 100644 index 00000000..03959822 --- /dev/null +++ b/test/data-structures/hash-table.spec.js @@ -0,0 +1,129 @@ +'use strict'; + +var mod = require('../../src/data-structures/hash-table.js'); +var Node = mod.Node; +var Hashtable = mod.Hashtable; + +describe('Node', function () { + it('should be a constructor function', function () { + expect(typeof Node).toBe('function'); + }); +}); + +describe('Hash table', function () { + it('should be a constructor function.', function () { + expect(typeof Hashtable).toBe('function'); + }); + it('should start with empty table.', function () { + expect(new Hashtable().buckets.length).toBe(0); + }); + it('should put() K(int):V in table properly.', function () { + var hashTable = new Hashtable(); + hashTable.put(10, 'value'); + expect(hashTable.buckets[10].data).toBe('value'); + }); + it('should put() K(str):V in table properly.', function () { + var hashTable = new Hashtable(); + hashTable.put('key', 'value'); + /* + 'key' hashCode()'s to 106079. Then the hash is adjusted to fit + the number of configurable buckets (array size). + 106079 % 100 (100 is default maxBucketCount) + result is 79. + This is done to avoid using get() since it's untested at this point. + */ + expect(hashTable.buckets[79].data).toBe('value'); + }); + it('should put() multiple K(int):Vs with hash collisions in properly (1).', function () { + var hashTable = new Hashtable(); + // Same hash so going to same bucket, but different keys. Collision. + hashTable.put(10, 'value', 'someHash'); + hashTable.put(35, 'anotherValue', 'someHash'); + /* + 'someHash' hashCode()'s to 1504481314. Then the hash is adjusted to fit + the number of configurable buckets (array size). + 1504481314 % 100 (100 is default maxBucketCount) + result is 14. + This is done to avoid using get() since it's untested at this point. + */ + expect(hashTable.buckets[14].data).toBe('value'); + expect(hashTable.buckets[14].next.data).toBe('anotherValue'); + }); + it('should put() multiple K:Vs with hash collisions in properly (2).', function () { + var hashTable = new Hashtable(); + hashTable.put(10, 'value', 'someHash'); + hashTable.put(35, 'anotherValue', 'someHash'); + hashTable.put(77, 'lastValue', 'someHash'); + expect(hashTable.buckets[14].data).toBe('value'); + expect(hashTable.buckets[14].next.data).toBe('anotherValue'); + expect(hashTable.buckets[14].next.next.data).toBe('lastValue'); + }); + it('should get() a k:v from table properly.', function () { + var hashTable = new Hashtable(); + hashTable.put(10, 'value'); + expect(hashTable.get(10)).toBe('value'); + }); + it('should get() a k:v with collisions from table properly (1).', function () { + var hashTable = new Hashtable(); + hashTable.put(10, 'value', 'someHash'); + hashTable.put(35, 'anotherValue', 'someHash'); + expect(hashTable.get(35, 'someHash')).toBe('anotherValue'); + }); + it('should get() a k:v with collisions from table properly (2).', function () { + var hashTable = new Hashtable(); + hashTable.put(10, 'value', 'someHash'); + hashTable.put(35, 'anotherValue', 'someHash'); + hashTable.put(77, 'lastValue', 'someHash'); + expect(hashTable.get(77, 'someHash')).toBe('lastValue'); + }); + it('should get() a k:v with collisions from table properly (3).', function () { + var hashTable = new Hashtable(); + hashTable.put(10, 'value', 'someHash'); + hashTable.put(35, 'anotherValue', 'someHash'); + hashTable.put(77, 'lastValue', 'someHash'); + expect(hashTable.get(35, 'someHash')).toBe('anotherValue'); + }); + it('should get() a k:v with collisions from table properly (4).', function () { + var hashTable = new Hashtable(); + hashTable.put(10, 'value', 'someHash'); + hashTable.put(35, 'anotherValue', 'someHash'); + hashTable.put(77, 'lastValue', 'someHash'); + expect(hashTable.get(10, 'someHash')).toBe('value'); + }); + it('should remove() a k:v from table properly.', function () { + // remove only node/link in bucket : (B) + var hashTable = new Hashtable(); + hashTable.put(10, 'value'); + hashTable.remove(10); + expect(hashTable.get(10)).toBe(undefined); + }); + it('should remove() a k:v with collisions from table properly (2).', function () { + // remove start node/link in bucket : (B) - A + var hashTable = new Hashtable(); + hashTable.put(10, 'value', 'someHash'); + hashTable.put(35, 'anotherValue', 'someHash'); + expect(hashTable.remove(10, 'someHash')).toBe('value'); + expect(hashTable.get(35, 'someHash')).toBe('anotherValue'); + expect(hashTable.get(10, 'someHash')).toBe(undefined); + }); + it('should remove() a k:v with collisions from table properly (3).', function () { + // remove start node/link in bucket : (B) - A - C + var hashTable = new Hashtable(); + hashTable.put(10, 'value', 'someHash'); + hashTable.put(35, 'anotherValue', 'someHash'); + hashTable.put(66, 'lastValue', 'someHash'); + expect(hashTable.remove(10, 'someHash')).toBe('value'); + expect(hashTable.get(35, 'someHash')).toBe('anotherValue'); + expect(hashTable.get(66, 'someHash')).toBe('lastValue'); + }); + it('should remove() a k:v with collisions from table properly (4).', function () { + // remove middle node/link in bucket : A - (B) - C + var hashTable = new Hashtable(); + hashTable.put(10, 'value', 'someHash'); + hashTable.put(35, 'anotherValue', 'someHash'); + hashTable.put(66, 'lastValue', 'someHash'); + expect(hashTable.remove(35, 'someHash')).toBe('anotherValue'); + expect(hashTable.get(10, 'someHash')).toBe('value'); + expect(hashTable.get(66, 'someHash')).toBe('lastValue'); + }); +});