From 713b145a1c1cd9535147a8ed1358029553c9b948 Mon Sep 17 00:00:00 2001 From: Revone Date: Thu, 21 Nov 2024 19:02:19 +1300 Subject: [PATCH] feat: Add a highly generic search method to binary tree data structures. Adjust the parameter order for the getPathToRoot method. Add example code to the BST documentation. Rename the get method in DoublyLinkedList and SinglyLinkedList to search. --- scripts/testToExample.ts | 6 +- src/data-structures/binary-tree/avl-tree.ts | 2 +- .../binary-tree/binary-tree.ts | 131 ++++++++++++------ src/data-structures/binary-tree/bst.ts | 114 +++++++++++---- .../linked-list/doubly-linked-list.ts | 2 +- .../linked-list/singly-linked-list.ts | 2 +- .../binary-tree/binary-tree.test.ts | 4 +- .../data-structures/binary-tree/bst.test.ts | 84 +++++++++++ 8 files changed, 264 insertions(+), 81 deletions(-) diff --git a/scripts/testToExample.ts b/scripts/testToExample.ts index a2c6891..b2761f8 100644 --- a/scripts/testToExample.ts +++ b/scripts/testToExample.ts @@ -216,9 +216,9 @@ function updateExamples(testDir: string, sourceBaseDir: string): void { const relativePath = path.relative(testDir, file); const sourceFilePath = path.resolve(sourceBaseDir, relativePath.replace('.test.ts', '.ts')); - const className = toPascalCase(path.basename(sourceFilePath, '.ts')); - - addExamplesToSourceFile(sourceFilePath, toPascalCase(className), examples); + let className = toPascalCase(path.basename(sourceFilePath, '.ts')); + if (className === 'Bst') className = 'BST'; + addExamplesToSourceFile(sourceFilePath, className, examples); const dirKey = dirMap[className]; if (!dirKey) { diff --git a/src/data-structures/binary-tree/avl-tree.ts b/src/data-structures/binary-tree/avl-tree.ts index c7369f1..1b3635d 100644 --- a/src/data-structures/binary-tree/avl-tree.ts +++ b/src/data-structures/binary-tree/avl-tree.ts @@ -434,7 +434,7 @@ export class AVLTree< */ protected _balancePath(node: BTNRep | R): void { node = this.ensureNode(node); - const path = this.getPathToRoot(node => node, node, false); // first O(log n) + O(log n) + const path = this.getPathToRoot(node, node => node, false); // first O(log n) + O(log n) for (let i = 0; i < path.length; i++) { // second O(log n) const A = path[i]; diff --git a/src/data-structures/binary-tree/binary-tree.ts b/src/data-structures/binary-tree/binary-tree.ts index 0b94a5f..7f20f0a 100644 --- a/src/data-structures/binary-tree/binary-tree.ts +++ b/src/data-structures/binary-tree/binary-tree.ts @@ -302,6 +302,13 @@ export class BinaryTree< return keyNodeEntryOrRaw instanceof BinaryTreeNode; } + /** + * The function `isRaw` checks if the input parameter is of type `R` by verifying if it is an object. + * @param {BTNRep | R} keyNodeEntryOrRaw - BTNRep | R + * @returns The function `isRaw` is checking if the `keyNodeEntryOrRaw` parameter is of type `R` by + * checking if it is an object. If the parameter is an object, the function will return `true`, + * indicating that it is of type `R`. + */ isRaw(keyNodeEntryOrRaw: BTNRep | R): keyNodeEntryOrRaw is R { return typeof keyNodeEntryOrRaw === 'object'; } @@ -585,6 +592,75 @@ export class BinaryTree< return deletedResult; } + /** + * Time Complexity: O(n) + * Space Complexity: O(k + log n) + * + * The `search` function in TypeScript performs a depth-first or breadth-first search on a tree + * structure based on a given predicate or key, with options to return multiple results or just one. + * @param {BTNRep | R | NodePredicate} keyNodeEntryRawOrPredicate - The + * `keyNodeEntryRawOrPredicate` parameter in the `search` function can accept three types of values: + * @param [onlyOne=false] - The `onlyOne` parameter in the `search` function is a boolean flag that + * determines whether the search should stop after finding the first matching node. If `onlyOne` is + * set to `true`, the search will return as soon as a matching node is found. If `onlyOne` is + * @param {C} callback - The `callback` parameter in the `search` function is a callback function + * that will be called on each node that matches the search criteria. It is of type `C`, which + * extends `NodeCallback`. The default value for `callback` is `this._DEFAULT_NODE_CALLBACK` if + * @param {BTNRep | R} startNode - The `startNode` parameter in the `search` function is + * used to specify the node from which the search operation should begin. It represents the starting + * point in the binary tree where the search will be performed. If no specific `startNode` is + * provided, the search operation will start from the root + * @param {IterationType} iterationType - The `iterationType` parameter in the `search` function + * specifies the type of iteration to be used when searching for nodes in a binary tree. It can have + * two possible values: + * @returns The `search` function returns an array of values that match the provided criteria based + * on the search algorithm implemented within the function. + */ + search>( + keyNodeEntryRawOrPredicate: BTNRep | R | NodePredicate, + onlyOne = false, + callback: C = this._DEFAULT_NODE_CALLBACK as C, + startNode: BTNRep | R = this._root, + iterationType: IterationType = this.iterationType + ): ReturnType[] { + if (keyNodeEntryRawOrPredicate === undefined) return []; + if (keyNodeEntryRawOrPredicate === null) return []; + startNode = this.ensureNode(startNode); + if (!startNode) return []; + const predicate = this._ensurePredicate(keyNodeEntryRawOrPredicate); + + const ans: ReturnType[] = []; + + if (iterationType === 'RECURSIVE') { + const dfs = (cur: NODE) => { + if (predicate(cur)) { + ans.push(callback(cur)); + if (onlyOne) return; + } + if (!this.isRealNode(cur.left) && !this.isRealNode(cur.right)) return; + if (this.isRealNode(cur.left)) dfs(cur.left); + if (this.isRealNode(cur.right)) dfs(cur.right); + }; + + dfs(startNode); + } else { + const stack = [startNode]; + while (stack.length > 0) { + const cur = stack.pop(); + if (this.isRealNode(cur)) { + if (predicate(cur)) { + ans.push(callback(cur)); + if (onlyOne) return ans; + } + if (this.isRealNode(cur.left)) stack.push(cur.left); + if (this.isRealNode(cur.right)) stack.push(cur.right); + } + } + } + + return ans; + } + /** * Time Complexity: O(n) * Space Complexity: O(k + log n) @@ -612,42 +688,7 @@ export class BinaryTree< startNode: BTNRep | R = this._root, iterationType: IterationType = this.iterationType ): NODE[] { - if (keyNodeEntryRawOrPredicate === undefined) return []; - if (keyNodeEntryRawOrPredicate === null) return []; - startNode = this.ensureNode(startNode); - if (!startNode) return []; - const callback = this._ensurePredicate(keyNodeEntryRawOrPredicate); - - const ans: NODE[] = []; - - if (iterationType === 'RECURSIVE') { - const dfs = (cur: NODE) => { - if (callback(cur)) { - ans.push(cur); - if (onlyOne) return; - } - if (!this.isRealNode(cur.left) && !this.isRealNode(cur.right)) return; - if (this.isRealNode(cur.left)) dfs(cur.left); - if (this.isRealNode(cur.right)) dfs(cur.right); - }; - - dfs(startNode); - } else { - const stack = [startNode]; - while (stack.length > 0) { - const cur = stack.pop(); - if (this.isRealNode(cur)) { - if (callback(cur)) { - ans.push(cur); - if (onlyOne) return ans; - } - if (this.isRealNode(cur.left)) stack.push(cur.left); - if (this.isRealNode(cur.right)) stack.push(cur.right); - } - } - } - - return ans; + return this.search(keyNodeEntryRawOrPredicate, onlyOne, node => node, startNode, iterationType); } /** @@ -675,7 +716,7 @@ export class BinaryTree< startNode: BTNRep | R = this._root, iterationType: IterationType = this.iterationType ): OptNodeOrNull { - return this.getNodes(keyNodeEntryRawOrPredicate, true, startNode, iterationType)[0] ?? null; + return this.search(keyNodeEntryRawOrPredicate, true, node => node, startNode, iterationType)[0] ?? null; } /** @@ -723,7 +764,7 @@ export class BinaryTree< iterationType: IterationType = this.iterationType ): V | undefined { if (this._isMapMode) { - const key = this._getKey(keyNodeEntryRawOrPredicate); + const key = this._extractKey(keyNodeEntryRawOrPredicate); if (key === null || key === undefined) return; return this._store.get(key); } @@ -756,7 +797,7 @@ export class BinaryTree< startNode: BTNRep | R = this._root, iterationType: IterationType = this.iterationType ): boolean { - return this.getNodes(keyNodeEntryRawOrPredicate, true, startNode, iterationType).length > 0; + return this.search(keyNodeEntryRawOrPredicate, true, node => node, startNode, iterationType).length > 0; } /** @@ -1023,9 +1064,9 @@ export class BinaryTree< * parameter. */ getPathToRoot>>( - callback: C = this._DEFAULT_NODE_CALLBACK as C, beginNode: BTNRep | R, - isReverse = true + callback: C = this._DEFAULT_NODE_CALLBACK as C, + isReverse = false ): ReturnType[] { const result: ReturnType[] = []; let beginNodeEnsured = this.ensureNode(beginNode); @@ -2144,16 +2185,16 @@ export class BinaryTree< * Time Complexity: O(1) * Space Complexity: O(1) * - * The function `_getKey` in TypeScript returns the key from a given input, which can be a node, + * The function `_extractKey` in TypeScript returns the key from a given input, which can be a node, * entry, raw data, or null/undefined. - * @param {BTNRep | R} keyNodeEntryOrRaw - The `_getKey` method you provided is a + * @param {BTNRep | R} keyNodeEntryOrRaw - The `_extractKey` method you provided is a * TypeScript method that takes in a parameter `keyNodeEntryOrRaw` of type `BTNRep | R`, * where `BTNRep` is a generic type with keys `K`, `V`, and `NODE`, and ` - * @returns The `_getKey` method returns the key value extracted from the `keyNodeEntryOrRaw` + * @returns The `_extractKey` method returns the key value extracted from the `keyNodeEntryOrRaw` * parameter. The return value can be a key value of type `K`, `null`, or `undefined`, depending on * the conditions checked in the method. */ - protected _getKey(keyNodeEntryOrRaw: BTNRep | R): K | null | undefined { + protected _extractKey(keyNodeEntryOrRaw: BTNRep | R): K | null | undefined { if (keyNodeEntryOrRaw === null) return null; if (keyNodeEntryOrRaw === undefined) return; if (keyNodeEntryOrRaw === this._NIL) return; diff --git a/src/data-structures/binary-tree/bst.ts b/src/data-structures/binary-tree/bst.ts index f5827fe..45cd80d 100644 --- a/src/data-structures/binary-tree/bst.ts +++ b/src/data-structures/binary-tree/bst.ts @@ -92,6 +92,58 @@ export class BSTNode = BSTNod * 5. Logarithmic Operations: Ideal operations like insertion, deletion, and searching are O(log n) time-efficient. * 6. Balance Variability: Can become unbalanced; special types maintain balance. * 7. No Auto-Balancing: Standard BSTs don't automatically balance themselves. + * @example + * // Find kth smallest element + * // Create a BST with some elements + * const bst = new BST([5, 3, 7, 1, 4, 6, 8]); + * const sortedKeys = bst.dfs(node => node.key, 'IN'); + * + * // Helper function to find kth smallest + * const findKthSmallest = (k: number): number | undefined => { + * return sortedKeys[k - 1]; + * }; + * + * // Assertions + * console.log(findKthSmallest(1)); // 1 + * console.log(findKthSmallest(3)); // 4 + * console.log(findKthSmallest(7)); // 8 + * @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 + * console.log(findElementsInRange(4, 12)); // [10, 5, 7, 12] + * console.log(findElementsInRange(15, 20)); // [15, 18] + * @example + * // Find lowest common ancestor + * const bst = new BST([20, 10, 30, 5, 15, 25, 35, 3, 7, 12, 18]); + * + * function findFirstCommon(arr1: number[], arr2: number[]): number | undefined { + * for (const num of arr1) { + * if (arr2.indexOf(num) !== -1) { + * return num; + * } + * } + * return undefined; + * } + * + * // LCA helper function + * const findLCA = (num1: number, num2: number): number | undefined => { + * const path1 = bst.getPathToRoot(num1); + * const path2 = bst.getPathToRoot(num2); + * // Find the first common ancestor + * return findFirstCommon(path1, path2); + * }; + * + * // Assertions + * console.log(findLCA(3, 10)); // 7 + * console.log(findLCA(5, 35)); // 15 + * console.log(findLCA(20, 30)); // 25 */ export class BST< K = any, @@ -396,48 +448,54 @@ export class BST< * Time Complexity: O(log n) * Space Complexity: O(k + log n) * - * The function `getNodes` in TypeScript overrides the base class method to retrieve nodes based on a - * given keyNodeEntryRawOrPredicate and iteration type. - * @param {BTNRep | R | NodePredicate} keyNodeEntryRawOrPredicate - The `keyNodeEntryRawOrPredicate` - * parameter in the `getNodes` method is used to filter the nodes that will be returned. It can be a - * key, a node, an entry, or a custom keyNodeEntryRawOrPredicate function that determines whether a node should be - * included in the result. - * @param [onlyOne=false] - The `onlyOne` parameter in the `getNodes` method is a boolean flag that - * determines whether to return only the first node that matches the keyNodeEntryRawOrPredicate (`true`) or all nodes - * that match the keyNodeEntryRawOrPredicate (`false`). If `onlyOne` is set to `true`, the method will stop iterating - * and - * @param {BTNRep | R} startNode - The `startNode` parameter in the - * `getNodes` method is used to specify the starting point for traversing the tree when searching for - * nodes that match a given keyNodeEntryRawOrPredicate. It represents the root node of the subtree where the search - * should begin. If not explicitly provided, the default value for `begin - * @param {IterationType} iterationType - The `iterationType` parameter in the `getNodes` method - * specifies the type of iteration to be performed when traversing the nodes of a binary tree. It can - * have two possible values: - * @returns The `getNodes` method returns an array of nodes that satisfy the given keyNodeEntryRawOrPredicate. + * The function `search` in TypeScript overrides the search behavior in a binary tree structure based + * on specified criteria. + * @param {BTNRep | R | NodePredicate} keyNodeEntryRawOrPredicate - The + * `keyNodeEntryRawOrPredicate` parameter in the `override search` method can accept one of the + * following types: + * @param [onlyOne=false] - The `onlyOne` parameter is a boolean flag that determines whether the + * search should stop after finding the first matching node. If `onlyOne` is set to `true`, the + * search will return as soon as a matching node is found. If `onlyOne` is set to `false`, the + * @param {C} callback - The `callback` parameter in the `override search` function is a function + * that will be called on each node that matches the search criteria. It is of type `C`, which + * extends `NodeCallback`. The callback function should accept a node of type `NODE` as its + * argument and + * @param {BTNRep | R} startNode - The `startNode` parameter in the `override search` + * method represents the node from which the search operation will begin. It is the starting point + * for searching within the tree data structure. The method ensures that the `startNode` is a valid + * node before proceeding with the search operation. If the ` + * @param {IterationType} iterationType - The `iterationType` parameter in the `override search` + * function determines the type of iteration to be used during the search operation. It can have two + * possible values: + * @returns The `override search` method returns an array of values that match the search criteria + * specified by the input parameters. The method performs a search operation on a binary tree + * structure based on the provided key, predicate, and other options. The search results are + * collected in an array and returned as the output of the method. */ - override getNodes( + override search>( keyNodeEntryRawOrPredicate: BTNRep | R | NodePredicate, onlyOne = false, + callback: C = this._DEFAULT_NODE_CALLBACK as C, startNode: BTNRep | R = this._root, iterationType: IterationType = this.iterationType - ): NODE[] { + ): ReturnType[] { if (keyNodeEntryRawOrPredicate === undefined) return []; if (keyNodeEntryRawOrPredicate === null) return []; startNode = this.ensureNode(startNode); if (!startNode) return []; - const callback = this._ensurePredicate(keyNodeEntryRawOrPredicate); - const ans: NODE[] = []; + const predicate = this._ensurePredicate(keyNodeEntryRawOrPredicate); + const ans: ReturnType[] = []; if (iterationType === 'RECURSIVE') { const dfs = (cur: NODE) => { - if (callback(cur)) { - ans.push(cur); + if (predicate(cur)) { + ans.push(callback(cur)); if (onlyOne) return; } if (!this.isRealNode(cur.left) && !this.isRealNode(cur.right)) return; if (!this._isPredicate(keyNodeEntryRawOrPredicate)) { - const benchmarkKey = this._getKey(keyNodeEntryRawOrPredicate); + const benchmarkKey = this._extractKey(keyNodeEntryRawOrPredicate); if ( this.isRealNode(cur.left) && benchmarkKey !== null && @@ -463,12 +521,12 @@ export class BST< const stack = [startNode]; while (stack.length > 0) { const cur = stack.pop()!; - if (callback(cur)) { - ans.push(cur); + if (predicate(cur)) { + ans.push(callback(cur)); if (onlyOne) return ans; } if (!this._isPredicate(keyNodeEntryRawOrPredicate)) { - const benchmarkKey = this._getKey(keyNodeEntryRawOrPredicate); + const benchmarkKey = this._extractKey(keyNodeEntryRawOrPredicate); if ( this.isRealNode(cur.right) && benchmarkKey !== null && diff --git a/src/data-structures/linked-list/doubly-linked-list.ts b/src/data-structures/linked-list/doubly-linked-list.ts index e7cbbd7..9430390 100644 --- a/src/data-structures/linked-list/doubly-linked-list.ts +++ b/src/data-structures/linked-list/doubly-linked-list.ts @@ -999,7 +999,7 @@ export class DoublyLinkedList extends IterableElementBase | ((node: DoublyLinkedListNode) => boolean) ): E | undefined { const predicate = this._ensurePredicate(elementNodeOrPredicate); diff --git a/src/data-structures/linked-list/singly-linked-list.ts b/src/data-structures/linked-list/singly-linked-list.ts index ef8e437..53954dc 100644 --- a/src/data-structures/linked-list/singly-linked-list.ts +++ b/src/data-structures/linked-list/singly-linked-list.ts @@ -223,7 +223,7 @@ export class SinglyLinkedList extends IterableElementBase | ((node: SinglyLinkedListNode) => boolean) ): E | undefined { const predicate = this._ensurePredicate(elementNodeOrPredicate); diff --git a/test/unit/data-structures/binary-tree/binary-tree.test.ts b/test/unit/data-structures/binary-tree/binary-tree.test.ts index dfabe5b..803b6fe 100644 --- a/test/unit/data-structures/binary-tree/binary-tree.test.ts +++ b/test/unit/data-structures/binary-tree/binary-tree.test.ts @@ -1155,8 +1155,8 @@ describe('BinaryTree', () => { tree.add([3, 'B']); tree.add([7, 'C']); - expect(tree.getPathToRoot(undefined, 7)).toEqual([5, 7]); - expect(tree.getPathToRoot(undefined, 1)).toEqual([]); + expect(tree.getPathToRoot(7)).toEqual([7, 5]); + expect(tree.getPathToRoot(1)).toEqual([]); }); it('should check if the tree is perfectly balanced', () => { diff --git a/test/unit/data-structures/binary-tree/bst.test.ts b/test/unit/data-structures/binary-tree/bst.test.ts index 9b01de8..446f768 100644 --- a/test/unit/data-structures/binary-tree/bst.test.ts +++ b/test/unit/data-structures/binary-tree/bst.test.ts @@ -1529,3 +1529,87 @@ describe('BST iterative methods not map mode test', () => { expect(balanced.leaves(node => balanced.get(node?.key))).toEqual(['a', 'f', 'd', 'i']); }); }); + +describe('classic use', () => { + // Test case for finding the kth smallest element + it('@example Find kth smallest element', () => { + // Create a BST with some elements + const bst = new BST([5, 3, 7, 1, 4, 6, 8]); + const sortedKeys = bst.dfs(node => node.key, 'IN'); + + // Helper function to find kth smallest + const findKthSmallest = (k: number): number | undefined => { + return sortedKeys[k - 1]; + }; + + // Assertions + expect(findKthSmallest(1)).toBe(1); + expect(findKthSmallest(3)).toBe(4); + expect(findKthSmallest(7)).toBe(8); + }); + + // 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]); + }); + + // Test case for Huffman coding simulation + it('Huffman coding frequency simulation', () => { + // Create a BST to simulate Huffman tree + const frequencyBST = new BST([ + ['a', 5], + ['b', 9], + ['c', 12], + ['d', 13], + ['e', 16], + ['f', 45] + ]); + + // Sort nodes by frequency + const sortedFrequencies = frequencyBST.dfs(node => ({ char: node.key, freq: node.value }), 'IN'); + + // Build Huffman tree simulation + expect(sortedFrequencies[0].char).toBe('a'); + expect(sortedFrequencies[5].char).toBe('f'); + }); + + // Test case for Lowest Common Ancestor (LCA) + it('@example Find lowest common ancestor', () => { + const bst = new BST([20, 10, 30, 5, 15, 25, 35, 3, 7, 12, 18]); + + function findFirstCommon(arr1: number[], arr2: number[]): number | undefined { + for (const num of arr1) { + if (arr2.indexOf(num) !== -1) { + return num; + } + } + return undefined; + } + + // LCA helper function + const findLCA = (num1: number, num2: number): number | undefined => { + const path1 = bst.getPathToRoot(num1); + const path2 = bst.getPathToRoot(num2); + // Find the first common ancestor + return findFirstCommon(path1, path2); + }; + + // Assertions + expect(findLCA(3, 10)).toBe(7); + expect(findLCA(5, 35)).toBe(15); + expect(findLCA(20, 30)).toBe(25); + }); +});