diff --git a/DEVELOPMENT_PLAN.md b/DEVELOPMENT_PLAN.md index 50cecaf..b70fddc 100644 --- a/DEVELOPMENT_PLAN.md +++ b/DEVELOPMENT_PLAN.md @@ -18,7 +18,7 @@ This document outlines possible improvements and future developments for the `TS - **Graph**: Implement a basic graph structure with common algorithms such as BFS, DFS, and shortest path algorithms. *(Implemented)* - **PriorityQueue**: Provide a priority queue implementation for efficient task scheduling. *(Implemented)* -- **Balanced Trees**: Add self-balancing tree variants (e.g., AVL or Red-Black trees) for more efficient search and insert operations. +- **Balanced Trees**: Add self-balancing tree variants (e.g., AVL or Red-Black trees) for more efficient search and insert operations. *(Implemented)* ## Documentation diff --git a/README.md b/README.md index 6969c10..a5f20fa 100644 --- a/README.md +++ b/README.md @@ -306,6 +306,20 @@ g.addEdge(1, 3); const path = g.shortestPath(1, 3); ``` +## AVLTree + +`AVLTree` maintains balance automatically after each insertion to keep +operations fast. + +```typescript +const avl = new AVLTree(); +avl.insert(1); +avl.insert(2); +avl.insert(3); // tree rotates, keeping 2 as the root +avl.contains(3); // true +avl.toArray(); // [1, 2, 3] +``` + ## BinaryTree #### getRoot(): BinaryTreeNode diff --git a/src/AVLTree/AVLTree.ts b/src/AVLTree/AVLTree.ts new file mode 100644 index 0000000..e8681f6 --- /dev/null +++ b/src/AVLTree/AVLTree.ts @@ -0,0 +1,167 @@ +export class AVLTreeNode { + private value: T; + private left: AVLTreeNode | null = null; + private right: AVLTreeNode | null = null; + private height: number = 1; + + constructor(value: T) { + this.value = value; + } + + getValue(): T { + return this.value; + } + + setValue(val: T): void { + this.value = val; + } + + getLeft(): AVLTreeNode | null { + return this.left; + } + + setLeft(node: AVLTreeNode | null): void { + this.left = node; + } + + getRight(): AVLTreeNode | null { + return this.right; + } + + setRight(node: AVLTreeNode | null): void { + this.right = node; + } + + getHeight(): number { + return this.height; + } + + setHeight(h: number): void { + this.height = h; + } +} + +export class AVLTree { + private root: AVLTreeNode | null = null; + + getRoot(): AVLTreeNode | null { + return this.root; + } + + insert(value: T): void { + this.root = this.insertNode(this.root, value); + } + + contains(value: T): boolean { + return this.findNode(this.root, value) !== null; + } + + toArray(): T[] { + const result: T[] = []; + this.inOrder(this.root, result); + return result; + } + + private insertNode(node: AVLTreeNode | null, value: T): AVLTreeNode { + if (node === null) { + return new AVLTreeNode(value); + } + + if (value < node.getValue()) { + node.setLeft(this.insertNode(node.getLeft(), value)); + } else if (value > node.getValue()) { + node.setRight(this.insertNode(node.getRight(), value)); + } else { + return node; // no duplicates + } + + this.updateHeight(node); + return this.balance(node); + } + + private findNode(node: AVLTreeNode | null, value: T): AVLTreeNode | null { + if (node === null) { + return null; + } + if (value === node.getValue()) { + return node; + } + if (value < node.getValue()) { + return this.findNode(node.getLeft(), value); + } + return this.findNode(node.getRight(), value); + } + + private inOrder(node: AVLTreeNode | null, arr: T[]): void { + if (node === null) { + return; + } + this.inOrder(node.getLeft(), arr); + arr.push(node.getValue()); + this.inOrder(node.getRight(), arr); + } + + private heightOf(node: AVLTreeNode | null): number { + return node ? node.getHeight() : 0; + } + + private updateHeight(node: AVLTreeNode): void { + node.setHeight(Math.max(this.heightOf(node.getLeft()), this.heightOf(node.getRight())) + 1); + } + + private balanceFactor(node: AVLTreeNode): number { + return this.heightOf(node.getLeft()) - this.heightOf(node.getRight()); + } + + private balance(node: AVLTreeNode): AVLTreeNode { + const balance = this.balanceFactor(node); + + if (balance > 1) { + if (this.balanceFactor(node.getLeft()) < 0) { + node.setLeft(this.rotateLeft(node.getLeft())); + } + return this.rotateRight(node); + } + + if (balance < -1) { + if (this.balanceFactor(node.getRight()) > 0) { + node.setRight(this.rotateRight(node.getRight())); + } + return this.rotateLeft(node); + } + + return node; + } + + private rotateLeft(x: AVLTreeNode): AVLTreeNode { + const y = x.getRight(); + if (!y) { + return x; + } + const T2 = y.getLeft(); + + y.setLeft(x); + x.setRight(T2); + + this.updateHeight(x); + this.updateHeight(y); + + return y; + } + + private rotateRight(y: AVLTreeNode): AVLTreeNode { + const x = y.getLeft(); + if (!x) { + return y; + } + const T2 = x.getRight(); + + x.setRight(y); + y.setLeft(T2); + + this.updateHeight(y); + this.updateHeight(x); + + return x; + } +} diff --git a/src/index.ts b/src/index.ts index a3531c3..25a3906 100644 --- a/src/index.ts +++ b/src/index.ts @@ -13,3 +13,4 @@ export * from './PriorityQueue/PriorityQueue'; export * from './Nodes/BinaryTreeNode'; export * from './BinaryTree/BinaryTree'; export * from './Graph/Graph'; +export * from './AVLTree/AVLTree'; diff --git a/tests/AVLTree/AVLTree.spec.ts b/tests/AVLTree/AVLTree.spec.ts new file mode 100644 index 0000000..deecbb9 --- /dev/null +++ b/tests/AVLTree/AVLTree.spec.ts @@ -0,0 +1,29 @@ +import { expect } from 'chai'; +import { AVLTree } from '../../src/AVLTree/AVLTree'; + +describe('AVLTree', () => { + it('should keep tree balanced for ascending inserts', () => { + const tree = new AVLTree(); + tree.insert(1); + tree.insert(2); + tree.insert(3); + + const root = tree.getRoot(); + expect(root.getValue()).to.equal(2); + expect(root.getLeft().getValue()).to.equal(1); + expect(root.getRight().getValue()).to.equal(3); + }); + + it('should contain inserted values', () => { + const tree = new AVLTree(); + [10, 5, 15, 3, 8].forEach(v => tree.insert(v)); + expect(tree.contains(8)).to.be.true; + expect(tree.contains(100)).to.be.false; + }); + + it('toArray should return values in order', () => { + const tree = new AVLTree(); + [4, 2, 6, 1, 3, 5, 7].forEach(v => tree.insert(v)); + expect(tree.toArray()).to.deep.equal([1,2,3,4,5,6,7]); + }); +});