You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

516 lines
10 KiB

3 years ago
/* 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;