From 080a671de127e3ee9e7d46f711c9ac4f58048de8 Mon Sep 17 00:00:00 2001 From: Revone Date: Fri, 22 Nov 2024 11:23:57 +1300 Subject: [PATCH] feat: add range search functionality to BST --- src/common/index.ts | 19 ++++ src/constants/index.ts | 4 - .../binary-tree/avl-tree-multi-map.ts | 13 +-- .../binary-tree/binary-tree.ts | 31 ++++-- src/data-structures/binary-tree/bst.ts | 90 ++++++++++++---- src/data-structures/binary-tree/rb-tree.ts | 6 +- .../binary-tree/tree-multi-map.ts | 4 +- src/index.ts | 2 +- src/types/common.ts | 2 + .../binary-tree/binary-tree.ts | 2 +- src/types/data-structures/binary-tree/bst.ts | 3 +- .../data-structures/binary-tree/rb-tree.ts | 2 +- .../data-structures/binary-tree/bst.test.ts | 20 +--- .../binary-tree/rb-tree.test.ts | 101 +++++++++++++++++- 14 files changed, 236 insertions(+), 63 deletions(-) create mode 100644 src/common/index.ts delete mode 100644 src/constants/index.ts diff --git a/src/common/index.ts b/src/common/index.ts new file mode 100644 index 0000000..dc88776 --- /dev/null +++ b/src/common/index.ts @@ -0,0 +1,19 @@ +export enum DFSOperation { + VISIT = 0, + PROCESS = 1 +} +export class Range { + constructor( + public low: K, + public high: K, + public includeLow: boolean = true, + public includeHigh: boolean = true + ) {} + + // Determine whether a key is within the range + isInRange(key: K, comparator: (a: K, b: K) => number): boolean { + const lowCheck = this.includeLow ? comparator(key, this.low) >= 0 : comparator(key, this.low) > 0; + const highCheck = this.includeHigh ? comparator(key, this.high) <= 0 : comparator(key, this.high) < 0; + return lowCheck && highCheck; + } +} diff --git a/src/constants/index.ts b/src/constants/index.ts deleted file mode 100644 index f91c437..0000000 --- a/src/constants/index.ts +++ /dev/null @@ -1,4 +0,0 @@ -export enum DFSOperation { - VISIT = 0, - PROCESS = 1 -} diff --git a/src/data-structures/binary-tree/avl-tree-multi-map.ts b/src/data-structures/binary-tree/avl-tree-multi-map.ts index 3d8afcf..9cd2c98 100644 --- a/src/data-structures/binary-tree/avl-tree-multi-map.ts +++ b/src/data-structures/binary-tree/avl-tree-multi-map.ts @@ -187,17 +187,14 @@ export class AVLTreeMultiMap< return [this.createNode(key, finalValue, count), finalValue]; } - if (this.isKey(keyNodeEntryOrRaw)) return [this.createNode(keyNodeEntryOrRaw, value, count), value]; - if (this.isRaw(keyNodeEntryOrRaw)) { - if (this._toEntryFn) { - const [key, entryValue] = this._toEntryFn(keyNodeEntryOrRaw as R); - const finalValue = value ?? entryValue; - if (this.isKey(key)) return [this.createNode(key, finalValue, count), finalValue]; - } - return [undefined, undefined]; + const [key, entryValue] = this._toEntryFn!(keyNodeEntryOrRaw); + const finalValue = value ?? entryValue; + if (this.isKey(key)) return [this.createNode(key, finalValue, count), finalValue]; } + if (this.isKey(keyNodeEntryOrRaw)) return [this.createNode(keyNodeEntryOrRaw, value, count), value]; + return [undefined, undefined]; } diff --git a/src/data-structures/binary-tree/binary-tree.ts b/src/data-structures/binary-tree/binary-tree.ts index 7f20f0a..f7462b0 100644 --- a/src/data-structures/binary-tree/binary-tree.ts +++ b/src/data-structures/binary-tree/binary-tree.ts @@ -29,7 +29,7 @@ import { IBinaryTree } from '../../interfaces'; import { isComparable, trampoline } from '../../utils'; import { Queue } from '../queue'; import { IterableEntryBase } from '../base'; -import { DFSOperation } from '../../constants'; +import { DFSOperation, Range } from '../../common'; /** * Represents a node in a binary tree. @@ -233,17 +233,14 @@ export class BinaryTree< return [this.createNode(key, finalValue), finalValue]; } - if (this.isKey(keyNodeEntryOrRaw)) return [this.createNode(keyNodeEntryOrRaw, value), value]; - if (this.isRaw(keyNodeEntryOrRaw)) { - if (this._toEntryFn) { - const [key, entryValue] = this._toEntryFn(keyNodeEntryOrRaw); - const finalValue = value ?? entryValue; - if (this.isKey(key)) return [this.createNode(key, finalValue), finalValue]; - } - return [undefined, undefined]; + const [key, entryValue] = this._toEntryFn!(keyNodeEntryOrRaw); + const finalValue = value ?? entryValue; + if (this.isKey(key)) return [this.createNode(key, finalValue), finalValue]; } + if (this.isKey(keyNodeEntryOrRaw)) return [this.createNode(keyNodeEntryOrRaw, value), value]; + return [undefined, undefined]; } @@ -310,7 +307,7 @@ export class BinaryTree< * indicating that it is of type `R`. */ isRaw(keyNodeEntryOrRaw: BTNRep | R): keyNodeEntryOrRaw is R { - return typeof keyNodeEntryOrRaw === 'object'; + return this._toEntryFn !== undefined && typeof keyNodeEntryOrRaw === 'object'; } /** @@ -352,6 +349,12 @@ export class BinaryTree< return keyNodeEntryOrRaw === this._NIL; } + isRange( + keyNodeEntryRawOrPredicate: BTNRep | R | NodePredicate | Range + ): keyNodeEntryRawOrPredicate is Range { + return keyNodeEntryRawOrPredicate instanceof Range; + } + /** * The function determines whether a given key, node, entry, or raw data is a leaf node in a binary * tree. @@ -515,6 +518,14 @@ export class BinaryTree< return inserted; } + /** + * Time Complexity: O(k * n) + * Space Complexity: O(1) + */ + merge(anotherTree: BinaryTree) { + this.addMany(anotherTree, []); + } + /** * Time Complexity: O(k * n) * Space Complexity: O(1) diff --git a/src/data-structures/binary-tree/bst.ts b/src/data-structures/binary-tree/bst.ts index 45cd80d..d359c60 100644 --- a/src/data-structures/binary-tree/bst.ts +++ b/src/data-structures/binary-tree/bst.ts @@ -23,6 +23,7 @@ import { BinaryTree, BinaryTreeNode } from './binary-tree'; import { IBinaryTree } from '../../interfaces'; import { Queue } from '../queue'; import { isComparable } from '../../utils'; +import { Range } from '../../common'; export class BSTNode = BSTNodeNested> extends BinaryTreeNode< K, @@ -167,8 +168,9 @@ export class BST< super([], options); if (options) { - const { comparator } = options; - if (comparator) this._comparator = comparator; + const { comparator, isReverse } = options; + if (isReverse !== undefined) this._isReverse = isReverse; + if (comparator !== undefined) this._comparator = comparator; } if (keysNodesEntriesOrRaws) this.addMany(keysNodesEntriesOrRaws); @@ -184,6 +186,12 @@ export class BST< return this._root; } + protected _isReverse = false; + + get isReverse() { + return this._isReverse; + } + /** * The function creates a new BSTNode with the given key and value and returns it. * @param {K} key - The key parameter is of type K, which represents the type of the key for the node @@ -269,11 +277,11 @@ export class BST< * @param {any} key - The `key` parameter is a value that will be checked to determine if it is of * type `K`. * @returns The `override isKey(key: any): key is K` function is returning a boolean value based on - * the result of the `isComparable` function with the condition `this.comparator !== + * the result of the `isComparable` function with the condition `this._compare !== * this._DEFAULT_COMPARATOR`. */ override isKey(key: any): key is K { - return isComparable(key, this.comparator !== this._DEFAULT_COMPARATOR); + return isComparable(key, this._compare !== this._DEFAULT_COMPARATOR); } /** @@ -300,11 +308,11 @@ export class BST< let current = this._root; while (current !== undefined) { - if (this.comparator(current.key, newNode.key) === 0) { + if (this._compare(current.key, newNode.key) === 0) { this._replaceNode(current, newNode); if (this._isMapMode) this._setValue(current.key, newValue); return true; - } else if (this.comparator(current.key, newNode.key) > 0) { + } else if (this._compare(current.key, newNode.key) > 0) { if (current.left === undefined) { current.left = newNode; if (this._isMapMode) this._setValue(newNode?.key, newValue); @@ -402,7 +410,7 @@ export class BST< } if (keyA !== undefined && keyA !== null && keyB !== undefined && keyB !== null) { - return this.comparator(keyA, keyB); + return this._compare(keyA, keyB); } return 0; }); @@ -444,6 +452,14 @@ export class BST< return inserted; } + /** + * Time Complexity: O(k * n) + * Space Complexity: O(1) + */ + override merge(anotherTree: BST) { + this.addMany(anotherTree, [], false); + } + /** * Time Complexity: O(log n) * Space Complexity: O(k + log n) @@ -473,7 +489,7 @@ export class BST< * collected in an array and returned as the output of the method. */ override search>( - keyNodeEntryRawOrPredicate: BTNRep | R | NodePredicate, + keyNodeEntryRawOrPredicate: BTNRep | R | NodePredicate | Range, onlyOne = false, callback: C = this._DEFAULT_NODE_CALLBACK as C, startNode: BTNRep | R = this._root, @@ -483,9 +499,36 @@ export class BST< if (keyNodeEntryRawOrPredicate === null) return []; startNode = this.ensureNode(startNode); if (!startNode) return []; - const predicate = this._ensurePredicate(keyNodeEntryRawOrPredicate); - const ans: ReturnType[] = []; + let predicate: NodePredicate; + const isRange = this.isRange(keyNodeEntryRawOrPredicate); + // Set predicate based on parameter type + if (isRange) { + predicate = node => keyNodeEntryRawOrPredicate.isInRange(node.key, this._comparator); + } else { + predicate = this._ensurePredicate(keyNodeEntryRawOrPredicate); + } + const isToLeftByRange = (cur: NODE) => { + if (isRange) { + const range = keyNodeEntryRawOrPredicate; + const leftS = this.isReverse ? range.high : range.low; + const leftI = this.isReverse ? range.includeHigh : range.includeLow; + return (leftI && this._compare(cur.key, leftS) >= 0) || (!leftI && this._compare(cur.key, leftS) > 0); + } + return false; + }; + + const isToRightByRange = (cur: NODE) => { + if (isRange) { + const range = keyNodeEntryRawOrPredicate; + const rightS = this.isReverse ? range.low : range.high; + const rightI = this.isReverse ? range.includeLow : range.includeLow; + + return (rightI && this._compare(cur.key, rightS) <= 0) || (!rightI && this._compare(cur.key, rightS) < 0); + } + return false; + }; + const ans: ReturnType[] = []; if (iterationType === 'RECURSIVE') { const dfs = (cur: NODE) => { if (predicate(cur)) { @@ -494,20 +537,24 @@ export class BST< } if (!this.isRealNode(cur.left) && !this.isRealNode(cur.right)) return; - if (!this._isPredicate(keyNodeEntryRawOrPredicate)) { + + if (isRange) { + if (this.isRealNode(cur.left) && isToLeftByRange(cur)) dfs(cur.left); + if (this.isRealNode(cur.right) && isToRightByRange(cur)) dfs(cur.right); + } else if (!this._isPredicate(keyNodeEntryRawOrPredicate)) { const benchmarkKey = this._extractKey(keyNodeEntryRawOrPredicate); if ( this.isRealNode(cur.left) && benchmarkKey !== null && benchmarkKey !== undefined && - this.comparator(cur.key, benchmarkKey) > 0 + this._compare(cur.key, benchmarkKey) > 0 ) dfs(cur.left); if ( this.isRealNode(cur.right) && benchmarkKey !== null && benchmarkKey !== undefined && - this.comparator(cur.key, benchmarkKey) < 0 + this._compare(cur.key, benchmarkKey) < 0 ) dfs(cur.right); } else { @@ -525,20 +572,23 @@ export class BST< ans.push(callback(cur)); if (onlyOne) return ans; } - if (!this._isPredicate(keyNodeEntryRawOrPredicate)) { + if (isRange) { + if (this.isRealNode(cur.left) && isToLeftByRange(cur)) stack.push(cur.left); + if (this.isRealNode(cur.right) && isToRightByRange(cur)) stack.push(cur.right); + } else if (!this._isPredicate(keyNodeEntryRawOrPredicate)) { const benchmarkKey = this._extractKey(keyNodeEntryRawOrPredicate); if ( this.isRealNode(cur.right) && benchmarkKey !== null && benchmarkKey !== undefined && - this.comparator(cur.key, benchmarkKey) < 0 + this._compare(cur.key, benchmarkKey) < 0 ) stack.push(cur.right); if ( this.isRealNode(cur.left) && benchmarkKey !== null && benchmarkKey !== undefined && - this.comparator(cur.key, benchmarkKey) > 0 + this._compare(cur.key, benchmarkKey) > 0 ) stack.push(cur.left); } else { @@ -712,7 +762,7 @@ export class BST< if (iterationType === 'RECURSIVE') { const dfs = (cur: NODE) => { - const compared = this.comparator(cur.key, targetKey); + const compared = this._compare(cur.key, targetKey); if (Math.sign(compared) === lesserOrGreater) ans.push(callback(cur)); if (this.isRealNode(cur.left)) dfs(cur.left); @@ -726,7 +776,7 @@ export class BST< while (queue.size > 0) { const cur = queue.shift(); if (this.isRealNode(cur)) { - const compared = this.comparator(cur.key, targetKey); + const compared = this._compare(cur.key, targetKey); if (Math.sign(compared) === lesserOrGreater) ans.push(callback(cur)); if (this.isRealNode(cur.left)) queue.push(cur.left); @@ -876,4 +926,8 @@ export class BST< } this._root = v; } + + protected _compare(a: K, b: K) { + return this._isReverse ? -this._comparator(a, b) : this._comparator(a, b); + } } diff --git a/src/data-structures/binary-tree/rb-tree.ts b/src/data-structures/binary-tree/rb-tree.ts index 93644f5..d4e35e2 100644 --- a/src/data-structures/binary-tree/rb-tree.ts +++ b/src/data-structures/binary-tree/rb-tree.ts @@ -51,6 +51,10 @@ export class RedBlackTreeNode< } } +/** + * 1. Efficient self-balancing, but not completely balanced. Compared with AVLTree, the addition and deletion efficiency is high but the query efficiency is slightly lower. + * 2. It is BST itself. Compared with Heap which is not completely ordered, RedBlackTree is completely ordered. + */ export class RedBlackTree< K = any, V = any, @@ -351,7 +355,7 @@ export class RedBlackTree< while (this.isRealNode(current)) { parent = current; - const compared = this.comparator(node.key, current.key); + const compared = this._compare(node.key, current.key); if (compared < 0) { current = current.left ?? this.NIL; } else if (compared > 0) { diff --git a/src/data-structures/binary-tree/tree-multi-map.ts b/src/data-structures/binary-tree/tree-multi-map.ts index bdbd1d1..aa4ba5f 100644 --- a/src/data-structures/binary-tree/tree-multi-map.ts +++ b/src/data-structures/binary-tree/tree-multi-map.ts @@ -173,8 +173,8 @@ export class TreeMultiMap< if (this.isKey(key)) return [this.createNode(key, finalValue, 'BLACK', count), finalValue]; } - if (this._toEntryFn) { - const [key, entryValue] = this._toEntryFn(keyNodeEntryOrRaw as R); + if (this.isRaw(keyNodeEntryOrRaw)) { + const [key, entryValue] = this._toEntryFn!(keyNodeEntryOrRaw); const finalValue = value ?? entryValue; if (this.isKey(key)) return [this.createNode(key, finalValue, 'BLACK', count), finalValue]; } diff --git a/src/index.ts b/src/index.ts index a98e67c..3d7dfff 100644 --- a/src/index.ts +++ b/src/index.ts @@ -2,4 +2,4 @@ export * from './data-structures'; export * from './utils'; export * from './interfaces'; export * from './types'; -export * from './constants'; +export * from './common'; diff --git a/src/types/common.ts b/src/types/common.ts index 9e17049..a4f07a2 100644 --- a/src/types/common.ts +++ b/src/types/common.ts @@ -23,3 +23,5 @@ export type OptValue = V | undefined; export type IterableWithSizeOrLength = IterableWithSize | IterableWithLength; export type CRUD = 'CREATED' | 'READ' | 'UPDATED' | 'DELETED'; + +export type Arithmetic = number | bigint; diff --git a/src/types/data-structures/binary-tree/binary-tree.ts b/src/types/data-structures/binary-tree/binary-tree.ts index 5c80c40..1dc9ea7 100644 --- a/src/types/data-structures/binary-tree/binary-tree.ts +++ b/src/types/data-structures/binary-tree/binary-tree.ts @@ -1,6 +1,6 @@ import { BinaryTree, BinaryTreeNode } from '../../../data-structures'; import { IterationType, OptValue } from '../../common'; -import { DFSOperation } from '../../../constants'; +import { DFSOperation } from '../../../common'; export type BinaryTreeNodeNested = BinaryTreeNode>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> diff --git a/src/types/data-structures/binary-tree/bst.ts b/src/types/data-structures/binary-tree/bst.ts index 37e7e12..138e662 100644 --- a/src/types/data-structures/binary-tree/bst.ts +++ b/src/types/data-structures/binary-tree/bst.ts @@ -7,7 +7,8 @@ export type BSTNodeNested = BSTNode> = BST>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> export type BSTOptions = BinaryTreeOptions & { - comparator?: Comparator + comparator?: Comparator; + isReverse?: boolean; } export type BSTNOptKey = K | undefined; diff --git a/src/types/data-structures/binary-tree/rb-tree.ts b/src/types/data-structures/binary-tree/rb-tree.ts index 3822199..ee415a7 100644 --- a/src/types/data-structures/binary-tree/rb-tree.ts +++ b/src/types/data-structures/binary-tree/rb-tree.ts @@ -7,4 +7,4 @@ export type RedBlackTreeNodeNested = RedBlackTreeNode> = RedBlackTree>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> -export type RBTreeOptions = BSTOptions & {}; +export type RBTreeOptions = Omit, 'isReverse'> & {}; diff --git a/test/unit/data-structures/binary-tree/bst.test.ts b/test/unit/data-structures/binary-tree/bst.test.ts index 446f768..0c510b9 100644 --- a/test/unit/data-structures/binary-tree/bst.test.ts +++ b/test/unit/data-structures/binary-tree/bst.test.ts @@ -1,4 +1,4 @@ -import { BinaryTreeNode, BST, BSTNode } from '../../../../src'; +import { BinaryTreeNode, BST, BSTNode, Range } from '../../../../src'; import { isDebugTest, isTestStackOverflow, SYSTEM_MAX_CALL_STACK } from '../../../config'; const isDebug = isDebugTest; @@ -1550,20 +1550,10 @@ describe('classic use', () => { // Test case for finding elements in a given range it('@example Find elements in a range', () => { - const bst = new BST([10, 5, 15, 3, 7, 12, 18]); - - // Helper function to find elements in range - const findElementsInRange = (min: number, max: number): number[] => { - return bst.search( - node => node.key >= min && node.key <= max, - false, - node => node.key - ); - }; - - // Assertions - expect(findElementsInRange(4, 12)).toEqual([10, 5, 7, 12]); - expect(findElementsInRange(15, 20)).toEqual([15, 18]); + const bst = new BST([10, 5, 15, 3, 7, 12, 18], { isReverse: true }); + expect(bst.search(new Range(5, 10))).toEqual([10, 5, 7]); + expect(bst.search(new Range(4, 12))).toEqual([10, 5, 7, 12]); + expect(bst.search(new Range(15, 20))).toEqual([15, 18]); }); // Test case for Huffman coding simulation diff --git a/test/unit/data-structures/binary-tree/rb-tree.test.ts b/test/unit/data-structures/binary-tree/rb-tree.test.ts index f07d61d..f2a7796 100644 --- a/test/unit/data-structures/binary-tree/rb-tree.test.ts +++ b/test/unit/data-structures/binary-tree/rb-tree.test.ts @@ -1,4 +1,4 @@ -import { BinaryTreeNode, BSTNode, RedBlackTree, RedBlackTreeNode } from '../../../../src'; +import { BinaryTreeNode, BSTNode, Range, RedBlackTree, RedBlackTreeNode } from '../../../../src'; import { getRandomInt, getRandomIntArray, magnitude } from '../../../utils'; import { OrderedMap } from 'js-sdsl'; @@ -819,3 +819,102 @@ describe('RedBlackTree - _deleteFixup', () => { ]); }); }); + +describe('classic use', () => { + it('Database Index: Add, Search, and Delete Records', () => { + const dbIndex = new RedBlackTree(); + + // Insert records + dbIndex.add(1, 'Alice'); + dbIndex.add(2, 'Bob'); + dbIndex.add(3, 'Charlie'); + + // Search for records + expect(dbIndex.get(1)).toBe('Alice'); + expect(dbIndex.get(2)).toBe('Bob'); + expect(dbIndex.get(3)).toBe('Charlie'); + + // Delete a record + dbIndex.delete(2); + expect(dbIndex.get(2)).toBeUndefined(); + }); + + it('@example Merge 3 sorted datasets', () => { + const dataset1 = new RedBlackTree([ + [1, 'A'], + [7, 'G'] + ]); + const dataset2 = [ + [2, 'B'], + [6, 'F'] + ]; + const dataset3 = new RedBlackTree([ + [3, 'C'], + [5, 'E'], + [4, 'D'] + ]); + + // Merge datasets into a single Red-Black Tree + const merged = new RedBlackTree(dataset1); + merged.addMany(dataset2); + merged.merge(dataset3); + + // Verify merged dataset is in sorted order + expect([...merged.values()]).toEqual(['A', 'B', 'C', 'D', 'E', 'F', 'G']); + }); + + // Test case for finding elements in a given range + it('Find elements in a range', () => { + const bst = new RedBlackTree([10, 5, 15, 3, 7, 12, 18]); + expect(bst.search(new Range(5, 10))).toEqual([5, 10, 7]); + expect(bst.search(new Range(4, 12))).toEqual([5, 10, 12, 7]); + expect(bst.search(new Range(15, 20))).toEqual([15, 18]); + }); + + it('Timer List: Manage Timed Tasks', () => { + const timerList = new RedBlackTree(); // Key: Time in ms, Value: Task Name + + // Schedule tasks + timerList.add(100, 'Task A'); + timerList.add(200, 'Task B'); + timerList.add(50, 'Task C'); + + // Verify the order of tasks by retrieval + expect([...timerList.values()]).toEqual(['Task C', 'Task A', 'Task B']); // Sorted by key (time) + + // Remove the earliest task + timerList.delete(50); + expect([...timerList.values()]).toEqual(['Task A', 'Task B']); + }); + + it('Scheduler: Manage Tasks by Priority', () => { + const scheduler = new RedBlackTree(); // Key: Priority, Value: Task Name + + // Add tasks with different priorities + scheduler.add(3, 'Low Priority Task'); + scheduler.add(1, 'High Priority Task'); + scheduler.add(2, 'Medium Priority Task'); + + // Verify the order of tasks by retrieval + expect([...scheduler.values()]).toEqual(['High Priority Task', 'Medium Priority Task', 'Low Priority Task']); + + // Remove the highest priority task + scheduler.delete(1); + expect([...scheduler.values()]).toEqual(['Medium Priority Task', 'Low Priority Task']); + }); + + it('Routing Table: Manage IP Routes', () => { + const routingTable = new RedBlackTree(); // Key: IP Address, Value: Route + + // Add routes + routingTable.add(1921680101, 'Route A'); + routingTable.add(1921680102, 'Route B'); + routingTable.add(1921680100, 'Route C'); + + // Search for a specific route + expect(routingTable.get(1921680101)).toBe('Route A'); + + // Verify all routes in sorted order + expect([...routingTable.values()]).toEqual(['Route C', 'Route A', 'Route B']); + }); +});