forked from public/fvtt-cthulhu-eternal
413 lines
12 KiB
JavaScript
413 lines
12 KiB
JavaScript
/**
|
|
* 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
|