/** * Simple binary search tree */ const customUtils = require('./customUtils') class BinarySearchTree { /** * Constructor * @param {Object} options Optional * @param {Boolean} options.unique Whether to enforce a 'unique' constraint on the key or not * @param {Key} options.key Initialize this BST's key with key * @param {Value} options.value Initialize this BST's data with [value] * @param {Function} options.compareKeys Initialize this BST's compareKeys */ constructor (options) { options = options || {} this.left = null this.right = null this.parent = options.parent !== undefined ? options.parent : null if (Object.prototype.hasOwnProperty.call(options, 'key')) { this.key = options.key } this.data = Object.prototype.hasOwnProperty.call(options, 'value') ? [options.value] : [] this.unique = options.unique || false this.compareKeys = options.compareKeys || customUtils.defaultCompareKeysFunction this.checkValueEquality = options.checkValueEquality || customUtils.defaultCheckValueEquality } /** * Get the descendant with max key */ getMaxKeyDescendant () { if (this.right) return this.right.getMaxKeyDescendant() else return this } /** * Get the maximum key */ getMaxKey () { return this.getMaxKeyDescendant().key } /** * Get the descendant with min key */ getMinKeyDescendant () { if (this.left) return this.left.getMinKeyDescendant() else return this } /** * Get the minimum key */ getMinKey () { return this.getMinKeyDescendant().key } /** * Check that all nodes (incl. leaves) fullfil condition given by fn * test is a function passed every (key, data) and which throws if the condition is not met */ checkAllNodesFullfillCondition (test) { if (!Object.prototype.hasOwnProperty.call(this, 'key')) return test(this.key, this.data) if (this.left) this.left.checkAllNodesFullfillCondition(test) if (this.right) this.right.checkAllNodesFullfillCondition(test) } /** * Check that the core BST properties on node ordering are verified * Throw if they aren't */ checkNodeOrdering () { if (!Object.prototype.hasOwnProperty.call(this, 'key')) return if (this.left) { this.left.checkAllNodesFullfillCondition(k => { if (this.compareKeys(k, this.key) >= 0) throw new Error(`Tree with root ${this.key} is not a binary search tree`) }) this.left.checkNodeOrdering() } if (this.right) { this.right.checkAllNodesFullfillCondition(k => { if (this.compareKeys(k, this.key) <= 0) throw new Error(`Tree with root ${this.key} is not a binary search tree`) }) this.right.checkNodeOrdering() } } /** * Check that all pointers are coherent in this tree */ checkInternalPointers () { if (this.left) { if (this.left.parent !== this) throw new Error(`Parent pointer broken for key ${this.key}`) this.left.checkInternalPointers() } if (this.right) { if (this.right.parent !== this) throw new Error(`Parent pointer broken for key ${this.key}`) this.right.checkInternalPointers() } } /** * Check that a tree is a BST as defined here (node ordering and pointer references) */ checkIsBST () { this.checkNodeOrdering() this.checkInternalPointers() if (this.parent) throw new Error("The root shouldn't have a parent") } /** * Get number of keys inserted */ getNumberOfKeys () { let res if (!Object.prototype.hasOwnProperty.call(this, 'key')) return 0 res = 1 if (this.left) res += this.left.getNumberOfKeys() if (this.right) res += this.right.getNumberOfKeys() return res } /** * Create a BST similar (i.e. same options except for key and value) to the current one * Use the same constructor (i.e. BinarySearchTree, AVLTree etc) * @param {Object} options see constructor */ createSimilar (options) { options = options || {} options.unique = this.unique options.compareKeys = this.compareKeys options.checkValueEquality = this.checkValueEquality return new this.constructor(options) } /** * Create the left child of this BST and return it */ createLeftChild (options) { const leftChild = this.createSimilar(options) leftChild.parent = this this.left = leftChild return leftChild } /** * Create the right child of this BST and return it */ createRightChild (options) { const rightChild = this.createSimilar(options) rightChild.parent = this this.right = rightChild return rightChild } /** * Insert a new element */ insert (key, value) { // Empty tree, insert as root if (!Object.prototype.hasOwnProperty.call(this, 'key')) { this.key = key this.data.push(value) return } // Same key as root if (this.compareKeys(this.key, key) === 0) { if (this.unique) { const err = new Error(`Can't insert key ${JSON.stringify(key)}, it violates the unique constraint`) err.key = key err.errorType = 'uniqueViolated' throw err } else this.data.push(value) return } if (this.compareKeys(key, this.key) < 0) { // Insert in left subtree if (this.left) this.left.insert(key, value) else this.createLeftChild({ key: key, value: value }) } else { // Insert in right subtree if (this.right) this.right.insert(key, value) else this.createRightChild({ key: key, value: value }) } } /** * Search for all data corresponding to a key */ search (key) { if (!Object.prototype.hasOwnProperty.call(this, 'key')) return [] if (this.compareKeys(this.key, key) === 0) return this.data if (this.compareKeys(key, this.key) < 0) { if (this.left) return this.left.search(key) else return [] } else { if (this.right) return this.right.search(key) else return [] } } /** * Return a function that tells whether a given key matches a lower bound */ getLowerBoundMatcher (query) { // No lower bound if (!Object.prototype.hasOwnProperty.call(query, '$gt') && !Object.prototype.hasOwnProperty.call(query, '$gte')) return () => true if (Object.prototype.hasOwnProperty.call(query, '$gt') && Object.prototype.hasOwnProperty.call(query, '$gte')) { if (this.compareKeys(query.$gte, query.$gt) === 0) return key => this.compareKeys(key, query.$gt) > 0 if (this.compareKeys(query.$gte, query.$gt) > 0) return key => this.compareKeys(key, query.$gte) >= 0 else return key => this.compareKeys(key, query.$gt) > 0 } if (Object.prototype.hasOwnProperty.call(query, '$gt')) return key => this.compareKeys(key, query.$gt) > 0 else return key => this.compareKeys(key, query.$gte) >= 0 } /** * Return a function that tells whether a given key matches an upper bound */ getUpperBoundMatcher (query) { // No lower bound if (!Object.prototype.hasOwnProperty.call(query, '$lt') && !Object.prototype.hasOwnProperty.call(query, '$lte')) return () => true if (Object.prototype.hasOwnProperty.call(query, '$lt') && Object.prototype.hasOwnProperty.call(query, '$lte')) { if (this.compareKeys(query.$lte, query.$lt) === 0) return key => this.compareKeys(key, query.$lt) < 0 if (this.compareKeys(query.$lte, query.$lt) < 0) return key => this.compareKeys(key, query.$lte) <= 0 else return key => this.compareKeys(key, query.$lt) < 0 } if (Object.prototype.hasOwnProperty.call(query, '$lt')) return key => this.compareKeys(key, query.$lt) < 0 else return key => this.compareKeys(key, query.$lte) <= 0 } /** * Get all data for a key between bounds * Return it in key order * @param {Object} query Mongo-style query where keys are $lt, $lte, $gt or $gte (other keys are not considered) * @param {Functions} lbm/ubm matching functions calculated at the first recursive step */ betweenBounds (query, lbm, ubm) { const res = [] if (!Object.prototype.hasOwnProperty.call(this, 'key')) return [] // Empty tree lbm = lbm || this.getLowerBoundMatcher(query) ubm = ubm || this.getUpperBoundMatcher(query) if (lbm(this.key) && this.left) append(res, this.left.betweenBounds(query, lbm, ubm)) if (lbm(this.key) && ubm(this.key)) append(res, this.data) if (ubm(this.key) && this.right) append(res, this.right.betweenBounds(query, lbm, ubm)) return res } /** * Delete the current node if it is a leaf * Return true if it was deleted */ deleteIfLeaf () { if (this.left || this.right) return false // The leaf is itself a root if (!this.parent) { delete this.key this.data = [] return true } if (this.parent.left === this) this.parent.left = null else this.parent.right = null return true } /** * Delete the current node if it has only one child * Return true if it was deleted */ deleteIfOnlyOneChild () { let child if (this.left && !this.right) child = this.left if (!this.left && this.right) child = this.right if (!child) return false // Root if (!this.parent) { this.key = child.key this.data = child.data this.left = null if (child.left) { this.left = child.left child.left.parent = this } this.right = null if (child.right) { this.right = child.right child.right.parent = this } return true } if (this.parent.left === this) { this.parent.left = child child.parent = this.parent } else { this.parent.right = child child.parent = this.parent } return true } /** * Delete a key or just a value * @param {Key} key * @param {Value} value Optional. If not set, the whole key is deleted. If set, only this value is deleted */ delete (key, value) { const newData = [] let replaceWith if (!Object.prototype.hasOwnProperty.call(this, 'key')) return if (this.compareKeys(key, this.key) < 0) { if (this.left) this.left.delete(key, value) return } if (this.compareKeys(key, this.key) > 0) { if (this.right) this.right.delete(key, value) return } if (!this.compareKeys(key, this.key) === 0) return // Delete only a value if (this.data.length > 1 && value !== undefined) { this.data.forEach(d => { if (!this.checkValueEquality(d, value)) newData.push(d) }) this.data = newData return } // Delete the whole node if (this.deleteIfLeaf()) return if (this.deleteIfOnlyOneChild()) return // We are in the case where the node to delete has two children if (Math.random() >= 0.5) { // Randomize replacement to avoid unbalancing the tree too much // Use the in-order predecessor replaceWith = this.left.getMaxKeyDescendant() this.key = replaceWith.key this.data = replaceWith.data if (this === replaceWith.parent) { // Special case this.left = replaceWith.left if (replaceWith.left) replaceWith.left.parent = replaceWith.parent } else { replaceWith.parent.right = replaceWith.left if (replaceWith.left) replaceWith.left.parent = replaceWith.parent } } else { // Use the in-order successor replaceWith = this.right.getMinKeyDescendant() this.key = replaceWith.key this.data = replaceWith.data if (this === replaceWith.parent) { // Special case this.right = replaceWith.right if (replaceWith.right) replaceWith.right.parent = replaceWith.parent } else { replaceWith.parent.left = replaceWith.right if (replaceWith.right) replaceWith.right.parent = replaceWith.parent } } } /** * Execute a function on every node of the tree, in key order * @param {Function} fn Signature: node. Most useful will probably be node.key and node.data */ executeOnEveryNode (fn) { if (this.left) this.left.executeOnEveryNode(fn) fn(this) if (this.right) this.right.executeOnEveryNode(fn) } /** * Pretty print a tree * @param {Boolean} printData To print the nodes' data along with the key */ prettyPrint (printData, spacing) { spacing = spacing || '' console.log(`${spacing}* ${this.key}`) if (printData) console.log(`${spacing}* ${this.data}`) if (!this.left && !this.right) return if (this.left) this.left.prettyPrint(printData, `${spacing} `) else console.log(`${spacing} *`) if (this.right) this.right.prettyPrint(printData, `${spacing} `) else console.log(`${spacing} *`) } } // ================================ // Methods used to test the tree // ================================ // ============================================ // Methods used to actually work on the tree // ============================================ // Append all elements in toAppend to array function append (array, toAppend) { for (let i = 0; i < toAppend.length; i += 1) { array.push(toAppend[i]) } } // Interface module.exports = BinarySearchTree