diff --git a/.travis.yml b/.travis.yml index 623a256..32b2ca6 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,8 +1,10 @@ language: node_js node_js: - - "6" - - "7" - "8" + - "9" + - "10" + - "11" + - "12" install: - npm install -g grunt-cli - npm install diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..ba0e126 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,11 @@ +# Changelog +All notable changes to this project will be documented in this file. + +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] + +## [3.0.0] - 2020-03-22 +### Changed +- New release diff --git a/Gruntfile.js b/Gruntfile.js index 468355e..bc33154 100644 --- a/Gruntfile.js +++ b/Gruntfile.js @@ -1,14 +1,14 @@ module.exports = (grunt) => { grunt.initConfig({ eslint: { - src: ['./*.js', './*.test.js'] + src: ['src/*.js', 'test/*.test.js'] }, mochaTest: { - files: ['./*.test.js'] + files: ['test/*.test.js'] }, mocha_istanbul: { coverage: { - src: './', + src: 'test', options: { mask: '*.test.js' } diff --git a/LICENSE b/LICENSE index 3a48586..64ed8f6 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ The MIT License (MIT) -Copyright (c) 2018 Eyas Ranjous +Copyright (c) 2020 Eyas Ranjous Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/README.md b/README.md index c5a489a..c848413 100644 --- a/README.md +++ b/README.md @@ -4,52 +4,85 @@ [![npm](https://img.shields.io/npm/v/@datastructures-js/binary-search-tree.svg)](https://www.npmjs.com/package/@datastructures-js/binary-search-tree) [![npm](https://img.shields.io/npm/dm/@datastructures-js/binary-search-tree.svg)](https://www.npmjs.com/package/@datastructures-js/binary-search-tree) [![npm](https://img.shields.io/badge/node-%3E=%206.0-blue.svg)](https://www.npmjs.com/package/@datastructures-js/binary-search-tree) -node's **key** data type: **string**, **number**. -node's **value** data type: any. +javascript implementation of Binary Search Tree. Binary Search Tree -## Usage -``` +# Table of Contents +* [Install](#install) +* [API](#api) + * [require](#require) + * [import](#import) + * [Creating a Tree](#create-a-tree) + * [.insert(key, value)](#insertkey-value) + * [.has(key)](#haskey) + * [.find(key)](#findkey) + * [.min()](#min) + * [.max()](#max) + * [.root()](#root) + * [.count()](#count) + * [.traverseInOrder(cb)](#traverseinordercb) + * [.traversePreOrder(cb)](#traversepreordercb) + * [.traversePostOrder(cb)](#traversepostordercb) + * [.remove(key)](#removekey) + * [.clear()](#clear) + * [Build](#build) + * [License](#license) + +## install +```sh npm install --save @datastructures-js/binary-search-tree ``` -then +## API +### require ```js -const binarySearchTree = require('@datastructures-js/binary-search-tree'); -const bst = binarySearchTree(); +const { BinarySearchTree } = require('@datastructures-js/binary-search-tree'); ``` -## API - -### .node(key, value, parent, left, right) -creates a bst node with the following api. +### import +```js +import { BinarySearchTree } from '@datastructures-js/binary-search-tree'; +``` -* .setKey(key) -* .getKey() -* .setValue(value) -* .getValue() -* .setParent(node) -* .getParent() -* .setLeft(node) -* .getLeft() -* .setRight(node) -* .getRight() +### Create a Tree ```js -const n = bst.node(1, 'test'); -console.log(n.getKey()); // 1 -console.log(n.getValue()); // test -console.log(n.getParent()); // null -console.log(n.getLeft()); // null -console.log(n.getRight()); // null +const bst = new BinarySearchTree(); ``` ### .insert(key, value) -inserts a node with key/value into the tree. -```javascript +inserts a node with key/value into the tree. Inserting an node with existing key, would update the existing node's value with the new inserted one. + + + + + + + + + + + + +
runtimeparamsreturn
O(log(n)) + key: {number} or {string} +

+ value: {object} +
+ {BinarySearchTreeNode} the inserted node +

+ .getKey() {number|string} returns the node's key that is used to compare with other nodes.
+ .setValue(value) change the value that is associated with a node.
+ .getValue() {object} returns the value that is associated with a node.
+ .getLeft() {BinarySearchTreeNode} returns node's left child node.
+ .getRight() {BinarySearchTreeNode} returns node's right child node.
+ .getParent() {BinarySearchTreeNode} returns node's parent node. +
+ +```js bst.insert(50, 'v1'); bst.insert(80, 'v2'); bst.insert(30, 'v3'); @@ -59,132 +92,276 @@ bst.insert(40, 'v6'); bst.insert(20, 'v7'); ``` -### .root() +### .has(key) +checks if a node exists by its key. + + + + + + + + + + + + +
runtimeparamsreturn
O(log(n)) + key: {number} or {string} + + {boolean} +
-gets the root node -```javascript -console.log(bst.root().getKey()); // 50 +```js +bst.has(50); // true +bst.has(100); // false ``` -### .min() +### .find(key) +finds a node in the tree by its key. + + + + + + + + + + + + +
runtimeparamsreturn
O(log(n)) + key: {number} or {string} + + {BinarySearchTreeNode} +
-finds the min key node (most left). -```javascript -console.log(bst.min().getKey()); // 20 -``` - -### .max() +```js +const n60 = bst.find(60); +console.log(n60.getKey()); // 60 +console.log(n60.getValue()); // v5 -finds the max key node (most right). -```javascript -console.log(bst.max().getKey()); // 90 +console.log(bst.find(100)); // null ``` -### .count() +### .min() +finds the node with min key in the tree. + + + + + + + + + + +
runtimereturn
O(log(n)) + {BinarySearchTreeNode} +
-gets nodes count. -```javascript -console.log(bst.count()); // 7 +```js +const min = bst.min(); +console.log(min.getKey()); // 20 +console.log(min.getValue()); // v7 ``` -### .search(key) +### .max() +finds the node with max key in the tree. + + + + + + + + + + +
runtimereturn
O(log(n)) + {BinarySearchTreeNode} +
-finds a node by key or returns null if not found. -```javascript -const n = bst.search(30); -console.log(n.getKey()); // 30 -console.log(n.getRight().getKey()); // 40 -console.log(n.getLeft().getKey()); // 20 +```js +const max = bst.max(); +console.log(max.getKey()); // 90 +console.log(max.getValue()); // v4 ``` +### .root() +returns the root node of the tree. + + + + + + + + + + +
runtimereturn
O(1) + {BinarySearchTreeNode} +
-### .traverseInOrder(cb) ```js -// in-order traverse (left-parent-right) -bst.traverseInOrder(node => console.log(node.getKey())); - -// 20 -// 30 -// 40 -// 50 -// 60 -// 80 -// 90 +const root = bst.root(); +console.log(root.getKey()); // 50 +console.log(root.getValue()); // v1 ``` -### .traversePreOrder(cb) +### .count() +returns the count of nodes in the tree. + + + + + + + + + + +
runtimereturn
O(1) + {number} +
```js -// pre-order traverse (parent-left-right) -bst.traversePreOrder(node => console.log(node.getKey())); - -// 50 -// 30 -// 20 -// 40 -// 80 -// 60 -// 90 +console.log(bst.count()); // 7 ``` -### .traversePostOrder(cb) +### .traverseInOrder(cb) +traverses the tree in order (left-node-right). + + + + + + + + + + +
runtimeparam
O(n) + cb: {function} +
```js -// post-order traverse (left-right-parent) -bst.traverse(node => console.log(node.getKey())); - -// 20 -// 40 -// 30 -// 60 -// 90 -// 80 -// 50 +bst.traverseInOrder((node) => console.log(node.getKey())); + +/* +20 +30 +40 +50 +60 +80 +90 +*/ ``` -### .traverse(cb, order) - -traverse the tree in the defined order and apply a callback on each node. - -order values: `inOrder`, `preOrder` OR `postOrder`. default is `inOrder` +### .traversePreOrder(cb) +traverses the tree pre order (node-left-right). + + + + + + + + + + +
runtimeparam
O(n) + cb: {function} +
```js -bst.traverse(node => console.log(node.getKey())); // in-order - -// 20 -// 30 -// 40 -// 50 -// 60 -// 80 -// 90 +bst.traversePreOrder((node) => console.log(node.getKey())); + +/* +50 +30 +20 +40 +80 +60 +90 +*/ +``` -bst.traverse(node => console.log(node.getKey()), 'preOrder'); +### .traversePostOrder(cb) +traverses the tree post order (left-right-node). + + + + + + + + + + +
runtimeparam
O(n) + cb: {function} +
-// 50 -// 30 -// 20 -// 40 -// 80 -// 60 -// 90 +```js +bst.traversePostOrder((node) => console.log(node.getKey())); + +/* +20 +40 +30 +60 +90 +80 +50 +*/ ``` - ### .remove(key) +removes a node from the tree by its key. + + + + + + + + + + + + +
runtimeparamsreturn
O(log(n)) + key: {number} or {string} + + {boolean} +
-removes a node by its key (if exists) from the tree. -```javascript -console.log(bst.search(30).getKey()); // 30 -bst.remove(30); -console.log(bst.search(30)); // null +```js +bst.remove(20); // true +bst.remove(100); // false +console.log(bst.count()); // 6 ``` ### .clear() - clears the tree. -```javascript + + + + + + + + +
runtime
O(1)
+ +```js bst.clear(); console.log(bst.count()); // 0 +console.log(bst.root()); // null ``` ## Build diff --git a/index.js b/index.js index 102786b..b5c1762 100644 --- a/index.js +++ b/index.js @@ -1,334 +1,3 @@ -/** - * datastructures-js/binary-search-tree - * @copyright 2018 Eyas Ranjous - * @license MIT - */ +const BinarySearchTree = require('./src/binarySearchTree'); -/** - * binary tree node - * @function - */ -const node = (k, v, p, l, r) => { - let key = k; - let value = v; - let parent = p || null; - let left = l || null; - let right = r || null; - - /** - * @returns {string|number} - */ - const getKey = () => key; - - /** - * @param {string|number} - */ - const setKey = (ky) => { - key = ky; - }; - - /** - * @param {object} value - */ - const setValue = (val) => { - value = val; - }; - - /** - * @returns {object} - */ - const getValue = () => value; - - /** - * @param {object} pr - */ - const setParent = (pr) => { - parent = pr; - }; - - /** - * @returns {object} node - */ - const getParent = () => parent; - - /** - * @param {BinaryNode} node - */ - const setLeft = (lf) => { - left = lf; - }; - - /** - * @returns {object} node - */ - const getLeft = () => left; - - /** - * @param {object} rg - */ - const setRight = (rg) => { - right = rg; - }; - - /** - * @returns {BinaryNode} - */ - const getRight = () => right; - - // binary tree node api - return { - setKey, - getKey, - setValue, - getValue, - setParent, - getParent, - setRight, - getRight, - setLeft, - getLeft - }; -}; - -/** - * binary search tree - * @function - */ -const binarySearchTree = () => { - let rootNode = null; - let nodesCount = 0; - - /** - * @returns {object} node - */ - const root = () => rootNode; - - /** - * @returns {number} - */ - const count = () => nodesCount; - - /** - * gets max value node in the tree - * @returns {object} node - */ - const max = (startingNode) => { - let currentNode = startingNode || rootNode; - while (currentNode !== null && currentNode.getRight() !== null) { - currentNode = currentNode.getRight(); - } - return currentNode; - }; - - /** - * gets min value node in the tree - * @returns {object} node - */ - const min = (startingNode) => { - let currentNode = startingNode || rootNode; - while (currentNode !== null && currentNode.getLeft() !== null) { - currentNode = currentNode.getLeft(); - } - return currentNode; - }; - - /** - * finds a node in the tree by a given value - * @param {(string|number)} value - * @returns {object} node - */ - const search = (key) => { - let currentNode = rootNode; - while (currentNode !== null) { - if (key > currentNode.getKey()) { - currentNode = currentNode.getRight(); - } else if (key < currentNode.getKey()) { - currentNode = currentNode.getLeft(); - } else { - return currentNode; - } - } - return null; - }; - - /** - * inserts a node by a given (key, value) into the tree - * @param {(string|number)} key - * @param {object} value - */ - const insert = (key, value) => { - const insertFn = (currentNode) => { - if (currentNode === null) { - rootNode = node(key, value); - nodesCount += 1; - } else if (key < currentNode.getKey()) { - if (currentNode.getLeft() === null) { - currentNode.setLeft(node(key, value, currentNode)); - nodesCount += 1; - } else { - insertFn(currentNode.getLeft()); - } - } else if (key > currentNode.getKey()) { - if (currentNode.getRight() === null) { - currentNode.setRight(node(key, value, currentNode)); - nodesCount += 1; - } else { - insertFn(currentNode.getRight()); - } - } - }; - insertFn(rootNode); - }; - - /** - * removes a node by a given value from the tree - * @param {(string|number)} key - */ - const remove = (key) => { - const removeFn = (k, currentNode) => { - if (currentNode !== null) { - const left = currentNode.getLeft(); - const right = currentNode.getRight(); - if (k > currentNode.getKey()) { - removeFn(k, right); - } else if (k < currentNode.getKey()) { - removeFn(k, left); - } else { - const parent = currentNode.getParent(); - if (right === null && left === null) { - // remove a node with no children - if (parent === null) { - rootNode = null; - } else if (currentNode.getKey() >= parent.getKey()) { - parent.setRight(null); - } else { - parent.setLeft(null); - } - nodesCount -= 1; - } else if (right === null) { - // remove a node with a left child - if (parent === null) { - rootNode = left; - } else if (currentNode.getKey() > parent.getKey()) { - parent.setRight(left); - } else { - parent.setLeft(left); - } - left.setParent(parent); - nodesCount -= 1; - } else if (left === null) { - // remove a node with a right child - if (parent === null) { - rootNode = right; - } else if (currentNode.getKey() > parent.getKey()) { - parent.setRight(right); - } else { - parent.setLeft(right); - } - right.setParent(parent); - nodesCount -= 1; - } else { - // remove a node with two children - const minRight = min(right); - currentNode.setKey(minRight.getKey()); - removeFn(minRight.getKey(), minRight); - } - } - } - }; - removeFn(key, rootNode); - }; - - /** - * traverse the binary tree in-order (left-parent-right) - * @param {function} cb - called with each node value - */ - const traverseInOrder = (cb) => { - const traverseInOrderFn = (currentNode) => { - if (currentNode !== null) { - traverseInOrderFn(currentNode.getLeft()); - cb(currentNode); - traverseInOrderFn(currentNode.getRight()); - } - }; - traverseInOrderFn(rootNode); - }; - - /** - * traverse the binary tree pre-order (parent-left-right) - * @param {function} cb - called with each node value - */ - const traversePreOrder = (cb) => { - const traversePreOrderFn = (currentNode) => { - if (currentNode !== null) { - cb(currentNode); - traversePreOrderFn(currentNode.getLeft()); - traversePreOrderFn(currentNode.getRight()); - } - }; - traversePreOrderFn(rootNode); - }; - - /** - * traverse the binary tree post-order (left-right-parent) - * @param {function} cb - called with each node value - */ - const traversePostOrder = (cb) => { - const traversePostOrderFn = (currentNode) => { - if (currentNode !== null) { - traversePostOrderFn(currentNode.getLeft()); - traversePostOrderFn(currentNode.getRight()); - cb(currentNode); - } - }; - traversePostOrderFn(rootNode); - }; - - /** - * traverse the binary tree - * @param {function} cb - called with each node value - * @param {string} type - 'inOrder' | 'preOrder' | 'postOrder' - */ - const traverse = (cb, type) => { - switch (type) { - case 'inOrder': - traverseInOrder(cb); - break; - case 'preOrder': - traversePreOrder(cb); - break; - case 'postOrder': - traversePostOrder(cb); - break; - default: - traverseInOrder(cb); - } - }; - - /** - * clears the tree - */ - const clear = () => { - rootNode = null; - nodesCount = 0; - }; - - // binary tree api - return { - node, - root, - count, - clear, - max, - min, - search, - insert, - remove, - traverseInOrder, - traversePreOrder, - traversePostOrder, - traverse - }; -}; - -module.exports = binarySearchTree; +module.exports = { BinarySearchTree }; diff --git a/index.test.js b/index.test.js deleted file mode 100644 index 5b6caa2..0000000 --- a/index.test.js +++ /dev/null @@ -1,133 +0,0 @@ -const { expect } = require('chai'); -const binarySearchTree = require('./index'); - -describe('binarySearchTree tests', () => { - const bst = binarySearchTree(); - - describe('.insert(key, value)', () => - it('should insert nodes to the tree', () => { - bst.insert(50, 'n1'); - bst.insert(80, 'n2'); - bst.insert(30, 'n3'); - bst.insert(90, 'n4'); - bst.insert(60, 'n5'); - bst.insert(40, 'n6'); - bst.insert(20, 'n7'); - bst.insert(20, 'n8'); // should not be inserted. - expect(bst.count()).to.be.equal(7); - expect(bst.root().getKey()).to.equal(50); - expect(bst.root().getValue()).to.equal('n1'); - expect(bst.root().getRight().getKey()).to.equal(80); - expect(bst.root().getRight().getValue()).to.equal('n2'); - expect(bst.root().getLeft().getKey()).to.equal(30); - expect(bst.root().getLeft().getValue()).to.equal('n3'); - })); - - describe('.min()', () => - it('should get the node with min value', () => - expect(bst.min().getKey(20)))); - - describe('.max()', () => - it('should get the node with max value', () => - expect(bst.max().getKey(90)))); - - describe('.root()', () => - it('should get the root node', () => - expect(bst.root().getKey(50)))); - - describe('.search(key)', () => - it('should search a node by its key in the tree', () => { - expect(bst.search(40).getKey()).to.equal(40); - expect(bst.search(100)).to.equal(null); - })); - - describe('.traverse(cb, type)', () => { - it('should traverse the tree in order', () => { - const keys = []; - bst.traverse(node => keys.push(node.getKey()), 'inOrder'); - expect(keys).to.deep.equal([20, 30, 40, 50, 60, 80, 90]); - }); - - it('should traverse the tree pre order', () => { - const keys = []; - bst.traverse(node => keys.push(node.getKey()), 'preOrder'); - expect(keys).to.deep.equal([50, 30, 20, 40, 80, 60, 90]); - }); - - it('should traverse the tree post order', () => { - const keys = []; - bst.traverse(node => keys.push(node.getKey()), 'postOrder'); - expect(keys).to.deep.equal([20, 40, 30, 60, 90, 80, 50]); - }); - - it('should traverse the tree in order by default', () => { - const keys = []; - bst.traverse(node => keys.push(node.getKey())); - expect(keys).to.deep.equal([20, 30, 40, 50, 60, 80, 90]); - }); - }); - - describe('.remove(key)', () => { - it('should remove a leaf node', () => { - bst.remove(20); - expect(bst.search(20)).to.equal(null); - expect(bst.search(30).getLeft()).to.equal(null); - expect(bst.count()).to.equal(6); - }); - - it('should remove a node with a right child only', () => { - bst.remove(30); - expect(bst.search(30)).to.equal(null); - expect(bst.root().getLeft().getKey()).to.equal(40); - expect(bst.count()).to.equal(5); - }); - - it('should remove a node with a left child only', () => { - bst.insert(30); - bst.remove(40); - expect(bst.search(40)).to.equal(null); - expect(bst.root().getLeft().getKey()).to.equal(30); - expect(bst.count()).to.equal(5); - }); - - it('should remove a node with two children', () => { - bst.remove(80); - expect(bst.search(80)).to.equal(null); - expect(bst.root().getRight().getKey()).to.equal(90); - expect(bst.search(90).getRight()).to.equal(null); - expect(bst.search(90).getLeft().getKey()).to.equal(60); - expect(bst.count()).to.equal(4); - }); - - it('should remove root node with right child', () => { - bst.insert(100); - bst.remove(60); - bst.remove(90); - bst.remove(30); - bst.remove(50); - expect(bst.root().getKey()).to.equal(100); - }); - - it('should remove root node with left child', () => { - bst.insert(20); - bst.insert(30); - bst.insert(25); - bst.remove(30); - bst.remove(25); - bst.remove(100); - expect(bst.root().getKey()).to.equal(20); - }); - - it('should remove root node', () => { - bst.remove(20); - expect(bst.root()).to.equal(null); - }); - }); - - describe('.clear()', () => { - bst.clear(); - expect(bst.count()).to.equal(0); - expect(bst.root()).to.equal(null); - expect(bst.remove(10)).to.equal(undefined); - }); -}); diff --git a/package.json b/package.json index 824515b..5ab9938 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@datastructures-js/binary-search-tree", - "version": "2.0.1", + "version": "3.0.0", "description": "binary search tree implementation in javascript", "main": "index.js", "scripts": { @@ -24,15 +24,15 @@ }, "homepage": "https://github.com/datastructures-js/binary-search-tree#readme", "devDependencies": { - "chai": "^4.1.2", - "eslint": "^4.19.1", - "eslint-config-airbnb-base": "^12.1.0", - "eslint-plugin-import": "^2.12.0", - "grunt-eslint": "^20.2.0", - "grunt": "^1.0.1", + "chai": "^4.2.0", + "eslint": "^6.7.2", + "eslint-config-airbnb-base": "^14.0.0", + "eslint-plugin-import": "^2.19.1", + "grunt": "^1.0.4", + "grunt-eslint": "^22.0.0", "grunt-mocha-istanbul": "^5.0.2", "grunt-mocha-test": "^0.13.3", "istanbul": "^0.4.5", - "mocha": "^5.0.0" + "mocha": "^6.2.2" } } diff --git a/src/binarySearchTree.js b/src/binarySearchTree.js new file mode 100644 index 0000000..045d3b5 --- /dev/null +++ b/src/binarySearchTree.js @@ -0,0 +1,257 @@ +/** + * datastructures-js/binary-search-tree + * @copyright 2020 Eyas Ranjous + * @license MIT + */ + +const BinarySearchTreeNode = require('./binarySearchTreeNode'); + +/** + * @class BinarySearchTree + */ +class BinarySearchTree { + constructor() { + this.rootNode = null; + this.nodesCount = 0; + } + + /** + * @public + * inserts a node with a key/value into the tree + * @param {number|string} key + * @param {object} vaue + * @return {BinarySearchTreeNode} + */ + insert(key, value, node = this.rootNode) { + const newNode = new BinarySearchTreeNode(key, value); + + if (node === null) { + this.rootNode = newNode; + this.nodesCount += 1; + return newNode; + } + + if (key < node.getKey() && node.getLeft() === null) { + node.setLeft(newNode); + newNode.setParent(node); + this.nodesCount += 1; + return newNode; + } + + if (key > node.getKey() && node.getRight() === null) { + node.setRight(newNode); + newNode.setParent(node); + this.nodesCount += 1; + return newNode; + } + + if (key === node.getKey()) { + node.setValue(value); + return newNode; + } + + if (key < node.getKey()) { + return this.insert(key, value, node.getLeft()); + } + + return this.insert(key, value, node.getRight()); + } + + /** + * @public + * check if a value exists in the tree by its key + * @param {number|string} key + * @return {boolean} + */ + has(key, node = this.rootNode) { + if (node === null) return false; + + if (key === node.getKey()) return true; + + if (key < node.getKey()) return this.has(key, node.getLeft()); + + return this.has(key, node.getRight()); + } + + /** + * @public + * finds the key's node in the tree + * @param {number|string} key + * @return {BinarySearchTreeNode} + */ + find(key, node = this.rootNode) { + if (node === null) return null; + + if (key === node.getKey()) return node; + + if (key < node.getKey()) return this.find(key, node.getLeft()); + + return this.find(key, node.getRight()); + } + + /** + * @public + * finds the node with max key (most right) in the tree + * @return {BinarySearchTreeNode} + */ + max(node = this.rootNode) { + if (node === null) return null; + + if (node.getRight() === null) return node; + + return this.max(node.getRight()); + } + + /** + * @public + * finds the node with min key (most left) in the tree + * @return {BinarySearchTreeNode} + */ + min(node = this.rootNode) { + if (node === null) return null; + + if (node.getLeft() === null) return node; + + return this.min(node.getLeft()); + } + + /** + * @public + * gets the tree root node + * @return {BinarySearchTreeNode} + */ + root() { + return this.rootNode; + } + + /** + * @public + * gets nodes count in the tree + * @return {number} + */ + count() { + return this.nodesCount; + } + + /** + * @public + * remove a node by its key + * @param {number|string} key + * @return {boolean} + */ + remove(key, node = this.rootNode) { + if (node === null) return false; + + if (key < node.getKey()) { + return this.remove(key, node.getLeft()); + } + + if (key > node.getKey()) { + return this.remove(key, node.getRight()); + } + + if (node.getLeft() === null && node.getRight() === null) { + if (node.getParent() === null) { + this.rootNode = null; + } else if (node.getKey() < node.getParent().getKey()) { + node.getParent().setLeft(null); + } else { + node.getParent().setRight(null); + } + this.nodesCount -= 1; + return true; + } + + if (node.getRight() === null) { + if (node.getParent() === null) { + this.rootNode = node.getLeft(); + } else if (node.getKey() < node.getParent().getKey()) { + node.getParent().setLeft(node.getLeft()); + } else { + node.getParent().setRight(node.getLeft()); + } + node.getLeft().setParent(node.getParent()); + this.nodesCount -= 1; + return true; + } + + if (node.getLeft() === null) { + if (node.getParent() === null) { + this.rootNode = node.getRight(); + } else if (node.getKey() < node.getParent().getKey()) { + node.getParent().setLeft(node.getRight()); + } else { + node.getParent().setRight(node.getRight()); + } + node.getRight().setParent(node.getParent()); + this.nodesCount -= 1; + return true; + } + + const minRight = this.min(node.getRight()); + node.setKey(minRight.getKey()); + return this.remove(minRight.getKey(), minRight); + } + + /** + * @public + * traverse the tree in-order (left-node-right) + * @param {function} cb + */ + traverseInOrder(cb, node = this.rootNode) { + if (typeof cb !== 'function') { + throw new Error('.traverseInOrder(cb) expects a callback'); + } + + if (node === null) return; + + this.traverseInOrder(cb, node.getLeft()); + cb(node); + this.traverseInOrder(cb, node.getRight()); + } + + /** + * @public + * traverse the tree pre-order (node-left-right) + * @param {function} cb + */ + traversePreOrder(cb, node = this.rootNode) { + if (typeof cb !== 'function') { + throw new Error('.traversePreOrder(cb) expects a callback'); + } + + if (node === null) return; + + cb(node); + this.traversePreOrder(cb, node.getLeft()); + this.traversePreOrder(cb, node.getRight()); + } + + /** + * @public + * traverse the tree post-order (left-right-node) + * @param {function} cb + */ + traversePostOrder(cb, node = this.rootNode) { + if (typeof cb !== 'function') { + throw new Error('.traversePostOrder(cb) expects a callback'); + } + + if (node === null) return; + + this.traversePostOrder(cb, node.getLeft()); + this.traversePostOrder(cb, node.getRight()); + cb(node); + } + + /** + * @public + * clears the tree + */ + clear() { + this.rootNode = null; + this.nodesCount = 0; + } +} + +module.exports = BinarySearchTree; diff --git a/src/binarySearchTreeNode.js b/src/binarySearchTreeNode.js new file mode 100644 index 0000000..ceca90f --- /dev/null +++ b/src/binarySearchTreeNode.js @@ -0,0 +1,140 @@ +/** + * datastructures-js/binary-search-tree + * @copyright 2020 Eyas Ranjous + * @license MIT + */ + +/** + * @class BinarySearchTreeNode + */ +class BinarySearchTreeNode { + constructor(key, value) { + this.key = key; + this.value = value; + this.left = null; + this.right = null; + this.parent = null; + this.height = 1; + } + + /** + * @internal + * @param {number|string} + */ + setKey(key) { + this.key = key; + } + + /** + * @public + * @return {number|string} + */ + getKey() { + return this.key; + } + + /** + * @public + * @param {object} + */ + setValue(value) { + this.value = value; + } + + /** + * @public + * @return {object} + */ + getValue() { + return this.value; + } + + /** + * @internal + * @param {BinarySearchTreeNode} + */ + setLeft(left) { + this.left = left; + } + + /** + * @public + * @return {BinarySearchTreeNode} + */ + getLeft() { + return this.left; + } + + /** + * @internal + * @param {BinarySearchTreeNode} + */ + setRight(right) { + this.right = right; + } + + /** + * @public + * @return {BinarySearchTreeNode} + */ + getRight() { + return this.right; + } + + /** + * @internal + * @param {BinarySearchTreeNode} + */ + setParent(parent) { + this.parent = parent; + } + + /** + * @public + * @return {BinarySearchTreeNode} + */ + getParent() { + return this.parent; + } + + /** + * @public + * @return {number} + */ + getLeftHeight() { + return this.left !== null ? this.left.getHeight() : 0; + } + + /** + * @public + * @return {number} + */ + getRightHeight() { + return this.right !== null ? this.right.getHeight() : 0; + } + + /** + * @public + */ + updateHeight() { + this.height = Math.max(this.getLeftHeight(), this.getRightHeight()) + 1; + } + + /** + * @public + * @return {number} + */ + getHeight() { + return this.height; + } + + /** + * @public + * @return {number} + */ + getBalance() { + return this.getLeftHeight() - this.getRightHeight(); + } +} + +module.exports = BinarySearchTreeNode; diff --git a/test/binarySearchTree.test.js b/test/binarySearchTree.test.js new file mode 100644 index 0000000..ae5c42d --- /dev/null +++ b/test/binarySearchTree.test.js @@ -0,0 +1,167 @@ +const { expect } = require('chai'); +const BinarySearchTreeNode = require('../src/binarySearchTreeNode'); +const BinarySearchTree = require('../src/binarySearchTree'); + +describe('binarySearchTree tests', () => { + const bst = new BinarySearchTree(); + + describe('.insert(key, value)', () => { + it('should insert nodes to the tree', () => { + bst.insert(50, 'n1'); + bst.insert(80, 'n2'); + bst.insert(30, 'n3'); + bst.insert(90, 'n4'); + bst.insert(60, 'n5'); + bst.insert(40, 'n6'); + bst.insert(20, 'n20'); + bst.insert(20, 'n7'); // updates value of existing node + }); + }); + + describe('.root()', () => { + it('should get the root node', () => { + expect(bst.root().getKey()).to.equal(50); + expect(bst.root().getValue()).to.equal('n1'); + expect(bst.root().getRight().getKey()).to.equal(80); + expect(bst.root().getRight().getValue()).to.equal('n2'); + expect(bst.root().getLeft().getKey()).to.equal(30); + expect(bst.root().getLeft().getValue()).to.equal('n3'); + }); + }); + + describe('.count()', () => { + it('get the count of nodes in the tree', () => { + expect(bst.count()).to.be.equal(7); + }); + }); + + describe('.has(key)', () => { + it('checks if a node exists by key', () => { + expect(bst.has(50)).to.equal(true); + expect(bst.has(80)).to.equal(true); + expect(bst.has(30)).to.equal(true); + expect(bst.has(90)).to.equal(true); + expect(bst.has(50)).to.equal(true); + expect(bst.has(40)).to.equal(true); + expect(bst.has(20)).to.equal(true); + expect(bst.has(100)).to.equal(false); + }); + }); + + describe('.find(key)', () => { + it('should search a node by its key in the tree', () => { + expect(bst.find(50)).to.be.instanceof(BinarySearchTreeNode); + expect(bst.find(80)).to.be.instanceof(BinarySearchTreeNode); + expect(bst.find(30)).to.be.instanceof(BinarySearchTreeNode); + expect(bst.find(90)).to.be.instanceof(BinarySearchTreeNode); + expect(bst.find(50)).to.be.instanceof(BinarySearchTreeNode); + expect(bst.find(40)).to.be.instanceof(BinarySearchTreeNode); + expect(bst.find(20)).to.be.instanceof(BinarySearchTreeNode); + expect(bst.find(100)).to.equal(null); + }); + }); + + describe('.max()', () => { + it('should get the node with max value', () => { + const max = bst.max(); + expect(max.getKey()).to.equal(90); + expect(max.getValue()).to.equal('n4'); + }); + }); + + describe('.min()', () => { + it('should get the node with min value', () => { + const min = bst.min(); + expect(min.getKey()).to.equal(20); + expect(min.getValue()).to.equal('n7'); + }); + }); + + describe('.traverseInOrder(cb)', () => { + it('traverse the tree in-order', () => { + const keys = []; + bst.traverseInOrder((node) => keys.push(node.getKey())); + expect(keys).to.deep.equal([20, 30, 40, 50, 60, 80, 90]); + }); + }); + + describe('.traversePreOrder(cb)', () => { + it('traverse the tree pre-order', () => { + const keys = []; + bst.traversePreOrder((node) => keys.push(node.getKey())); + expect(keys).to.deep.equal([50, 30, 20, 40, 80, 60, 90]); + }); + }); + + describe('.traversePostOrder(cb)', () => { + it('traverse the tree post-order', () => { + const keys = []; + bst.traversePostOrder((node) => keys.push(node.getKey())); + expect(keys).to.deep.equal([20, 40, 30, 60, 90, 80, 50]); + }); + }); + + describe('.remove(key)', () => { + it('should remove a leaf node', () => { + bst.remove(20); + expect(bst.has(20)).to.equal(false); + expect(bst.find(30).getLeft()).to.equal(null); + expect(bst.count()).to.equal(6); + }); + + it('should remove a node with a right child only', () => { + bst.remove(30); + expect(bst.has(30)).to.equal(false); + expect(bst.root().getLeft().getKey()).to.equal(40); + expect(bst.count()).to.equal(5); + }); + + it('should remove a node with a left child only', () => { + bst.insert(30); + bst.remove(40); + expect(bst.has(40)).to.equal(false); + expect(bst.root().getLeft().getKey()).to.equal(30); + expect(bst.count()).to.equal(5); + }); + + it('should remove a node with two children', () => { + bst.remove(80); + expect(bst.has(80)).to.equal(false); + expect(bst.root().getRight().getKey()).to.equal(90); + expect(bst.find(90).getRight()).to.equal(null); + expect(bst.find(90).getLeft().getKey()).to.equal(60); + expect(bst.count()).to.equal(4); + }); + + it('should remove root node with right child', () => { + bst.insert(100); + bst.remove(60); + bst.remove(90); + bst.remove(30); + bst.remove(50); + expect(bst.root().getKey()).to.equal(100); + }); + + it('should remove root node with left child', () => { + bst.insert(20); + bst.insert(30); + bst.insert(25); + bst.remove(30); + bst.remove(25); + bst.remove(100); + expect(bst.root().getKey()).to.equal(20); + }); + + it('should remove root node', () => { + bst.remove(20); + expect(bst.root()).to.equal(null); + }); + }); + + describe('.clear()', () => { + bst.clear(); + expect(bst.count()).to.equal(0); + expect(bst.root()).to.equal(null); + expect(bst.remove(10)).to.equal(false); + }); +});