Initial import with skill sheet working

This commit is contained in:
2024-12-04 00:11:23 +01:00
commit 9050c80ab4
4488 changed files with 671048 additions and 0 deletions

View File

@ -0,0 +1,412 @@
/**
* Self-balancing binary search tree using the AVL implementation
*/
const BinarySearchTree = require('./bst')
const customUtils = require('./customUtils')
class AVLTree {
/**
* Constructor
* We can't use a direct pointer to the root node (as in the simple binary search tree)
* as the root will change during tree rotations
* @param {Boolean} options.unique Whether to enforce a 'unique' constraint on the key or not
* @param {Function} options.compareKeys Initialize this BST's compareKeys
*/
constructor (options) {
this.tree = new _AVLTree(options)
}
checkIsAVLT () { this.tree.checkIsAVLT() }
// Insert in the internal tree, update the pointer to the root if needed
insert (key, value) {
const newTree = this.tree.insert(key, value)
// If newTree is undefined, that means its structure was not modified
if (newTree) { this.tree = newTree }
}
// Delete a value
delete (key, value) {
const newTree = this.tree.delete(key, value)
// If newTree is undefined, that means its structure was not modified
if (newTree) { this.tree = newTree }
}
}
class _AVLTree extends BinarySearchTree {
/**
* Constructor of the internal AVLTree
* @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) {
super()
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
}
/**
* Check the recorded height is correct for every node
* Throws if one height doesn't match
*/
checkHeightCorrect () {
if (!Object.prototype.hasOwnProperty.call(this, 'key')) { return } // Empty tree
if (this.left && this.left.height === undefined) { throw new Error('Undefined height for node ' + this.left.key) }
if (this.right && this.right.height === undefined) { throw new Error('Undefined height for node ' + this.right.key) }
if (this.height === undefined) { throw new Error('Undefined height for node ' + this.key) }
const leftH = this.left ? this.left.height : 0
const rightH = this.right ? this.right.height : 0
if (this.height !== 1 + Math.max(leftH, rightH)) { throw new Error('Height constraint failed for node ' + this.key) }
if (this.left) { this.left.checkHeightCorrect() }
if (this.right) { this.right.checkHeightCorrect() }
}
/**
* Return the balance factor
*/
balanceFactor () {
const leftH = this.left ? this.left.height : 0
const rightH = this.right ? this.right.height : 0
return leftH - rightH
}
/**
* Check that the balance factors are all between -1 and 1
*/
checkBalanceFactors () {
if (Math.abs(this.balanceFactor()) > 1) { throw new Error('Tree is unbalanced at node ' + this.key) }
if (this.left) { this.left.checkBalanceFactors() }
if (this.right) { this.right.checkBalanceFactors() }
}
/**
* When checking if the BST conditions are met, also check that the heights are correct
* and the tree is balanced
*/
checkIsAVLT () {
super.checkIsBST()
this.checkHeightCorrect()
this.checkBalanceFactors()
}
/**
* Perform a right rotation of the tree if possible
* and return the root of the resulting tree
* The resulting tree's nodes' heights are also updated
*/
rightRotation () {
const q = this
const p = this.left
if (!p) return q // No change
const b = p.right
// Alter tree structure
if (q.parent) {
p.parent = q.parent
if (q.parent.left === q) q.parent.left = p
else q.parent.right = p
} else {
p.parent = null
}
p.right = q
q.parent = p
q.left = b
if (b) { b.parent = q }
// Update heights
const ah = p.left ? p.left.height : 0
const bh = b ? b.height : 0
const ch = q.right ? q.right.height : 0
q.height = Math.max(bh, ch) + 1
p.height = Math.max(ah, q.height) + 1
return p
}
/**
* Perform a left rotation of the tree if possible
* and return the root of the resulting tree
* The resulting tree's nodes' heights are also updated
*/
leftRotation () {
const p = this
const q = this.right
if (!q) { return this } // No change
const b = q.left
// Alter tree structure
if (p.parent) {
q.parent = p.parent
if (p.parent.left === p) p.parent.left = q
else p.parent.right = q
} else {
q.parent = null
}
q.left = p
p.parent = q
p.right = b
if (b) { b.parent = p }
// Update heights
const ah = p.left ? p.left.height : 0
const bh = b ? b.height : 0
const ch = q.right ? q.right.height : 0
p.height = Math.max(ah, bh) + 1
q.height = Math.max(ch, p.height) + 1
return q
}
/**
* Modify the tree if its right subtree is too small compared to the left
* Return the new root if any
*/
rightTooSmall () {
if (this.balanceFactor() <= 1) return this // Right is not too small, don't change
if (this.left.balanceFactor() < 0) this.left.leftRotation()
return this.rightRotation()
}
/**
* Modify the tree if its left subtree is too small compared to the right
* Return the new root if any
*/
leftTooSmall () {
if (this.balanceFactor() >= -1) { return this } // Left is not too small, don't change
if (this.right.balanceFactor() > 0) this.right.rightRotation()
return this.leftRotation()
}
/**
* Rebalance the tree along the given path. The path is given reversed (as he was calculated
* in the insert and delete functions).
* Returns the new root of the tree
* Of course, the first element of the path must be the root of the tree
*/
rebalanceAlongPath (path) {
let newRoot = this
let rotated
let i
if (!Object.prototype.hasOwnProperty.call(this, 'key')) {
delete this.height
return this
} // Empty tree
// Rebalance the tree and update all heights
for (i = path.length - 1; i >= 0; i -= 1) {
path[i].height = 1 + Math.max(path[i].left ? path[i].left.height : 0, path[i].right ? path[i].right.height : 0)
if (path[i].balanceFactor() > 1) {
rotated = path[i].rightTooSmall()
if (i === 0) newRoot = rotated
}
if (path[i].balanceFactor() < -1) {
rotated = path[i].leftTooSmall()
if (i === 0) newRoot = rotated
}
}
return newRoot
}
/**
* Insert a key, value pair in the tree while maintaining the AVL tree height constraint
* Return a pointer to the root node, which may have changed
*/
insert (key, value) {
const insertPath = []
let currentNode = this
// Empty tree, insert as root
if (!Object.prototype.hasOwnProperty.call(this, 'key')) {
this.key = key
this.data.push(value)
this.height = 1
return this
}
// Insert new leaf at the right place
while (true) {
// Same key: no change in the tree structure
if (currentNode.compareKeys(currentNode.key, key) === 0) {
if (currentNode.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 currentNode.data.push(value)
return this
}
insertPath.push(currentNode)
if (currentNode.compareKeys(key, currentNode.key) < 0) {
if (!currentNode.left) {
insertPath.push(currentNode.createLeftChild({ key: key, value: value }))
break
} else currentNode = currentNode.left
} else {
if (!currentNode.right) {
insertPath.push(currentNode.createRightChild({ key: key, value: value }))
break
} else currentNode = currentNode.right
}
}
return this.rebalanceAlongPath(insertPath)
}
/**
* Delete a key or just a value and return the new root of the tree
* @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
let currentNode = this
const deletePath = []
if (!Object.prototype.hasOwnProperty.call(this, 'key')) return this // Empty tree
// Either no match is found and the function will return from within the loop
// Or a match is found and deletePath will contain the path from the root to the node to delete after the loop
while (true) {
if (currentNode.compareKeys(key, currentNode.key) === 0) { break }
deletePath.push(currentNode)
if (currentNode.compareKeys(key, currentNode.key) < 0) {
if (currentNode.left) {
currentNode = currentNode.left
} else return this // Key not found, no modification
} else {
// currentNode.compareKeys(key, currentNode.key) is > 0
if (currentNode.right) {
currentNode = currentNode.right
} else return this // Key not found, no modification
}
}
// Delete only a value (no tree modification)
if (currentNode.data.length > 1 && value !== undefined) {
currentNode.data.forEach(function (d) {
if (!currentNode.checkValueEquality(d, value)) newData.push(d)
})
currentNode.data = newData
return this
}
// Delete a whole node
// Leaf
if (!currentNode.left && !currentNode.right) {
if (currentNode === this) { // This leaf is also the root
delete currentNode.key
currentNode.data = []
delete currentNode.height
return this
} else {
if (currentNode.parent.left === currentNode) currentNode.parent.left = null
else currentNode.parent.right = null
return this.rebalanceAlongPath(deletePath)
}
}
// Node with only one child
if (!currentNode.left || !currentNode.right) {
replaceWith = currentNode.left ? currentNode.left : currentNode.right
if (currentNode === this) { // This node is also the root
replaceWith.parent = null
return replaceWith // height of replaceWith is necessarily 1 because the tree was balanced before deletion
} else {
if (currentNode.parent.left === currentNode) {
currentNode.parent.left = replaceWith
replaceWith.parent = currentNode.parent
} else {
currentNode.parent.right = replaceWith
replaceWith.parent = currentNode.parent
}
return this.rebalanceAlongPath(deletePath)
}
}
// Node with two children
// Use the in-order predecessor (no need to randomize since we actively rebalance)
deletePath.push(currentNode)
replaceWith = currentNode.left
// Special case: the in-order predecessor is right below the node to delete
if (!replaceWith.right) {
currentNode.key = replaceWith.key
currentNode.data = replaceWith.data
currentNode.left = replaceWith.left
if (replaceWith.left) { replaceWith.left.parent = currentNode }
return this.rebalanceAlongPath(deletePath)
}
// After this loop, replaceWith is the right-most leaf in the left subtree
// and deletePath the path from the root (inclusive) to replaceWith (exclusive)
while (true) {
if (replaceWith.right) {
deletePath.push(replaceWith)
replaceWith = replaceWith.right
} else break
}
currentNode.key = replaceWith.key
currentNode.data = replaceWith.data
replaceWith.parent.right = replaceWith.left
if (replaceWith.left) replaceWith.left.parent = replaceWith.parent
return this.rebalanceAlongPath(deletePath)
}
}
/**
* Keep a pointer to the internal tree constructor for testing purposes
*/
AVLTree._AVLTree = _AVLTree;
/**
* Other functions we want to use on an AVLTree as if it were the internal _AVLTree
*/
['getNumberOfKeys', 'search', 'betweenBounds', 'prettyPrint', 'executeOnEveryNode'].forEach(function (fn) {
AVLTree.prototype[fn] = function () {
return this.tree[fn].apply(this.tree, arguments)
}
})
// Interface
module.exports = AVLTree

452
node_modules/@seald-io/binary-search-tree/lib/bst.js generated vendored Normal file
View 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

View File

@ -0,0 +1,38 @@
/**
* Return an array with the numbers from 0 to n-1, in a random order
*/
const getRandomArray = n => {
if (n === 0) return []
if (n === 1) return [0]
const res = getRandomArray(n - 1)
const next = Math.floor(Math.random() * n)
res.splice(next, 0, n - 1) // Add n-1 at a random position in the array
return res
}
module.exports.getRandomArray = getRandomArray
/*
* Default compareKeys function will work for numbers, strings and dates
*/
const defaultCompareKeysFunction = (a, b) => {
if (a < b) return -1
if (a > b) return 1
if (a === b) return 0
const err = new Error("Couldn't compare elements")
err.a = a
err.b = b
throw err
}
module.exports.defaultCompareKeysFunction = defaultCompareKeysFunction
/**
* Check whether two values are equal (used in non-unique deletion)
*/
const defaultCheckValueEquality = (a, b) => a === b
module.exports.defaultCheckValueEquality = defaultCheckValueEquality