diff --git a/CHANGELOG.md b/CHANGELOG.md index 4ac498d..b289a43 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,11 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ## [Unreleased] +## [5.3.0] - 2023-01-30 +### Added +- `removeNode` to remove a node by its reference. +- `upperBoundKey`, `floorKey`, `lowerBoundKey`, `ceilKey` to support finding nodes by the object comparison key. + ## [5.2.0] - 2022-12-12 ### Added diff --git a/README.md b/README.md index 1bd8911..c98fe68 100644 --- a/README.md +++ b/README.md @@ -21,13 +21,16 @@ Binary Search Tree & AVL Tree (Self Balancing Tree) implementation in javascript * [min](#min) * [max](#max) * [lowerBound (floor)](#lowerbound-floor) + * [lowerBoundKey (floorKey)](#lowerboundkey-floorkey) * [upperBound (ceil)](#upperbound-ceil) + * [upperBoundKey (ceilKey)](#upperboundkey-ceilkey) * [root](#root) * [count](#count) * [traverseInOrder](#traverseinorder) * [traversePreOrder](#traversepreorder) * [traversePostOrder](#traversepostorder) * [remove](#remove) + * [removeNode](#removeNode) * [clear](#clear) * [BinarySearchTreeNode](#binarysearchtreenodet) * [AvlTreeNode](#avltreenodet) @@ -78,13 +81,19 @@ constructor also accepts an options param, where the comparison key prob name ca ###### BinarySearchTree ```js const nums = new BinarySearchTree(); -const employees = new BinarySearchTree((a, b) => a.id - b.id); +const employees = new BinarySearchTree( + (a, b) => a.id - b.id, + { key: 'id } +); ``` ###### AvlTree ```js const nums = new AvlTree(); -const employees = new AvlTree((a, b) => a.id - b.id, { key: 'id' }); +const employees = new AvlTree( + (a, b) => a.id - b.id, + { key: 'id' } +); ``` ##### TS @@ -147,7 +156,7 @@ employees.has({ id: 100 }); // false ### hasKey O(log(n)) -checks if a value exists by its key if the node's key prob is provided in the constructor. +checks if an object exists by its key if the comparison key prob is provided in the constructor. ```js employees.hasKey(50); // true @@ -170,7 +179,7 @@ employees.find({ id: 100 }); // null ### findKey O(log(n)) -finds a node by its key if the node's key prob is provided in the constructor. +finds a node by its object key if the comparison key prob is provided in the constructor. ```js employees.findKey(60).getValue(); // { id: 60 } @@ -214,6 +223,17 @@ employees.floor({ id: 60 }, false).getValue(); // { id: 50 } employees.floor({ id: 10 }); // null ``` +### lowerBoundKey (floorKey) +O(log(n)) + +finds the node with the biggest key less or equal a given key if the comparison key prob is provided in the constructor. You can eliminate equal values by passing second param as false. `.floorKey` is an alias to the same function. + +```js +employees.floorKey(60).getValue(); // { id: 60 } +employees.floorKey(60, false).getValue(); // { id: 50 } +employees.floorKey(10); // null +``` + ### upperBound (ceil) O(log(n)) @@ -231,6 +251,19 @@ employees.ceil({ id: 80 }, false).getValue(); // { id: 90 } employees.ceil({ id: 110 }); // null ``` + +### upperBoundKey (ceilKey) +O(log(n)) + +finds the node with the smallest key bigger or equal a given key if the comparison key prob is provided in the constructor. You can eliminate equal values by passing second param as false. `.ceilKey` is an alias to the same function. + +```js +employees.ceilKey(75).getValue(); // { id: 80 } +employees.ceilKey(80).getValue(); // { id: 80 } +employees.ceilKey(80, false).getValue(); // { id: 90 } +employees.ceilKey(110); // null +``` + ### root O(1) @@ -376,7 +409,7 @@ employees.traversePostOrder((node) => { ### remove O(log(n)) -removes a node from the tree by its value. AVL tree will rotate nodes properly if the tree becomes unbalanced during deletion. +removes a node from the tree by its value. The function will first find the node that corresponds to the value and then remove it. AVL tree will rotate nodes properly if the tree becomes unbalanced. ```js nums.remove(20); // true @@ -388,6 +421,19 @@ employees.remove({ id: 100 }); // false employees.count(); // 6 ``` +### removeNode +O(log(n)) + +removes a node from the tree by its reference. + +```js +const node1 = nums.find(50); +nums.removeNode(node1); // true + +const node2 = employees.findKey(50); +employees.removeNode(node2); // true +``` + ### clear O(1) diff --git a/package.json b/package.json index 9c1bace..e054fed 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@datastructures-js/binary-search-tree", - "version": "5.2.0", + "version": "5.3.0", "description": "binary search tree & avl tree (self balancing tree) implementation in javascript", "main": "index.js", "types": "index.d.ts", diff --git a/src/avlTree.d.ts b/src/avlTree.d.ts index 64b1e74..15b1070 100644 --- a/src/avlTree.d.ts +++ b/src/avlTree.d.ts @@ -16,4 +16,5 @@ export class AvlTree extends BinarySearchTree { traverseInOrder(cb: (node: AvlTreeNode) => void): void; traversePreOrder(cb: (node: AvlTreeNode) => void): void; traversePostOrder(cb: (node: AvlTreeNode) => void): void; + removeNode(node: AvlTreeNode): boolean; } diff --git a/src/avlTree.js b/src/avlTree.js index 0b40bc2..1228ba5 100644 --- a/src/avlTree.js +++ b/src/avlTree.js @@ -124,57 +124,70 @@ class AvlTree extends BinarySearchTree { } // current node is the node to remove + return this.removeNode(current); + }; - // case 1: node has no children - if (current.isLeaf()) { - if (current.isRoot()) { - this._root = null; - } else if (this._compare(val, current.getParent().getValue()) < 0) { - current.getParent().setLeft(null).updateHeight(); - } else { - current.getParent().setRight(null).updateHeight(); - } - this._count -= 1; - return true; - } + return removeRecursively(value, this._root); + } - // case 2: node has a left child and no right child - if (!current.hasRight()) { - if (current.isRoot()) { - this._root = current.getLeft(); - } else if (this._compare(val, current.getParent().getValue()) < 0) { - current.getParent().setLeft(current.getLeft()).updateHeight(); - } else { - current.getParent().setRight(current.getLeft()).updateHeight(); - } - current.getLeft().setParent(current.getParent()); - this._count -= 1; - return true; + /** + * Removes a node from the tree + * @public + * @param {AvlTreeNode} node + * @return {boolean} + */ + removeNode(node) { + if (node === null || !(node instanceof AvlTreeNode)) { + return false; + } + + // case 1: node has no children + if (node.isLeaf()) { + if (node.isRoot()) { + this._root = null; + } else if (this._compare(node.getValue(), node.getParent().getValue()) < 0) { + node.getParent().setLeft(null).updateHeight(); + } else { + node.getParent().setRight(null).updateHeight(); } + this._count -= 1; + return true; + } - // case 3: node has a right child and no left child - if (!current.hasLeft()) { - if (current.isRoot()) { - this._root = current.getRight(); - } else if (this._compare(val, current.getParent().getValue()) < 0) { - current.getParent().setLeft(current.getRight()).updateHeight(); - } else { - current.getParent().setRight(current.getRight()).updateHeight(); - } - current.getRight().setParent(current.getParent()); - this._count -= 1; - return true; + // case 2: node has a left child and no right child + if (!node.hasRight()) { + if (node.isRoot()) { + this._root = node.getLeft(); + } else if (this._compare(node.getValue(), node.getParent().getValue()) < 0) { + node.getParent().setLeft(node.getLeft()).updateHeight(); + } else { + node.getParent().setRight(node.getLeft()).updateHeight(); } + node.getLeft().setParent(node.getParent()); + this._count -= 1; + return true; + } - // case 4: node has left and right children - const minRight = this.min(current.getRight()); - const removed = removeRecursively(minRight.getValue(), minRight); - current.setValue(minRight.getValue()); - this._balanceNode(current); - return removed; - }; + // case 3: node has a right child and no left child + if (!node.hasLeft()) { + if (node.isRoot()) { + this._root = node.getRight(); + } else if (this._compare(node.getValue(), node.getParent().getValue()) < 0) { + node.getParent().setLeft(node.getRight()).updateHeight(); + } else { + node.getParent().setRight(node.getRight()).updateHeight(); + } + node.getRight().setParent(node.getParent()); + this._count -= 1; + return true; + } - return removeRecursively(value, this._root); + // case 4: node has left and right children + const minRight = this.min(node.getRight()); + const removed = this.removeNode(minRight); + node.setValue(minRight.getValue()); + this._balanceNode(node); + return removed; } } diff --git a/src/binarySearchTree.d.ts b/src/binarySearchTree.d.ts index ae1972a..ec7336d 100644 --- a/src/binarySearchTree.d.ts +++ b/src/binarySearchTree.d.ts @@ -10,12 +10,17 @@ export class BinarySearchTree { max(node?: BinarySearchTreeNode): BinarySearchTreeNode | null; min(node?: BinarySearchTreeNode): BinarySearchTreeNode | null; lowerBound(value: T, includeEqual?: boolean): BinarySearchTreeNode | null; + lowerBoundKey(key: number|string, includeEqual?: boolean): BinarySearchTreeNode | null; floor(value: T, includeEqual?: boolean): BinarySearchTreeNode | null; + floorKey(key: number|string, includeEqual?: boolean): BinarySearchTreeNode | null; upperBound(value: T, includeEqual?: boolean): BinarySearchTreeNode | null; + upperBoundKey(key: number|string, includeEqual?: boolean): BinarySearchTreeNode | null; ceil(value: T, includeEqual?: boolean): BinarySearchTreeNode | null; + ceilKey(key: number|string, includeEqual?: boolean): BinarySearchTreeNode | null; root(): BinarySearchTreeNode | null; count(): number; remove(value: T): boolean; + removeNode(node: BinarySearchTreeNode): boolean; traverseInOrder(cb: (node: BinarySearchTreeNode) => void, abortCb?: () => boolean): void; traversePreOrder(cb: (node: BinarySearchTreeNode) => void, abortCb?: () => boolean): void; traversePostOrder(cb: (node: BinarySearchTreeNode) => void, abortCb?: () => boolean): void; diff --git a/src/binarySearchTree.js b/src/binarySearchTree.js index f4fbbbd..3eb9cbd 100644 --- a/src/binarySearchTree.js +++ b/src/binarySearchTree.js @@ -117,7 +117,7 @@ class BinarySearchTree { } /** - * Finds a node by its key + * Finds a node by its object's key * @public * @param {number|string} key * @return {BinarySearchTreeNode} @@ -180,6 +180,21 @@ class BinarySearchTree { return lowerBoundRecursive(this._root); } + /** + * Returns the node with the biggest object's key less or equal a given key + * @public + * @param {number|string} key + * @param {boolean} includeEqual + * @return {BinarySearchTreeNode|null} + */ + lowerBoundKey(key, includeEqual = true) { + if (this._options.key === undefined || this._options.key === null) { + throw new Error('Missing key prop name in constructor options'); + } + + return this.lowerBound({ [this._options.key]: key }, includeEqual); + } + /** * Returns the node with the biggest value less or equal a given value * @public @@ -191,6 +206,17 @@ class BinarySearchTree { return this.lowerBound(value, includeEqual); } + /** + * Returns the node with the biggest object's key less or equal a given value + * @public + * @param {number|string} value + * @param {boolean} includeEqual + * @return {BinarySearchTreeNode|null} + */ + floorKey(key, includeEqual = true) { + return this.lowerBoundKey(key, includeEqual); + } + /** * Returns the node with the smallest value greater or equal a given value * @public @@ -218,6 +244,21 @@ class BinarySearchTree { return upperBoundRecursive(this._root); } + /** + * Returns the node with the smallest object's key greater or equal a given key + * @public + * @param {number|string} key + * @param {boolean} includeEqual + * @return {BinarySearchTreeNode|null} + */ + upperBoundKey(key, includeEqual = true) { + if (this._options.key === undefined || this._options.key === null) { + throw new Error('Missing key prop name in constructor options'); + } + + return this.upperBound({ [this._options.key]: key }, includeEqual); + } + /** * Returns the node with the smallest value greater or equal a given value * @public @@ -229,6 +270,17 @@ class BinarySearchTree { return this.upperBound(value, includeEqual); } + /** + * Returns the node with the smallest object's key greater or equal a given key + * @public + * @param {number|string} key + * @param {boolean} includeEqual + * @return {BinarySearchTreeNode|null} + */ + ceilKey(key, includeEqual = true) { + return this.upperBoundKey(key, includeEqual); + } + /** * Returns the root node * @public @@ -248,7 +300,7 @@ class BinarySearchTree { } /** - * Removes a node by its key + * Removes a node by its value * @public * @param {number|string|object} value * @return {boolean} @@ -261,55 +313,68 @@ class BinarySearchTree { if (compare < 0) return removeRecursively(val, current.getLeft()); if (compare > 0) return removeRecursively(val, current.getRight()); - // current node is the node to remove - // case 1: node has no children - if (current.isLeaf()) { - if (current.isRoot()) { - this._root = null; - } else if (this._compare(val, current.getParent().getValue()) < 0) { - current.getParent().setLeft(null); - } else { - current.getParent().setRight(null); - } - this._count -= 1; - return true; - } + return this.removeNode(current); + }; - // case 2: node has a left child and no right child - if (!current.hasRight()) { - if (current.isRoot()) { - this._root = current.getLeft(); - } else if (this._compare(val, current.getParent().getValue()) < 0) { - current.getParent().setLeft(current.getLeft()); - } else { - current.getParent().setRight(current.getLeft()); - } - current.getLeft().setParent(current.getParent()); - this._count -= 1; - return true; + return removeRecursively(value, this._root); + } + + /** + * Removes a node from the tree + * @public + * @param {BinarySearchTreeNode} node + * @return {boolean} + */ + removeNode(node) { + if (node === null || !(node instanceof BinarySearchTreeNode)) { + return false; + } + + // case 1: node has no children + if (node.isLeaf()) { + if (node.isRoot()) { + this._root = null; + } else if (this._compare(node.getValue(), node.getParent().getValue()) < 0) { + node.getParent().setLeft(null); + } else { + node.getParent().setRight(null); } + this._count -= 1; + return true; + } - // case 3: node has a right child and no left child - if (!current.hasLeft()) { - if (current.isRoot()) { - this._root = current.getRight(); - } else if (this._compare(val, current.getParent().getValue()) < 0) { - current.getParent().setLeft(current.getRight()); - } else { - current.getParent().setRight(current.getRight()); - } - current.getRight().setParent(current.getParent()); - this._count -= 1; - return true; + // case 2: node has a left child and no right child + if (!node.hasRight()) { + if (node.isRoot()) { + this._root = node.getLeft(); + } else if (this._compare(node.getValue(), node.getParent().getValue()) < 0) { + node.getParent().setLeft(node.getLeft()); + } else { + node.getParent().setRight(node.getLeft()); } + node.getLeft().setParent(node.getParent()); + this._count -= 1; + return true; + } - // case 4: node has left and right children - const minRight = this.min(current.getRight()); - current.setValue(minRight.getValue()); - return removeRecursively(minRight.getValue(), minRight); - }; + // case 3: node has a right child and no left child + if (!node.hasLeft()) { + if (node.isRoot()) { + this._root = node.getRight(); + } else if (this._compare(node.getValue(), node.getParent().getValue()) < 0) { + node.getParent().setLeft(node.getRight()); + } else { + node.getParent().setRight(node.getRight()); + } + node.getRight().setParent(node.getParent()); + this._count -= 1; + return true; + } - return removeRecursively(value, this._root); + // case 4: node has left and right children + const minRight = this.min(node.getRight()); + node.setValue(minRight.getValue()); + return this.removeNode(minRight); } /** diff --git a/test/avlTree.test.js b/test/avlTree.test.js index d2f0a72..3b370d1 100644 --- a/test/avlTree.test.js +++ b/test/avlTree.test.js @@ -551,4 +551,21 @@ describe('AvlTree tests', () => { elements.forEach((n) => tree.remove(n)); }); }); + + describe('.removeNode(node)', () => { + const testRemoveTree = new AvlTree(); + testRemoveTree + .insert(50) + .insert(80) + .insert(30) + .insert(90) + .insert(60) + .insert(40) + .insert(20); + const n80 = testRemoveTree.find(80); + testRemoveTree.removeNode(n80); + expect(testRemoveTree.root().getRight().getValue()).to.equal(90); + expect(testRemoveTree.root().getRight().getLeft().getValue()).to.equal(60); + expect(testRemoveTree.root().getRight().getRight()).to.equal(null); + }); }); diff --git a/test/binarySearchTree.test.js b/test/binarySearchTree.test.js index 1a8ea37..b9685e9 100644 --- a/test/binarySearchTree.test.js +++ b/test/binarySearchTree.test.js @@ -125,8 +125,20 @@ describe('BinarySearchTree tests', () => { }); }); + describe('.lowerBoundKey(key) / floorKey', () => { + it('gets the node with biggest key less or equal k', () => { + const lowerBst = new BinarySearchTree((a, b) => a.id - b.id, { key: 'id' }); + lowerBst.insert({ id: 20 }); + lowerBst.insert({ id: 7 }); + lowerBst.insert({ id: 15 }); + lowerBst.insert({ id: 9 }); + expect(lowerBst.lowerBoundKey(60).getValue()).to.eql({ id: 20 }); + expect(lowerBst.floorKey(20, false).getValue()).to.eql({ id: 15 }); + }); + }); + describe('.upperBound(k)', () => { - it('gets the node with smallest key bigger than k', () => { + it('gets the node with smallest key bigger than a key', () => { expect(bst.upperBound(75).getValue()).to.equal(80); expect(bst.upperBound(80).getValue()).to.equal(80); expect(bst.upperBound(80, false).getValue()).to.equal(90); @@ -147,6 +159,18 @@ describe('BinarySearchTree tests', () => { }); }); + describe('.upperBoundKey(key) / ceilKey', () => { + it('gets the node with smallest key bigger than a key', () => { + const upperBst = new BinarySearchTree((a, b) => a.id - b.id, { key: 'id' }); + upperBst.insert({ id: 20 }); + upperBst.insert({ id: 7 }); + upperBst.insert({ id: 15 }); + upperBst.insert({ id: 9 }); + expect(upperBst.upperBoundKey(15).getValue()).to.eql({ id: 15 }); + expect(upperBst.ceilKey(15, false).getValue()).to.eql({ id: 20 }); + }); + }); + describe('.traverseInOrder(cb)', () => { it('traverse the tree in-order', () => { const keys = []; @@ -258,6 +282,23 @@ describe('BinarySearchTree tests', () => { }); }); + describe('.removeNode(node)', () => { + const testRemoveTree = new BinarySearchTree(); + testRemoveTree + .insert(50) + .insert(80) + .insert(30) + .insert(90) + .insert(60) + .insert(40) + .insert(20); + const n80 = testRemoveTree.find(80); + testRemoveTree.removeNode(n80); + expect(testRemoveTree.root().getRight().getValue()).to.equal(90); + expect(testRemoveTree.root().getRight().getLeft().getValue()).to.equal(60); + expect(testRemoveTree.root().getRight().getRight()).to.equal(null); + }); + describe('.clear()', () => { bst.clear(); expect(bst.count()).to.equal(0);