Skip to content

Commit

Permalink
feat: AVL Implementation of BinarySearchTree (#67)
Browse files Browse the repository at this point in the history
* refactor: exposed insertImpl and deleteImpl as protected methods

* chore: setup launch.jsonuseful for vscode Jest debugging. Just place breakpoints on your code and Ctrl+F5 away to debug :)Hints: 'test.skip' and 'test.only' are useful to run only selected tests

* feat: add AvtTree

the AvtTree is inherited from BinarySearchTree. However duplicates are not properly handled yet in Avl.

* Update index.ts

Co-authored-by: Yangshun Tay <[email protected]>
  • Loading branch information
ngtrhieu and yangshun authored Aug 7, 2020
1 parent da4ea9c commit 7b6f640
Show file tree
Hide file tree
Showing 6 changed files with 294 additions and 67 deletions.
12 changes: 12 additions & 0 deletions .vscode/launch.json
Original file line number Diff line number Diff line change
@@ -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"
}
]
}
78 changes: 78 additions & 0 deletions src/data-structures/AvlTree.ts
Original file line number Diff line number Diff line change
@@ -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<number> | null,
): BinaryTreeNode<number> {
return this._balance(super._insertImpl(value, node));
}

protected _deleteImpl = (
value: number,
node: BinaryTreeNode<number> | null,
): BinaryTreeNode<number> | null => {
node = super._deleteImpl(value, node);
return node ? this._balance(node!) : node;
};

_balance(node: BinaryTreeNode<number>): BinaryTreeNode<number> {
// Helper function to calculate the balance of the node
const calcBalance = (node: BinaryTreeNode<number>): 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<number>): BinaryTreeNode<number> {
const { left } = node;
const { right } = left!;

left!.right = node;
node.left = right;

return left!;
}

_rotateLeft(node: BinaryTreeNode<number>): BinaryTreeNode<number> {
const { right } = node;
const { left } = right!;

right!.left = node;
node.right = left;

return right!;
}
}

export default AvlTree;
121 changes: 55 additions & 66 deletions src/data-structures/BinarySearchTree.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,39 +5,9 @@ class BinarySearchTree extends BinaryTree<number> {
/**
* 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<number>) {
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);
}

/**
Expand Down Expand Up @@ -71,6 +41,25 @@ class BinarySearchTree extends BinaryTree<number> {
return searchImpl(val, this.root);
}

protected _insertImpl(
value: number,
node: BinaryTreeNode<number> | null,
): BinaryTreeNode<number> {
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<number> | null,
): BinaryTreeNode<number> | null {
Expand Down Expand Up @@ -126,48 +115,48 @@ class BinarySearchTree extends BinaryTree<number> {
* @return {BinaryTreeNode} The root node after deletion.
*/
delete(val: number): BinaryTreeNode<number> | null {
const deleteImpl = (
value: number,
node: BinaryTreeNode<number> | null,
): BinaryTreeNode<number> | 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<number> | null,
): BinaryTreeNode<number> | 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<number> = this._getMinimumNode(
right,
) as BinaryTreeNode<number>;
node.value = tempNode.value;
node.right = deleteImpl(tempNode.value, right);
if (!right) {
return left;
}

return node;
};
const tempNode: BinaryTreeNode<number> = this._getMinimumNode(
right,
) as BinaryTreeNode<number>;
node.value = tempNode.value;
node.right = this._deleteImpl(tempNode.value, right);

this.root = deleteImpl(val, this.root);
return this.root;
return node;
}
}

Expand Down
2 changes: 2 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -50,6 +51,7 @@ export {
quickSort,
topologicalSort,
// Data Structures
AvlTree,
BinarySearchTree,
BinaryTree,
BinaryTreeNode,
Expand Down
137 changes: 137 additions & 0 deletions test/data-structures/AvlTree.test.ts
Original file line number Diff line number Diff line change
@@ -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<number> | 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);
});
});
});
});
Loading

0 comments on commit 7b6f640

Please sign in to comment.