Initial import with skill sheet working
This commit is contained in:
452
node_modules/@seald-io/binary-search-tree/lib/bst.js
generated
vendored
Normal file
452
node_modules/@seald-io/binary-search-tree/lib/bst.js
generated
vendored
Normal file
@ -0,0 +1,452 @@
|
||||
/**
|
||||
* 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
|
Reference in New Issue
Block a user