/* eslint no-constant-condition: 0 */ /** * Mnemonist CritBitTreeMap * ========================= * * JavaScript implementation of a crit-bit tree, also called PATRICIA tree. * This tree is a basically a bitwise radix tree and is supposedly much more * efficient than a standard Trie. * * [References]: * https://cr.yp.to/critbit.html * https://www.imperialviolet.org/binary/critbit.pdf */ var bitwise = require('./utils/bitwise.js'); /** * Helpers. */ /** * Helper returning the direction we need to take given a key and an * encoded critbit. * * @param {string} key - Target key. * @param {number} critbit - Packed address of byte + mask. * @return {number} - 0, left or 1, right. */ function getDirection(key, critbit) { var byteIndex = critbit >> 8; if (byteIndex > key.length - 1) return 0; var byte = key.charCodeAt(byteIndex), mask = critbit & 0xff; return (1 + (byte | mask)) >> 8; } /** * Helper returning the packed address of byte + mask or -1 if strings * are identical. * * @param {string} a - First key. * @param {string} b - Second key. * @return {number} - Packed address of byte + mask. */ function findCriticalBit(a, b) { var i = 0, tmp; // Swapping so a is the shortest if (a.length > b.length) { tmp = b; b = a; a = tmp; } var l = a.length, mask; while (i < l) { if (a[i] !== b[i]) { mask = bitwise.criticalBit8Mask( a.charCodeAt(i), b.charCodeAt(i) ); return (i << 8) | mask; } i++; } // Strings are identical if (a.length === b.length) return -1; // NOTE: x ^ 0 is the same as x mask = bitwise.criticalBit8Mask(b.charCodeAt(i)); return (i << 8) | mask; } /** * Class representing a crit-bit tree's internal node. * * @constructor * @param {number} critbit - Packed address of byte + mask. */ function InternalNode(critbit) { this.critbit = critbit; this.left = null; this.right = null; } /** * Class representing a crit-bit tree's external node. * Note that it is possible to replace those nodes by flat arrays. * * @constructor * @param {string} key - Node's key. * @param {any} value - Arbitrary value. */ function ExternalNode(key, value) { this.key = key; this.value = value; } /** * CritBitTreeMap. * * @constructor */ function CritBitTreeMap() { // Properties this.root = null; this.size = 0; this.clear(); } /** * Method used to clear the CritBitTreeMap. * * @return {undefined} */ CritBitTreeMap.prototype.clear = function() { // Properties this.root = null; this.size = 0; }; /** * Method used to set the value of the given key in the trie. * * @param {string} key - Key to set. * @param {any} value - Arbitrary value. * @return {CritBitTreeMap} */ CritBitTreeMap.prototype.set = function(key, value) { // Tree is empty if (this.size === 0) { this.root = new ExternalNode(key, value); this.size++; return this; } // Walk state var node = this.root, ancestors = [], path = [], ancestor, parent, child, critbit, internal, left, leftPath, best, dir, i, l; // Walking the tree while (true) { // Traversing an internal node if (node instanceof InternalNode) { dir = getDirection(key, node.critbit); // Going left & creating key if not yet there if (dir === 0) { if (!node.left) { node.left = new ExternalNode(key, value); return this; } ancestors.push(node); path.push(true); node = node.left; } // Going right & creating key if not yet there else { if (!node.right) { node.right = new ExternalNode(key, value); return this; } ancestors.push(node); path.push(false); node = node.right; } } // Reaching an external node else { // 1. Creating a new external node critbit = findCriticalBit(key, node.key); // Key is identical, we just replace the value if (critbit === -1) { node.value = value; return this; } this.size++; internal = new InternalNode(critbit); left = getDirection(key, critbit) === 0; // TODO: maybe setting opposite pointer is not necessary if (left) { internal.left = new ExternalNode(key, value); internal.right = node; } else { internal.left = node; internal.right = new ExternalNode(key, value); } // 2. Bubbling up best = -1; l = ancestors.length; for (i = l - 1; i >= 0; i--) { ancestor = ancestors[i]; if (ancestor.critbit > critbit) continue; best = i; break; } // Do we need to attach to the root? if (best < 0) { this.root = internal; // Need to rewire parent as child? if (l > 0) { parent = ancestors[0]; if (left) internal.right = parent; else internal.left = parent; } } // Simple case without rotation else if (best === l - 1) { parent = ancestors[best]; leftPath = path[best]; if (leftPath) parent.left = internal; else parent.right = internal; } // Full rotation else { parent = ancestors[best]; leftPath = path[best]; child = ancestors[best + 1]; if (leftPath) parent.left = internal; else parent.right = internal; if (left) internal.right = child; else internal.left = child; } return this; } } }; /** * Method used to get the value attached to the given key in the tree or * undefined if not found. * * @param {string} key - Key to get. * @return {any} */ CritBitTreeMap.prototype.get = function(key) { // Walk state var node = this.root, dir; // Walking the tree while (true) { // Dead end if (node === null) return; // Traversing an internal node if (node instanceof InternalNode) { dir = getDirection(key, node.critbit); node = dir ? node.right : node.left; } // Reaching an external node else { if (node.key !== key) return; return node.value; } } }; /** * Method used to return whether the given key exists in the tree. * * @param {string} key - Key to test. * @return {boolean} */ CritBitTreeMap.prototype.has = function(key) { // Walk state var node = this.root, dir; // Walking the tree while (true) { // Dead end if (node === null) return false; // Traversing an internal node if (node instanceof InternalNode) { dir = getDirection(key, node.critbit); node = dir ? node.right : node.left; } // Reaching an external node else { return node.key === key; } } }; /** * Method used to delete the given key from the tree and return whether the * key did exist or not. * * @param {string} key - Key to delete. * @return {boolean} */ CritBitTreeMap.prototype.delete = function(key) { // Walk state var node = this.root, dir; var parent = null, grandParent = null, wentLeftForParent = false, wentLeftForGrandparent = false; // Walking the tree while (true) { // Dead end if (node === null) return false; // Traversing an internal node if (node instanceof InternalNode) { dir = getDirection(key, node.critbit); if (dir === 0) { grandParent = parent; wentLeftForGrandparent = wentLeftForParent; parent = node; wentLeftForParent = true; node = node.left; } else { grandParent = parent; wentLeftForGrandparent = wentLeftForParent; parent = node; wentLeftForParent = false; node = node.right; } } // Reaching an external node else { if (key !== node.key) return false; this.size--; // Rewiring if (parent === null) { this.root = null; } else if (grandParent === null) { if (wentLeftForParent) this.root = parent.right; else this.root = parent.left; } else { if (wentLeftForGrandparent) { if (wentLeftForParent) { grandParent.left = parent.right; } else { grandParent.left = parent.left; } } else { if (wentLeftForParent) { grandParent.right = parent.right; } else { grandParent.right = parent.left; } } } return true; } } }; /** * Method used to iterate over the tree in key order. * * @param {function} callback - Function to call for each item. * @param {object} scope - Optional scope. * @return {undefined} */ CritBitTreeMap.prototype.forEach = function(callback, scope) { scope = arguments.length > 1 ? scope : this; // Inorder traversal of the tree var current = this.root, stack = []; while (true) { if (current !== null) { stack.push(current); current = current instanceof InternalNode ? current.left : null; } else { if (stack.length > 0) { current = stack.pop(); if (current instanceof ExternalNode) callback.call(scope, current.value, current.key); current = current instanceof InternalNode ? current.right : null; } else { break; } } } }; /** * Convenience known methods. */ CritBitTreeMap.prototype.inspect = function() { return this; }; if (typeof Symbol !== 'undefined') CritBitTreeMap.prototype[Symbol.for('nodejs.util.inspect.custom')] = CritBitTreeMap.prototype.inspect; /** * Static @.from function taking an arbitrary iterable & converting it into * a CritBitTreeMap. * * @param {Iterable} iterable - Target iterable. * @return {CritBitTreeMap} */ // CritBitTreeMap.from = function(iterable) { // }; /** * Exporting. */ module.exports = CritBitTreeMap;