diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 0000000..05b6a9e --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,12 @@ +{ + "configurations": [ + { + "type": "node", + "request": "launch", + "name": "Jest Tests", + "program": "${workspaceRoot}/node_modules/tsdx/dist/index.js", + "args": ["test", "--runInBand"], + "internalConsoleOptions": "openOnSessionStart" + } + ] +} diff --git a/src/data-structures/AvlTree.ts b/src/data-structures/AvlTree.ts new file mode 100644 index 0000000..ebe27a3 --- /dev/null +++ b/src/data-structures/AvlTree.ts @@ -0,0 +1,78 @@ +import BinaryTreeNode from './BinaryTreeNode'; +import BinarySearchTree from './BinarySearchTree'; + +/** + * An implementation of AvlTree based on BinarySearchTrees + */ +class AvlTree extends BinarySearchTree { + protected _insertImpl( + value: number, + node: BinaryTreeNode | null, + ): BinaryTreeNode { + return this._balance(super._insertImpl(value, node)); + } + + protected _deleteImpl = ( + value: number, + node: BinaryTreeNode | null, + ): BinaryTreeNode | null => { + node = super._deleteImpl(value, node); + return node ? this._balance(node!) : node; + }; + + _balance(node: BinaryTreeNode): BinaryTreeNode { + // Helper function to calculate the balance of the node + const calcBalance = (node: BinaryTreeNode): number => + (node.left ? node.left!.height() + 1 : 0) - + (node.right ? node.right?.height() + 1 : 0); + + // Check for whether the node is out-of-balance + const balance = calcBalance(node); + + if (balance > 1) { + const leftBalance = calcBalance(node.left!); + if (leftBalance > 0) { + // LL case + node = this._rotateRight(node); + } else if (leftBalance < 0) { + // LR case + node.left = this._rotateLeft(node.left!); + node = this._rotateRight(node); + } + } else if (balance < -1) { + const rightBalance = calcBalance(node.right!); + if (rightBalance > 0) { + // RL case + node.right = this._rotateRight(node.right!); + node = this._rotateLeft(node); + } else if (rightBalance < 0) { + // RR case + node = this._rotateLeft(node); + } + } + + return node; + } + + _rotateRight(node: BinaryTreeNode): BinaryTreeNode { + const { left } = node; + const { right } = left!; + + left!.right = node; + node.left = right; + + return left!; + } + + _rotateLeft(node: BinaryTreeNode): BinaryTreeNode { + const { right } = node; + const { left } = right!; + + right!.left = node; + node.right = left; + + return right!; + } +} + +export default AvlTree; diff --git a/src/data-structures/BinarySearchTree.ts b/src/data-structures/BinarySearchTree.ts index ee19d61..7772fa5 100644 --- a/src/data-structures/BinarySearchTree.ts +++ b/src/data-structures/BinarySearchTree.ts @@ -5,39 +5,9 @@ class BinarySearchTree extends BinaryTree { /** * Recursively insert a new value in the BST. * @param {number} value The value being inserted - * @param {BinaryTreeNode} node The current node. Param is not required. - * @return {void} */ insert(val: number): void { - if (!this.root) { - this.root = new BinaryTreeNode(val); - return; - } - - function insertImpl(value: number, node: BinaryTreeNode) { - const nodeValue = node.value; - - if (value < nodeValue) { - const { left } = node; - if (!left) { - node.left = new BinaryTreeNode(value); - return; - } - - insertImpl(value, left); - return; - } - - const { right } = node; - if (!right) { - node.right = new BinaryTreeNode(value); - return; - } - - insertImpl(value, right); - } - - insertImpl(val, this.root); + this.root = this._insertImpl(val, this.root); } /** @@ -71,6 +41,25 @@ class BinarySearchTree extends BinaryTree { return searchImpl(val, this.root); } + protected _insertImpl( + value: number, + node: BinaryTreeNode | null, + ): BinaryTreeNode { + if (!node) { + return new BinaryTreeNode(value); + } + + // Normal BST insert + // NOTE: Duplicates are sent to the left + if (value <= node.value) { + node.left = this._insertImpl(value, node.left); + } else { + node.right = this._insertImpl(value, node.right); + } + + return node; + } + private _getMinimumNode( node: BinaryTreeNode | null, ): BinaryTreeNode | null { @@ -126,48 +115,48 @@ class BinarySearchTree extends BinaryTree { * @return {BinaryTreeNode} The root node after deletion. */ delete(val: number): BinaryTreeNode | null { - const deleteImpl = ( - value: number, - node: BinaryTreeNode | null, - ): BinaryTreeNode | null => { - if (!node) { - return null; - } + this.root = this._deleteImpl(val, this.root); + return this.root; + } - const nodeValue = node.value; - const { left } = node; - const { right } = node; - if (value < nodeValue) { - node.left = deleteImpl(value, left); - return node; - } else if (value > nodeValue) { - node.right = deleteImpl(value, right); - return node; - } + protected _deleteImpl( + value: number, + node: BinaryTreeNode | null, + ): BinaryTreeNode | null { + if (!node) { + return null; + } - if (!left && !right) { - return null; - } + const nodeValue = node.value; + const { left } = node; + const { right } = node; + if (value < nodeValue) { + node.left = this._deleteImpl(value, left); + return node; + } else if (value > nodeValue) { + node.right = this._deleteImpl(value, right); + return node; + } - if (!left) { - return right; - } + if (!left && !right) { + return null; + } - if (!right) { - return left; - } + if (!left) { + return right; + } - const tempNode: BinaryTreeNode = this._getMinimumNode( - right, - ) as BinaryTreeNode; - node.value = tempNode.value; - node.right = deleteImpl(tempNode.value, right); + if (!right) { + return left; + } - return node; - }; + const tempNode: BinaryTreeNode = this._getMinimumNode( + right, + ) as BinaryTreeNode; + node.value = tempNode.value; + node.right = this._deleteImpl(tempNode.value, right); - this.root = deleteImpl(val, this.root); - return this.root; + return node; } } diff --git a/src/index.ts b/src/index.ts index 5ce515a..4bcaf37 100644 --- a/src/index.ts +++ b/src/index.ts @@ -13,6 +13,7 @@ import quickSort from './algorithms/quickSort'; import topologicalSort from './algorithms/topologicalSort'; // Data Structures +import AvlTree from './data-structures/AvlTree'; import BinarySearchTree from './data-structures/BinarySearchTree'; import BinaryTree from './data-structures/BinaryTree'; import BinaryTreeNode from './data-structures/BinaryTreeNode'; @@ -50,6 +51,7 @@ export { quickSort, topologicalSort, // Data Structures + AvlTree, BinarySearchTree, BinaryTree, BinaryTreeNode, diff --git a/test/data-structures/AvlTree.test.ts b/test/data-structures/AvlTree.test.ts new file mode 100644 index 0000000..58b2e20 --- /dev/null +++ b/test/data-structures/AvlTree.test.ts @@ -0,0 +1,137 @@ +import _ from 'lodash'; +import { AvlTree, BinaryTreeNode } from '../../src'; + +import { binarySearchTreeTests } from './BinarySearchTree.test'; + +describe('AvtTree', () => { + describe('normal binary test', () => { + // Test against normal BST test cases + binarySearchTreeTests(describe, test); + }); + + describe('self-balancing', () => { + describe('insert', () => { + test('LL rotation', () => { + const tree = new AvlTree(); + tree.insert(10); + tree.insert(9); + tree.insert(8); + expect(tree.height()).toBe(1); + + tree.insert(11); + expect(tree.height()).toBe(2); + }); + + test('LR rotation', () => { + const tree = new AvlTree(); + tree.insert(10); + tree.insert(8); + tree.insert(9); + expect(tree.height()).toBe(1); + + tree.insert(11); + expect(tree.height()).toBe(2); + }); + + test('RR rotation', () => { + const tree = new AvlTree(); + tree.insert(8); + tree.insert(9); + tree.insert(10); + expect(tree.height()).toBe(1); + + tree.insert(11); + expect(tree.height()).toBe(2); + }); + + test('RL rotation', () => { + const tree = new AvlTree(); + tree.insert(8); + tree.insert(10); + tree.insert(9); + expect(tree.height()).toBe(1); + + tree.insert(11); + expect(tree.height()).toBe(2); + }); + }); + + describe('delete', () => { + test('LL rotation', () => { + const tree = new AvlTree(); + tree.insert(5); + tree.insert(3); + tree.insert(8); + tree.insert(4); + tree.insert(2); + tree.insert(10); + tree.insert(1); + expect(tree.height()).toBe(3); + + tree.delete(10); + expect(tree.height()).toBe(2); + }); + + test('LR rotation', () => { + const tree = new AvlTree(); + tree.insert(100); + tree.insert(200); + tree.insert(10); + tree.insert(1); + tree.insert(50); + tree.insert(1000); + tree.insert(40); + tree.insert(60); + expect(tree.height()).toBe(3); + + tree.delete(1000); + expect(tree.height()).toBe(2); + }); + }); + + describe('duplicate', () => { + test.skip('test for duplicates in AVL', () => { + expect(true).toBe(true); + }); + }); + + test.skip('random stress test', () => { + const assertAvl = (node: BinaryTreeNode | null) => { + if (!node) return; + + // assert BST property + if (node.left) + expect(node.value).toBeGreaterThanOrEqual(node.left.value); + if (node.right) expect(node.value).toBeLessThan(node.right.value); + + // assert balancing property + const balance = + (node.left ? node.left!.height() + 1 : 0) - + (node.right ? node.right?.height() + 1 : 0); + expect(Math.abs(balance)).toBeLessThanOrEqual(1); + + assertAvl(node.left); + assertAvl(node.right); + }; + + const tree = new AvlTree(); + + // Build a tree of 100 random numbers, assert the tree is AVL tree + _.times(100, () => { + tree.insert(Math.floor(Math.random() * 50)); + + assertAvl(tree.root); + }); + + // Delete off 50 random numbers, assert the tree is AVL the whole time + const arr = tree.inOrder(); + _.times(50, () => { + const index = Math.floor(Math.random() * arr.length); + tree.delete(arr[index]); + arr.splice(index, 1); + + assertAvl(tree.root); + }); + }); + }); +}); diff --git a/test/data-structures/BinarySearchTree.test.ts b/test/data-structures/BinarySearchTree.test.ts index cd31a3b..b60c388 100644 --- a/test/data-structures/BinarySearchTree.test.ts +++ b/test/data-structures/BinarySearchTree.test.ts @@ -1,7 +1,12 @@ import { BinarySearchTree } from '../../src'; import nullthrows from '../../src/utils/nullthrows'; -describe('BinarySearchTree', () => { +export const binarySearchTreeTests = ( + /* eslint-disable @typescript-eslint/no-explicit-any */ + describe: any, + test: any, + /* eslint-enable @typescript-eslint/no-explicit-any */ +) => { describe('insert()', () => { test('if empty tree, value becomes root', () => { const tree = new BinarySearchTree(); @@ -197,4 +202,8 @@ describe('BinarySearchTree', () => { expect(tree.inOrder()).toEqual([5, 14, 55, 70]); }); }); +}; + +describe('BinarySearchTree', () => { + binarySearchTreeTests(describe, test); });