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
 |