UNPKG

iterable-hash-table

Version:

A simple Iterable Hash Table written in TypeScript

464 lines (463 loc) 20.2 kB
'use strict'; var __generator = (this && this.__generator) || function (thisArg, body) { var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g; return g = { next: verb(0), "throw": verb(1), "return": verb(2) }, typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g; function verb(n) { return function (v) { return step([n, v]); }; } function step(op) { if (f) throw new TypeError("Generator is already executing."); while (_) try { if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t; if (y = 0, t) op = [op[0] & 2, t.value]; switch (op[0]) { case 0: case 1: t = op; break; case 4: _.label++; return { value: op[1], done: false }; case 5: _.label++; y = op[1]; op = [0]; continue; case 7: op = _.ops.pop(); _.trys.pop(); continue; default: if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; } if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; } if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; } if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; } if (t[2]) _.ops.pop(); _.trys.pop(); continue; } op = body.call(thisArg, _); } catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; } if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true }; } }; var __values = (this && this.__values) || function(o) { var s = typeof Symbol === "function" && Symbol.iterator, m = s && o[s], i = 0; if (m) return m.call(o); if (o && typeof o.length === "number") return { next: function () { if (o && i >= o.length) o = void 0; return { value: o && o[i++], done: !o }; } }; throw new TypeError(s ? "Object is not iterable." : "Symbol.iterator is not defined."); }; var __read = (this && this.__read) || function (o, n) { var m = typeof Symbol === "function" && o[Symbol.iterator]; if (!m) return o; var i = m.call(o), r, ar = [], e; try { while ((n === void 0 || n-- > 0) && !(r = i.next()).done) ar.push(r.value); } catch (error) { e = { error: error }; } finally { try { if (r && !r.done && (m = i["return"])) m.call(i); } finally { if (e) throw e.error; } } return ar; }; /** * A dictionary/hash map data structure for storing key/value pairs. Finding * an entry in a hash table takes O(1) constant time(same for 10 as 1 billion * items). Whereas finding an item via binary search takes time proportional * to the logarithm of the item in the list O(logn). Finding an item in a * regular old list takes time proportional to the length of the list O(n). * Very slow. Hash Tables = very fast */ var HashTable = /** @class */ (function () { function HashTable(s) { if (s === void 0) { s = 11; } this._buckets = new Array(s); this._numOfBuckets = 0; this._originalSize = s; this._size = s; this.length = 0; } ; /** * The Iterator method for the HashTable, allowing the table to be looped over if needed, * along with allowing the spread operator to work on it. O(n) with n being length of the * hashTable, NOT necessarily the size of the hashTable */ HashTable.prototype[Symbol.iterator] = function () { var i, _a, _b, tuple, e_1_1; var e_1, _c; return __generator(this, function (_d) { switch (_d.label) { case 0: i = 0; _d.label = 1; case 1: if (!(i < this._buckets.length)) return [3 /*break*/, 11]; if (!(this._buckets[i] !== undefined)) return [3 /*break*/, 9]; _d.label = 2; case 2: _d.trys.push([2, 7, 8, 9]); _a = (e_1 = void 0, __values(this._buckets[i])), _b = _a.next(); _d.label = 3; case 3: if (!!_b.done) return [3 /*break*/, 6]; tuple = _b.value; // Yield each tuple in the map return [4 /*yield*/, tuple]; case 4: // Yield each tuple in the map _d.sent(); _d.label = 5; case 5: _b = _a.next(); return [3 /*break*/, 3]; case 6: return [3 /*break*/, 9]; case 7: e_1_1 = _d.sent(); e_1 = { error: e_1_1 }; return [3 /*break*/, 9]; case 8: try { if (_b && !_b.done && (_c = _a.return)) _c.call(_a); } finally { if (e_1) throw e_1.error; } return [7 /*endfinally*/]; case 9: ; _d.label = 10; case 10: i++; return [3 /*break*/, 1]; case 11: ; return [2 /*return*/]; } }); }; ; /** * Hash is a method that converts any key such as 'alex', 'best hotels', 'google' etc.. * and converts it into a 'seemingly' random number. This number will be our hash key. * It's not really random since it will always be the same for any particular key. * Sort of like an encrypted version of our hash key that we will later decrypt when * we need to retrieve the desired key. Note that the number output by this function * cannot be any larger than the size of our hash table. See the size value we set above. * * @param {any} key The non-hashed value provided by the user, can be any type * @return {number} The hashed index from running method on the given key */ HashTable.prototype._hash = function (key) { // Does not allow keys or values to be undefined or have a length of 0 if (key === undefined || key.length === 0) { throw ('Key cannot be undefined'); } ; var i; var length = key.length - 3; var t0 = 0; var t1 = 0; var v0 = 0x9dc5; var v1 = 0x811c; for (i = 0; i < length; i++) { v0 ^= key.toString().charCodeAt(); t0 = v0 * 403; t1 = v1 * 403; t1 += v0 << 8; v1 = (t1 + (t0 >>> 16)) & 65535; v0 = t0 & 65535; } ; while (i < length + 3) { v0 ^= key.toString().charCodeAt(i++); t0 = v0 * 403; t1 = v1 * 403; t1 += v0 << 8; v1 = (t1 + (t0 >>> 16)) & 65535; v0 = t0 & 65535; } ; return (((v1 << 16) >>> 0) + v0) % this._size; }; ; /** * doubleHash is a method that is a second simpler hashing function used in tandem with the * original hash method. This makes it to where, if there is a collision, i is increased and * doubleHash is used to look for a open bucket. * * @param {any} key The non-hashed value provided by the user, can be any type * @return {number} The hashed index from running method on the given key */ HashTable.prototype._doubleHash = function (key) { // Helper function used to check if a number is Prime var isPrime = function (num) { for (var i = 2, s = Math.sqrt(num); i <= s; i++) if (num % i === 0) return false; return num > 1; }; var PRIME = this._size - 1; var hash = 0; while (!isPrime(PRIME)) { PRIME--; } ; for (var i = 0; i < key.length; i++) { // hash << 5 transalets to: hash * (2 ** 5) hash = (hash << 5) - hash + key.toString().charCodeAt(i); // Convert to 32bit unsigned integer hash = hash >>> 0; } ; return PRIME - Math.abs(hash % PRIME); }; ; /** * The set method will call the hash method to encrypt our insertion key and insert its * value at this specified index in our storage array (any index from 0 to size) By using * a Map as our buckets, we get the benifit of being able to handle hash collisions while * still maintaining 0(1) constant time retrieval of any key and value in the Map * * @param {any} key The non-hashed value provided by the user, can be any type * @param {any} value The value stored at the given key, can be any type * @return {number} The index of the Hash Table where the key/value pair is stored */ HashTable.prototype.set = function (key, value) { // Does not allow keys or values to be undefined or have a length of 0 switch (value && key) { case '': throw new TypeError('Key or Value cannot be an empty string'); break; case undefined: throw new TypeError('Key or Value cannot be undefined'); break; } ; var i = 0; // Because the result of doubleHash is multiplied by i, which starts at zero, the value of // doubleHash is only used if there is a collision var hashedIndex = this._hash(key) + (i * this._doubleHash(key)); // While i and the hashedIndex are less than or equal to the size of the Hash Table and while // the hashedIndex is already an exisiting bucket while (i <= this._size && hashedIndex <= this._size && this._buckets[hashedIndex]) { hashedIndex = this._hash(key) + (i++ * this._doubleHash(key)); //? // If the hashedIndex in the buckets array is a Map and if the map has the given key if (this._buckets[hashedIndex] && this._buckets[hashedIndex].has(key)) { // Break out of the while loop break; } ; } ; // If the bucket is empty at this hashedIndex if (!this._buckets[hashedIndex]) { // Create a bucket this._buckets[hashedIndex] = new Map(); this._numOfBuckets++; } ; // If the hashedIndex in the buckets array is a Map and the Map does NOT have the given key if (!this._buckets[hashedIndex].has(key)) { // Increase the length because it is a new inserction this.length++; } ; // Add key value pair to the bucket this._buckets[hashedIndex].set(key, value); // Resize the buckets array if the buckets array is 70% full if (this._numOfBuckets > ~~(this._size * 0.70)) { this._resize('increase'); } ; return hashedIndex; }; ; /** * The get method will call the hash method again, this time to decrypt our key(in a way). * Since it knows the exact index of retrieval in our storage array, we are able to bypass * the need to search through the array, resulting in an almost instantaneous O(1) constant * time retrieval of any key and value in our hash, even if more then one key value pair * exists at the same index thanks to the buckets being Maps * * @param {any} key The non-hashed value provided by the user, can be any type * @return {any} The key/value pair at the given key in the Hash Table or false if * the key does not exist in the Hash Table */ HashTable.prototype.get = function (key) { // Does not allow keys to be undefined or have a length of 0 switch (key) { case '': throw new TypeError('Key or Value cannot be an empty string'); break; case undefined: throw new TypeError('Key or Value cannot be undefined'); break; } ; var i = 0; var hashedIndex = this._hash(key) + (i * this._doubleHash(key)); // While i is less then the Hash Tables size and while the hashedIndex is less than the // Hash Tables size while (i <= this._size && hashedIndex <= this._size) { hashedIndex = this._hash(key) + (i++ * this._doubleHash(key)); // If the hashedIndex in the buckets array is a Map and the Map the given key if (this._buckets[hashedIndex] && this._buckets[hashedIndex].has(key)) { // Return the key/value pair at that index return this._buckets[hashedIndex].get(key); } ; } ; return false; }; ; /** * The delete method will call the hash method to decrypt the given key, giving us the exact * index needed for retrieval, allowing us to bypass the need to search through the array. We * then use the key to delete the correct tupel in our bucket (which are maps). Giving us O(1) * delection even if there is more then one value at the same index. * * @param {any} key The non-hashed value provided by the user, can be any type * @return {boolean} A boolean indicating wether or not the key/value pair was successfully * removed from the Hash Table */ HashTable.prototype.delete = function (key) { // Does not allow keys to be undefined or have a length of 0 switch (key) { case '': throw new TypeError('Key or Value cannot be an empty string'); break; case undefined: throw new TypeError('Key or Value cannot be undefined'); break; } ; var i = 0; var hashedIndex = this._hash(key) + (i * this._doubleHash(key)); // While i and the hashedIndex are less than or equal to the size of the Hash Table and while // the hashedIndex is already an exisiting bucket while (i <= this._size && hashedIndex <= this._size) { hashedIndex = this._hash(key) + (i++ * this._doubleHash(key)); // If the buckets array at the hashedIndex is not undefined if (this._buckets[hashedIndex]) { // If the key/value pair in the bucket was successfully deleted if (this._buckets[hashedIndex].delete(key)) { // Decrease the length since an key/value pair was removed this.length--; // If the Map is now empty if (this._buckets[hashedIndex].size === 0) { // Delete the map, this does not alter the buckets array length delete this._buckets[hashedIndex]; // Decrease the number of buckets this._numOfBuckets--; } ; // If the number of buckets is less than 40% if (this._numOfBuckets < ~~(this._size * 0.40)) { this._resize('decrease'); } ; // Return true indicating that the key/value pair was removed return true; } ; } ; } ; return false; }; ; /** * The resize method resizes the hash table by either increasing or decreasing the buckets * array depending upon if the number of buckets drops below 40% or increases above 70%. It * then iterates over the old buckets array and rehashes the keys and inserts the key/value * pairs into the new resized array. * * @param {string} sizeType A string indicating wether to increase or decrease the size of * the Hash Table */ HashTable.prototype._resize = function (sizeType) { var e_2, _a; // Helper function used to check if a number is Prime var isPrime = function (num) { for (var i = 2, s = Math.sqrt(num); i <= s; i++) if (num % i === 0) return false; return num > 1; }; // If the buckets array is less than 70% or greater than 20% full if (this._numOfBuckets < ~~(this._size * 0.70) && this._numOfBuckets > ~~(this._size * 0.40)) { throw new Error('Cannot resize the Hash Table when the number of buckets is between 40% and 70%'); } ; // If the resize argument is not valid if (sizeType !== 'increase' && sizeType !== 'decrease') { throw new Error('Invalid resize argument'); } ; var newSize = 0; // If the hashTable needs to increase in size if (sizeType === 'increase') { // Double size newSize = this._size * 2; // Keep increasing size untill it is a prime number while (!isPrime(newSize)) { newSize++; } ; } ; // If the hashTable needs to decrease in size if (sizeType === 'decrease') { // Half the size newSize = ~~(this._size / 2); // Keep decreasing the size untill it is a prime number and while new size is greater // than or equal to the original size while (!isPrime(newSize) && newSize >= this._originalSize) { newSize--; } ; } ; // Set the new size this._size = newSize; // Create a copy of the current buckets array var oldBucketsArray = this._buckets.slice(); // Replace the current buckets array with the new array and reset the length and numOfBuckets this._buckets = new Array(newSize); this.length = 0; this._numOfBuckets = 0; // Loop over the old buckets array for (var i = 0, len = oldBucketsArray.length; i < len; i++) { // If the bucket exists if (oldBucketsArray[i] !== undefined) { try { // Loop over the map for (var _b = (e_2 = void 0, __values(oldBucketsArray[i])), _c = _b.next(); !_c.done; _c = _b.next()) { var _d = __read(_c.value, 2), k = _d[0], v = _d[1]; // Add all of the key/value pairs from the old buckets array to the new array this.set(k, v); } } catch (e_2_1) { e_2 = { error: e_2_1 }; } finally { try { if (_c && !_c.done && (_a = _b.return)) _a.call(_b); } finally { if (e_2) throw e_2.error; } } ; } ; } ; oldBucketsArray = []; }; ; /** * Clear is a method that resets the Hash Table back to its initial state */ HashTable.prototype.clear = function () { this._buckets = Array(this._originalSize); this._size = this._originalSize; this._numOfBuckets = 0; this.length = 0; }; ; return HashTable; }()); ; export default HashTable;