fix: Fix the issue where an extra array layer was processed for entries during TreeMultiMap instantiation.

feat: Add support for  configuration in TreeMultiMap and AVLTreeMultiMap.
docs: Add example code for a game leaderboard using TreeMultiMap.
This commit is contained in:
Revone 2024-12-03 11:47:32 +13:00
parent ae42526b91
commit 670225e775
15 changed files with 628 additions and 141 deletions

View file

@ -8,7 +8,7 @@ All notable changes to this project will be documented in this file.
- [Semantic Versioning](https://semver.org/spec/v2.0.0.html)
- [`auto-changelog`](https://github.com/CookPete/auto-changelog)
## [v1.54.2](https://github.com/zrwusa/data-structure-typed/compare/v1.51.5...main) (upcoming)
## [v1.54.3](https://github.com/zrwusa/data-structure-typed/compare/v1.51.5...main) (upcoming)
### Changes

44
package-lock.json generated
View file

@ -1,12 +1,12 @@
{
"name": "data-structure-typed",
"version": "1.54.2",
"version": "1.54.3",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "data-structure-typed",
"version": "1.54.2",
"version": "1.54.3",
"license": "MIT",
"devDependencies": {
"@eslint/compat": "^1.2.2",
@ -19,10 +19,10 @@
"@typescript-eslint/eslint-plugin": "^8.12.1",
"@typescript-eslint/parser": "^8.12.1",
"auto-changelog": "^2.5.0",
"avl-tree-typed": "^1.54.1",
"avl-tree-typed": "^1.54.2",
"benchmark": "^2.1.4",
"binary-tree-typed": "^1.54.1",
"bst-typed": "^1.54.1",
"binary-tree-typed": "^1.54.2",
"bst-typed": "^1.54.2",
"data-structure-typed": "^1.54.2",
"dependency-cruiser": "^16.5.0",
"doctoc": "^2.2.1",
@ -32,7 +32,7 @@
"eslint-import-resolver-typescript": "^3.6.3",
"eslint-plugin-import": "^2.31.0",
"fast-glob": "^3.3.2",
"heap-typed": "^1.54.1",
"heap-typed": "^1.54.2",
"istanbul-badges-readme": "^1.9.0",
"jest": "^29.7.0",
"js-sdsl": "^4.4.2",
@ -3450,13 +3450,13 @@
}
},
"node_modules/avl-tree-typed": {
"version": "1.54.1",
"resolved": "https://registry.npmjs.org/avl-tree-typed/-/avl-tree-typed-1.54.1.tgz",
"integrity": "sha512-PhaVjii3L+vm57mf/AplYZeIFuUoe8AS470ULkiBiLICt5g3O5FlP/B7JMMl8EyzDz3NyA9XcQXW3gwZ7mEL8w==",
"version": "1.54.2",
"resolved": "https://registry.npmjs.org/avl-tree-typed/-/avl-tree-typed-1.54.2.tgz",
"integrity": "sha512-bp+MMq2CpASeAB7up5kggHZhcqxx3sKdd5AJ8aWMqabZin13BxRnzwRY/II9gp4hb4Tvtitw6AYfJliGcdWYyQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"data-structure-typed": "^1.54.1"
"data-structure-typed": "^1.54.2"
}
},
"node_modules/babel-jest": {
@ -3615,13 +3615,13 @@
}
},
"node_modules/binary-tree-typed": {
"version": "1.54.1",
"resolved": "https://registry.npmjs.org/binary-tree-typed/-/binary-tree-typed-1.54.1.tgz",
"integrity": "sha512-lYFAKI9bCqA0yrXZYnoUU2szPng1JgyUanGOoqbgIuDncPY3jNnmRZYHUCgwkRi7mu2ukfoUIohP/JSH3g6MTg==",
"version": "1.54.2",
"resolved": "https://registry.npmjs.org/binary-tree-typed/-/binary-tree-typed-1.54.2.tgz",
"integrity": "sha512-U6qW0bIh6o2qGz8OzWzUHnNwXa6HjYp0I5Ds4VURqtl4OEOeJh57cNOlKgbiEHZ5viai7S5aRqXIZHW6vStezA==",
"dev": true,
"license": "MIT",
"dependencies": {
"data-structure-typed": "^1.54.1"
"data-structure-typed": "^1.54.2"
}
},
"node_modules/brace-expansion": {
@ -3705,13 +3705,13 @@
}
},
"node_modules/bst-typed": {
"version": "1.54.1",
"resolved": "https://registry.npmjs.org/bst-typed/-/bst-typed-1.54.1.tgz",
"integrity": "sha512-2wfrlfxeVdHMfXOJ8Uw7DOFpzBgw6weaE1u6ZrHdJhvgP1cWanpd3Vasnq8EI5g/NjAjH4hCaWkCRaDPjXyjKw==",
"version": "1.54.2",
"resolved": "https://registry.npmjs.org/bst-typed/-/bst-typed-1.54.2.tgz",
"integrity": "sha512-hl49IvHT/uJMphP1AL6sifiCKVc0pCdsGG8PW5ealU2K0LO9kO3nMBU+FcfwsmMcvhRrhikW/1fC63c9X7Lq+Q==",
"dev": true,
"license": "MIT",
"dependencies": {
"data-structure-typed": "^1.54.1"
"data-structure-typed": "^1.54.2"
}
},
"node_modules/buffer-from": {
@ -5904,13 +5904,13 @@
}
},
"node_modules/heap-typed": {
"version": "1.54.1",
"resolved": "https://registry.npmjs.org/heap-typed/-/heap-typed-1.54.1.tgz",
"integrity": "sha512-zIiCZeOC1YSUSxuML63bslyr4PS62DFEA9cEnZRSegppIwKtzsLhmnv4vFCfewbucoHJR+J410zMSsSJH/PW1g==",
"version": "1.54.2",
"resolved": "https://registry.npmjs.org/heap-typed/-/heap-typed-1.54.2.tgz",
"integrity": "sha512-BEgfIZndyoxN4XRY9kWSS8217u6nt4GuWrBWBnNxZwp8EVyhExS/CA3NsOH8xzj3mgsxqU88LdWwGFpQqBxMbA==",
"dev": true,
"license": "MIT",
"dependencies": {
"data-structure-typed": "^1.54.1"
"data-structure-typed": "^1.54.2"
}
},
"node_modules/html-escaper": {

View file

@ -65,10 +65,10 @@
"@typescript-eslint/eslint-plugin": "^8.12.1",
"@typescript-eslint/parser": "^8.12.1",
"auto-changelog": "^2.5.0",
"avl-tree-typed": "^1.54.1",
"avl-tree-typed": "^1.54.2",
"benchmark": "^2.1.4",
"binary-tree-typed": "^1.54.1",
"bst-typed": "^1.54.1",
"binary-tree-typed": "^1.54.2",
"bst-typed": "^1.54.2",
"data-structure-typed": "^1.54.2",
"dependency-cruiser": "^16.5.0",
"doctoc": "^2.2.1",
@ -78,7 +78,7 @@
"eslint-import-resolver-typescript": "^3.6.3",
"eslint-plugin-import": "^2.31.0",
"fast-glob": "^3.3.2",
"heap-typed": "^1.54.1",
"heap-typed": "^1.54.2",
"istanbul-badges-readme": "^1.9.0",
"jest": "^29.7.0",
"js-sdsl": "^4.4.2",

View file

@ -45,6 +45,14 @@ const dirMap: Record<string, string | string[]> = {
Trie: "trie-typed",
UndirectedGraph: "undirected-graph-typed",
};
const classMap: Record<string, string> = {
Bst: "BST",
AvlTree: "AVLTree",
AvlTreeMultiMap: "AVLTreeMultiMap",
AvlTreeCounter: "AVLTreeCounter"
}
const fileName = 'README.md';
/**
@ -218,6 +226,8 @@ function updateExamples(testDir: string, sourceBaseDir: string): void {
const sourceFilePath = path.resolve(sourceBaseDir, relativePath.replace('.test.ts', '.ts'));
let className = toPascalCase(path.basename(sourceFilePath, '.ts'));
if (className === 'Bst') className = 'BST';
if (className === 'AvlTree') className = 'AVLTree';
className = classMap[className] || className;
addExamplesToSourceFile(sourceFilePath, className, examples);
const dirKey = dirMap[className];

View file

@ -100,6 +100,7 @@ export class AVLTreeMultiMap<K = any, V = any, R = object, MK = any, MV = any, M
specifyComparable: this._specifyComparable,
toEntryFn: this._toEntryFn,
isReverse: this._isReverse,
isMapMode: this._isMapMode,
...options
});
}
@ -108,19 +109,23 @@ export class AVLTreeMultiMap<K = any, V = any, R = object, MK = any, MV = any, M
* Time Complexity: O(1)
* Space Complexity: O(1)
*
* The function `createNode` overrides the method to create a new AVLTreeMultiMapNode with a
* specified key and an empty array of values.
* @param {K} key - The `key` parameter in the `createNode` method represents the key of the node
* that will be created in the AVLTreeMultiMap.
* @returns An AVLTreeMultiMapNode object is being returned, initialized with the provided key and an
* empty array.
* The `createNode` function in TypeScript overrides the default implementation to create a new
* AVLTreeMultiMapNode with a specified key and value array.
* @param {K} key - The `key` parameter represents the key of the node being created in the
* AVLTreeMultiMap.
* @param {V[]} value - The `value` parameter in the `createNode` method represents an array of
* values associated with a specific key in the AVLTreeMultiMapNode. If no value is provided when
* calling the method, an empty array `[]` is used as the default value.
* @returns An AVLTreeMultiMapNode object is being returned, with the specified key and value. If the
* AVLTreeMultiMap is in map mode, an empty array is used as the value, otherwise the provided value
* array is used.
*/
override createNode(key: K): AVLTreeMultiMapNode<K, V> {
return new AVLTreeMultiMapNode<K, V>(key, []);
override createNode(key: K, value: V[] = []): AVLTreeMultiMapNode<K, V> {
return new AVLTreeMultiMapNode<K, V>(key, this._isMapMode ? [] : value);
}
override add(
node: K | AVLTreeMultiMapNode<K, V> | [K | null | undefined, V[] | undefined] | null | undefined
keyNodeOrEntry: K | AVLTreeMultiMapNode<K, V> | [K | null | undefined, V[] | undefined] | null | undefined
): boolean;
override add(key: K, value: V): boolean;
@ -129,17 +134,16 @@ export class AVLTreeMultiMap<K = any, V = any, R = object, MK = any, MV = any, M
* Time Complexity: O(log n)
* Space Complexity: O(log n)
*
* The function `add` in TypeScript overrides the superclass method to add key-value pairs to an AVL
* tree multi-map.
* @param {K | AVLTreeMultiMapNode<K, V> | [K | null | undefined, V[] | undefined] | null | undefined | K} keyNodeOrEntry - The `keyNodeOrEntry`
* parameter in the `override add` method can be either a key-value pair entry or just a key. If it
* is a key-value pair entry, it will be in the format `[key, values]`, where `key` is the key and
* `values`
* @param {V} [value] - The `value` parameter in the `override add` method represents the value that
* you want to add to the AVLTreeMultiMap. It can be a single value or an array of values associated
* with a specific key.
* @returns The `override add` method is returning a boolean value, which indicates whether the
* addition operation was successful or not.
* The function `add` in this TypeScript code overrides the superclass method to add key-value pairs
* to an AVLTreeMultiMap, handling different input types and scenarios.
* @param [key] - The `key` parameter in the `override add` method represents the key of the entry to
* be added to the AVLTreeMultiMap. It can be of type `K`, which is the key type of the map. The key
* can be a single key value, a node of the AVLTree
* @param {V[]} [values] - The `values` parameter in the `add` method represents an array of values
* that you want to add to the AVLTreeMultiMap. It can contain one or more values associated with a
* specific key.
* @returns The `add` method is returning a boolean value, which indicates whether the operation was
* successful or not.
*/
override add(
keyNodeOrEntry: K | AVLTreeMultiMapNode<K, V> | [K | null | undefined, V[] | undefined] | null | undefined | K,
@ -150,27 +154,38 @@ export class AVLTreeMultiMap<K = any, V = any, R = object, MK = any, MV = any, M
const _commonAdd = (key?: BTNOptKeyOrNull<K>, values?: V[]) => {
if (key === undefined || key === null) return false;
const existingValues = this.get(key);
if (existingValues !== undefined && values !== undefined) {
for (const value of values) existingValues.push(value);
return true;
}
const existingNode = this.getNode(key);
if (this.isRealNode(existingNode)) {
if (existingValues === undefined) {
super.add(key, values);
return true;
}
if (values !== undefined) {
const _addToValues = () => {
const existingValues = this.get(key);
if (existingValues !== undefined && values !== undefined) {
for (const value of values) existingValues.push(value);
return true;
} else {
return false;
}
} else {
return super.add(key, values);
return false;
};
const _addByNode = () => {
const existingNode = this.getNode(key);
if (this.isRealNode(existingNode)) {
const existingValues = this.get(existingNode);
if (existingValues === undefined) {
super.add(key, values);
return true;
}
if (values !== undefined) {
for (const value of values) existingValues.push(value);
return true;
} else {
return false;
}
} else {
return super.add(key, values);
}
};
if (this._isMapMode) {
return _addByNode() || _addToValues();
}
return _addToValues() || _addByNode();
};
if (this.isEntry(keyNodeOrEntry)) {

View file

@ -60,6 +60,69 @@ export class AVLTreeNode<K = any, V = any> extends BSTNode<K, V> {
* 5. Efficient Lookups: Offers O(log n) search time, where 'n' is the number of nodes, due to its balanced nature.
* 6. Complex Insertions and Deletions: Due to rebalancing, these operations are more complex than in a regular BST.
* 7. Path Length: The path length from the root to any leaf is longer compared to an unbalanced BST, but shorter than a linear chain of nodes.
* @example
* // Find elements in a range
* type Datum = { timestamp: Date; temperature: number };
* // Fixed dataset of CPU temperature readings
* const cpuData: Datum[] = [
* { timestamp: new Date('2024-12-02T00:00:00'), temperature: 55.1 },
* { timestamp: new Date('2024-12-02T00:01:00'), temperature: 56.3 },
* { timestamp: new Date('2024-12-02T00:02:00'), temperature: 54.8 },
* { timestamp: new Date('2024-12-02T00:03:00'), temperature: 57.2 },
* { timestamp: new Date('2024-12-02T00:04:00'), temperature: 58.0 },
* { timestamp: new Date('2024-12-02T00:05:00'), temperature: 59.4 },
* { timestamp: new Date('2024-12-02T00:06:00'), temperature: 60.1 },
* { timestamp: new Date('2024-12-02T00:07:00'), temperature: 61.3 },
* { timestamp: new Date('2024-12-02T00:08:00'), temperature: 62.0 },
* { timestamp: new Date('2024-12-02T00:09:00'), temperature: 63.5 },
* { timestamp: new Date('2024-12-02T00:10:00'), temperature: 64.0 },
* { timestamp: new Date('2024-12-02T00:11:00'), temperature: 62.8 },
* { timestamp: new Date('2024-12-02T00:12:00'), temperature: 61.5 },
* { timestamp: new Date('2024-12-02T00:13:00'), temperature: 60.2 },
* { timestamp: new Date('2024-12-02T00:14:00'), temperature: 59.8 },
* { timestamp: new Date('2024-12-02T00:15:00'), temperature: 58.6 },
* { timestamp: new Date('2024-12-02T00:16:00'), temperature: 57.4 },
* { timestamp: new Date('2024-12-02T00:17:00'), temperature: 56.2 },
* { timestamp: new Date('2024-12-02T00:18:00'), temperature: 55.7 },
* { timestamp: new Date('2024-12-02T00:19:00'), temperature: 54.5 },
* { timestamp: new Date('2024-12-02T00:20:00'), temperature: 53.2 },
* { timestamp: new Date('2024-12-02T00:21:00'), temperature: 52.8 },
* { timestamp: new Date('2024-12-02T00:22:00'), temperature: 51.9 },
* { timestamp: new Date('2024-12-02T00:23:00'), temperature: 50.5 },
* { timestamp: new Date('2024-12-02T00:24:00'), temperature: 49.8 },
* { timestamp: new Date('2024-12-02T00:25:00'), temperature: 48.7 },
* { timestamp: new Date('2024-12-02T00:26:00'), temperature: 47.5 },
* { timestamp: new Date('2024-12-02T00:27:00'), temperature: 46.3 },
* { timestamp: new Date('2024-12-02T00:28:00'), temperature: 45.9 },
* { timestamp: new Date('2024-12-02T00:29:00'), temperature: 45.0 }
* ];
*
* // Create an AVL tree to store CPU temperature data
* const cpuTemperatureTree = new AVLTree<Date, number, Datum>(cpuData, {
* toEntryFn: ({ timestamp, temperature }) => [timestamp, temperature]
* });
*
* // Query a specific time range (e.g., from 00:05 to 00:15)
* const rangeStart = new Date('2024-12-02T00:05:00');
* const rangeEnd = new Date('2024-12-02T00:15:00');
* const rangeResults = cpuTemperatureTree.rangeSearch([rangeStart, rangeEnd], node => ({
* minute: node ? node.key.getMinutes() : 0,
* temperature: cpuTemperatureTree.get(node ? node.key : undefined)
* }));
*
* console.log(rangeResults); // [
* // { minute: 5, temperature: 59.4 },
* // { minute: 6, temperature: 60.1 },
* // { minute: 7, temperature: 61.3 },
* // { minute: 8, temperature: 62 },
* // { minute: 9, temperature: 63.5 },
* // { minute: 10, temperature: 64 },
* // { minute: 11, temperature: 62.8 },
* // { minute: 12, temperature: 61.5 },
* // { minute: 13, temperature: 60.2 },
* // { minute: 14, temperature: 59.8 },
* // { minute: 15, temperature: 58.6 }
* // ]
*/
export class AVLTree<K = any, V = any, R = object, MK = any, MV = any, MR = object>
extends BST<K, V, R, MK, MV, MR>

View file

@ -129,6 +129,68 @@ export class BinaryTreeNode<K = any, V = any> {
* 3. Depth and Height: Depth is the number of edges from the root to a node; height is the maximum depth in the tree.
* 4. Subtrees: Each child of a node forms the root of a subtree.
* 5. Leaf Nodes: Nodes without children are leaves.
* @example
* // determine loan approval using a decision tree
* // Decision tree structure
* const loanDecisionTree = new BinaryTree<string>(
* ['stableIncome', 'goodCredit', 'Rejected', 'Approved', 'Rejected'],
* { isDuplicate: true }
* );
*
* function determineLoanApproval(
* node?: BinaryTreeNode<string> | null,
* conditions?: { [key: string]: boolean }
* ): string {
* if (!node) throw new Error('Invalid node');
*
* // If it's a leaf node, return the decision result
* if (!node.left && !node.right) return node.key;
*
* // Check if a valid condition exists for the current node's key
* return conditions?.[node.key]
* ? determineLoanApproval(node.left, conditions)
* : determineLoanApproval(node.right, conditions);
* }
*
* // Test case 1: Stable income and good credit score
* console.log(determineLoanApproval(loanDecisionTree.root, { stableIncome: true, goodCredit: true })); // 'Approved'
*
* // Test case 2: Stable income but poor credit score
* console.log(determineLoanApproval(loanDecisionTree.root, { stableIncome: true, goodCredit: false })); // 'Rejected'
*
* // Test case 3: No stable income
* console.log(determineLoanApproval(loanDecisionTree.root, { stableIncome: false, goodCredit: true })); // 'Rejected'
*
* // Test case 4: No stable income and poor credit score
* console.log(determineLoanApproval(loanDecisionTree.root, { stableIncome: false, goodCredit: false })); // 'Rejected'
* @example
* // evaluate the arithmetic expression represented by the binary tree
* const expressionTree = new BinaryTree<number | string>(['+', 3, '*', null, null, 5, '-', null, null, 2, 8]);
*
* function evaluate(node?: BinaryTreeNode<number | string> | null): number {
* if (!node) return 0;
*
* if (typeof node.key === 'number') return node.key;
*
* const leftValue = evaluate(node.left); // Evaluate the left subtree
* const rightValue = evaluate(node.right); // Evaluate the right subtree
*
* // Perform the operation based on the current node's operator
* switch (node.key) {
* case '+':
* return leftValue + rightValue;
* case '-':
* return leftValue - rightValue;
* case '*':
* return leftValue * rightValue;
* case '/':
* return rightValue !== 0 ? leftValue / rightValue : 0; // Handle division by zero
* default:
* throw new Error(`Unsupported operator: ${node.key}`);
* }
* }
*
* console.log(evaluate(expressionTree.root)); // -27
*/
export class BinaryTree<K = any, V = any, R = object, MK = any, MV = any, MR = object>
extends IterableEntryBase<K, V | undefined>

View file

@ -102,9 +102,9 @@ export class BSTNode<K = any, V = any> extends BinaryTreeNode<K, V> {
* @example
* // Find elements in a range
* const bst = new BST<number>([10, 5, 15, 3, 7, 12, 18]);
* console.log(bst.search(new Range(5, 10))); // [10, 5, 7]
* console.log(bst.rangeSearch([4, 12], node => node.key.toString())); // ['10', '12', '5', '7']
* console.log(bst.search(new Range(4, 12, true, false))); // [10, 5, 7]
* console.log(bst.search(new Range(5, 10))); // [5, 7, 10]
* console.log(bst.rangeSearch([4, 12], node => node.key.toString())); // ['5', '7', '10', '12']
* console.log(bst.search(new Range(4, 12, true, false))); // [5, 7, 10]
* console.log(bst.rangeSearch([15, 20])); // [15, 18]
* console.log(bst.search(new Range(15, 20, false))); // [18]
* @example
@ -441,14 +441,14 @@ export class BST<K = any, V = any, R = object, MK = any, MV = any, MR = object>
if (arr.length === 0) return;
const mid = Math.floor((arr.length - 1) / 2);
let { key, value } = arr[mid];
const { key, value } = arr[mid];
const { orgIndex } = arr[mid];
if (this.isRaw(key)) {
const entry = this._toEntryFn!(key);
key = entry[0];
value = entry[1] ?? value;
inserted[orgIndex] = this.add(entry);
} else {
inserted[orgIndex] = this.add(key, value);
}
inserted[orgIndex] = this.add(key, value);
_dfs(arr.slice(0, mid));
_dfs(arr.slice(mid + 1));
};
@ -462,14 +462,14 @@ export class BST<K = any, V = any, R = object, MK = any, MV = any, MR = object>
const [l, r] = popped;
if (l <= r) {
const m = l + Math.floor((r - l) / 2);
let { key, value } = sorted[m];
const { key, value } = sorted[m];
const { orgIndex } = sorted[m];
if (this.isRaw(key)) {
const entry = this._toEntryFn!(key);
key = entry[0];
value = entry[1] ?? value;
inserted[orgIndex] = this.add(entry);
} else {
inserted[orgIndex] = this.add(key, value);
}
inserted[orgIndex] = this.add(key, value);
stack.push([m + 1, r]);
stack.push([l, m - 1]);
}

View file

@ -51,12 +51,6 @@ export class RedBlackTreeNode<K = any, V = any> extends BSTNode<K, V> {
* 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.
* @example
* // Find elements in a range
* const bst = new RedBlackTree<number>([10, 5, 15, 3, 7, 12, 18]);
* console.log(bst.search(new Range(5, 10))); // [5, 10, 7]
* console.log(bst.search(new Range(4, 12))); // [5, 10, 12, 7]
* console.log(bst.search(new Range(15, 20))); // [15, 18]
* @example
* // using Red-Black Tree as a price-based index for stock data
* // Define the structure of individual stock records
* interface StockRecord {
@ -98,7 +92,7 @@ export class RedBlackTreeNode<K = any, V = any> extends BSTNode<K, V> {
* [200, 400], // Price range
* node => priceIndex.get(node)?.symbol // Extract stock symbols for the result
* );
* console.log(stocksInRange); // ['GOOGL', 'MSFT', 'META']
* console.log(stocksInRange); // ['GOOGL', 'META', 'MSFT']
*/
export class RedBlackTree<K = any, V = any, R = object, MK = any, MV = any, MR = object>
extends BST<K, V, R, MK, MV, MR>

View file

@ -21,7 +21,7 @@ export class TreeMultiMapNode<K = any, V = any> extends RedBlackTreeNode<K, V[]>
* @param {V[]} value - The `value` parameter in the constructor represents an array of values of
* type `V`.
*/
constructor(key: K, value: V[]) {
constructor(key: K, value?: V[]) {
super(key, value);
}
@ -57,8 +57,8 @@ export class TreeMultiMapNode<K = any, V = any> extends RedBlackTreeNode<K, V[]>
* @example
* // Find elements in a range
* const tmm = new TreeMultiMap<number>([10, 5, 15, 3, 7, 12, 18]);
* console.log(tmm.search(new Range(5, 10))); // [5, 10, 7]
* console.log(tmm.search(new Range(4, 12))); // [5, 10, 12, 7]
* console.log(tmm.search(new Range(5, 10))); // [5, 7, 10]
* console.log(tmm.search(new Range(4, 12))); // [5, 7, 10, 12]
* console.log(tmm.search(new Range(15, 20))); // [15, 18]
*/
export class TreeMultiMap<K = any, V = any, R = object, MK = any, MV = any, MR = object>
@ -82,7 +82,7 @@ export class TreeMultiMap<K = any, V = any, R = object, MK = any, MV = any, MR =
> = [],
options?: TreeMultiMapOptions<K, V[], R>
) {
super([], { ...options, isMapMode: true });
super([], { ...options });
if (keysNodesEntriesOrRaws) {
this.addMany(keysNodesEntriesOrRaws);
}
@ -107,6 +107,7 @@ export class TreeMultiMap<K = any, V = any, R = object, MK = any, MV = any, MR =
specifyComparable: this._specifyComparable,
toEntryFn: this._toEntryFn,
isReverse: this._isReverse,
isMapMode: this._isMapMode,
...options
});
}
@ -115,18 +116,23 @@ export class TreeMultiMap<K = any, V = any, R = object, MK = any, MV = any, MR =
* Time Complexity: O(1)
* Space Complexity: O(1)
*
* The function `createNode` overrides the method to create a new `TreeMultiMapNode` with a specified
* key and an empty array of values.
* @param {K} key - The `key` parameter in the `createNode` method represents the key of the node
* that will be created in the TreeMultiMap data structure.
* @returns A new instance of `TreeMultiMapNode<K, V>` is being returned, with the specified key and
* an empty array as its value.
* The function `createNode` overrides the creation of a new TreeMultiMapNode with a specified key
* and value array.
* @param {K} key - The `key` parameter represents the key of the node being created in the
* `TreeMultiMap`.
* @param {V[]} value - The `value` parameter in the `createNode` method represents an array of
* values associated with a specific key in the TreeMultiMap data structure.
* @returns A new instance of `TreeMultiMapNode<K, V>` is being returned with the specified key and
* value. If `_isMapMode` is true, an empty array is passed as the value, otherwise the provided
* value is used.
*/
override createNode(key: K): TreeMultiMapNode<K, V> {
return new TreeMultiMapNode<K, V>(key, []);
override createNode(key: K, value: V[] = []): TreeMultiMapNode<K, V> {
return new TreeMultiMapNode<K, V>(key, this._isMapMode ? [] : value);
}
override add(node: K | TreeMultiMapNode<K, V> | [K | null | undefined, V[] | undefined] | null | undefined): boolean;
override add(
keyNodeOrEntry: K | TreeMultiMapNode<K, V> | [K | null | undefined, V[] | undefined] | null | undefined
): boolean;
override add(key: K, value: V): boolean;
@ -134,14 +140,13 @@ export class TreeMultiMap<K = any, V = any, R = object, MK = any, MV = any, MR =
* Time Complexity: O(log n)
* Space Complexity: O(log n)
*
* The function `add` in TypeScript overrides the superclass method to add key-value pairs to a
* TreeMultiMapNode, handling different input types and scenarios.
* @param {K | TreeMultiMapNode<K, V> | [K | null | undefined, V[] | undefined] | null | undefined} keyNodeOrEntry - The `keyNodeOrEntry`
* parameter in the `override add` method can be either a `BTNRep` object containing a key, an array
* of values, and a `TreeMultiMapNode`, or just a key.
* @param {V} [value] - The `value` parameter in the `override add` method represents the value that
* you want to add to the TreeMultiMap. If the key is already present in the map, the new value will
* be added to the existing list of values associated with that key. If the key is not present,
* The function overrides the add method to handle different types of input for a TreeMultiMap data
* structure.
* @param [key] - The `key` parameter in the `override add` method represents the key of the entry to
* be added to the TreeMultiMap. It can be of type `K`, which is the key type of the TreeMultiMap, or
* it can be a TreeMultiMapNode containing the key and its
* @param {V[]} [values] - The `values` parameter in the `add` method represents an array of values
* that you want to add to the TreeMultiMap. It can contain one or more values of type `V`.
* @returns The `add` method is returning a boolean value, which indicates whether the operation was
* successful or not.
*/
@ -154,27 +159,38 @@ export class TreeMultiMap<K = any, V = any, R = object, MK = any, MV = any, MR =
const _commonAdd = (key?: BTNOptKeyOrNull<K>, values?: V[]) => {
if (key === undefined || key === null) return false;
const existingValues = this.get(key);
if (existingValues !== undefined && values !== undefined) {
for (const value of values) existingValues.push(value);
return true;
}
const existingNode = this.getNode(key);
if (this.isRealNode(existingNode)) {
if (existingValues === undefined) {
super.add(key, values);
return true;
}
if (values !== undefined) {
const _addToValues = () => {
const existingValues = this.get(key);
if (existingValues !== undefined && values !== undefined) {
for (const value of values) existingValues.push(value);
return true;
} else {
return false;
}
} else {
return super.add(key, values);
return false;
};
const _addByNode = () => {
const existingNode = this.getNode(key);
if (this.isRealNode(existingNode)) {
const existingValues = this.get(existingNode);
if (existingValues === undefined) {
super.add(key, values);
return true;
}
if (values !== undefined) {
for (const value of values) existingValues.push(value);
return true;
} else {
return false;
}
} else {
return super.add(key, values);
}
};
if (this._isMapMode) {
return _addByNode() || _addToValues();
}
return _addToValues() || _addByNode();
};
if (this.isEntry(keyNodeOrEntry)) {

View file

@ -1,3 +1,3 @@
import type { AVLTreeOptions } from './avl-tree';
export type AVLTreeMultiMapOptions<K, V, R> = Omit<AVLTreeOptions<K, V, R>, 'isMapMode'> & {}
export type AVLTreeMultiMapOptions<K, V, R> = AVLTreeOptions<K, V, R> & {}

View file

@ -1,3 +1,3 @@
import type { RedBlackTreeOptions } from './red-black-tree';
export type TreeMultiMapOptions<K, V, R> = Omit<RedBlackTreeOptions<K, V, R>, 'isMapMode'> & {}
export type TreeMultiMapOptions<K, V, R> = RedBlackTreeOptions<K, V, R> & {}

View file

@ -570,3 +570,171 @@ describe('AVLTreeMultiMap iterative methods not map mode', () => {
expect(cloned.get(cloned.root?.right?.key)).toEqual(['c']);
});
});
describe('classic use', () => {
// Test suite for TreeMultiMap with player ranking and equipment
it('players ranked by score with their equipment', () => {
type Equipment = {
name: string; // Equipment name
quality: 'legendary' | 'epic' | 'rare' | 'common';
level: number;
};
type Player = {
name: string;
score: number;
equipments: Equipment[];
};
// Mock player data with their scores and equipment
const players: Player[] = [
{
name: 'DragonSlayer',
score: 8750,
equipments: [
{ name: 'AWM', quality: 'legendary', level: 85 },
{ name: 'Level 3 Helmet', quality: 'epic', level: 80 },
{ name: 'Extended Quickdraw Mag', quality: 'rare', level: 75 },
{ name: 'Compensator', quality: 'epic', level: 78 },
{ name: 'Vertical Grip', quality: 'rare', level: 72 }
]
},
{
name: 'ShadowNinja',
score: 7200,
equipments: [
{ name: 'M416', quality: 'epic', level: 75 },
{ name: 'Ghillie Suit', quality: 'rare', level: 70 },
{ name: 'Red Dot Sight', quality: 'common', level: 65 },
{ name: 'Extended QuickDraw Mag', quality: 'rare', level: 68 }
]
},
{
name: 'RuneMaster',
score: 9100,
equipments: [
{ name: 'KAR98K', quality: 'legendary', level: 90 },
{ name: 'Level 3 Vest', quality: 'legendary', level: 85 },
{ name: 'Holographic Sight', quality: 'epic', level: 82 },
{ name: 'Suppressor', quality: 'legendary', level: 88 },
{ name: 'Level 3 Backpack', quality: 'epic', level: 80 }
]
},
{
name: 'BattleKing',
score: 8500,
equipments: [
{ name: 'AUG', quality: 'epic', level: 82 },
{ name: 'Red Dot Sight', quality: 'rare', level: 75 },
{ name: 'Extended Mag', quality: 'common', level: 70 },
{ name: 'Tactical Stock', quality: 'rare', level: 76 }
]
},
{
name: 'SniperElite',
score: 7800,
equipments: [
{ name: 'M24', quality: 'legendary', level: 88 },
{ name: 'Compensator', quality: 'epic', level: 80 },
{ name: 'Scope 8x', quality: 'legendary', level: 85 },
{ name: 'Level 2 Helmet', quality: 'rare', level: 75 }
]
},
{
name: 'RushMaster',
score: 7500,
equipments: [
{ name: 'Vector', quality: 'rare', level: 72 },
{ name: 'Level 2 Helmet', quality: 'common', level: 65 },
{ name: 'Quickdraw Mag', quality: 'common', level: 60 },
{ name: 'Laser Sight', quality: 'rare', level: 68 }
]
},
{
name: 'GhostWarrior',
score: 8200,
equipments: [
{ name: 'SCAR-L', quality: 'epic', level: 78 },
{ name: 'Extended Quickdraw Mag', quality: 'rare', level: 70 },
{ name: 'Holographic Sight', quality: 'epic', level: 75 },
{ name: 'Suppressor', quality: 'rare', level: 72 },
{ name: 'Vertical Grip', quality: 'common', level: 65 }
]
},
{
name: 'DeathDealer',
score: 7300,
equipments: [
{ name: 'SKS', quality: 'epic', level: 76 },
{ name: 'Holographic Sight', quality: 'rare', level: 68 },
{ name: 'Extended Mag', quality: 'common', level: 65 }
]
},
{
name: 'StormRider',
score: 8900,
equipments: [
{ name: 'MK14', quality: 'legendary', level: 92 },
{ name: 'Level 3 Backpack', quality: 'legendary', level: 85 },
{ name: 'Scope 8x', quality: 'epic', level: 80 },
{ name: 'Suppressor', quality: 'legendary', level: 88 },
{ name: 'Tactical Stock', quality: 'rare', level: 75 }
]
},
{
name: 'CombatLegend',
score: 7600,
equipments: [
{ name: 'UMP45', quality: 'rare', level: 74 },
{ name: 'Level 2 Vest', quality: 'common', level: 67 },
{ name: 'Red Dot Sight', quality: 'common', level: 62 },
{ name: 'Extended Mag', quality: 'rare', level: 70 }
]
}
];
// Create a TreeMultiMap for player rankings
const playerRankings = new AVLTreeMultiMap<number, Equipment, Player>(players, {
toEntryFn: ({ score, equipments }) => [score, equipments],
isMapMode: false
});
const topPlayersEquipments = playerRankings.rangeSearch([8900, 10000], node => playerRankings.get(node));
expect(topPlayersEquipments).toEqual([
[
{
name: 'MK14',
quality: 'legendary',
level: 92
},
{ name: 'Level 3 Backpack', quality: 'legendary', level: 85 },
{
name: 'Scope 8x',
quality: 'epic',
level: 80
},
{ name: 'Suppressor', quality: 'legendary', level: 88 },
{
name: 'Tactical Stock',
quality: 'rare',
level: 75
}
],
[
{ name: 'KAR98K', quality: 'legendary', level: 90 },
{
name: 'Level 3 Vest',
quality: 'legendary',
level: 85
},
{ name: 'Holographic Sight', quality: 'epic', level: 82 },
{
name: 'Suppressor',
quality: 'legendary',
level: 88
},
{ name: 'Level 3 Backpack', quality: 'epic', level: 80 }
]
]);
});
});

View file

@ -546,6 +546,7 @@ describe('AVLTree iterative methods not map mode', () => {
describe('classic use', () => {
// Test case for finding elements in a given range
it('@example Find elements in a range', () => {
// In interval queries, AVL trees, with their strictly balanced structure and lower height, offer better query efficiency, making them ideal for frequent and high-performance interval queries. In contrast, Red-Black trees, with lower update costs, are more suitable for scenarios involving frequent insertions and deletions where the requirements for interval queries are less demanding.
type Datum = { timestamp: Date; temperature: number };
// Fixed dataset of CPU temperature readings
const cpuData: Datum[] = [
@ -594,18 +595,18 @@ describe('classic use', () => {
temperature: cpuTemperatureTree.get(node ? node.key : undefined)
}));
expect(rangeResults).toEqual( [
{ minute: 5, temperature: 59.4 },
{ minute: 6, temperature: 60.1 },
{ minute: 7, temperature: 61.3 },
{ minute: 8, temperature: 62 },
{ minute: 9, temperature: 63.5 },
{ minute: 10, temperature: 64 },
{ minute: 11, temperature: 62.8 },
{ minute: 12, temperature: 61.5 },
{ minute: 13, temperature: 60.2 },
{ minute: 14, temperature: 59.8 },
{ minute: 15, temperature: 58.6 }
]);
expect(rangeResults).toEqual([
{ minute: 5, temperature: 59.4 },
{ minute: 6, temperature: 60.1 },
{ minute: 7, temperature: 61.3 },
{ minute: 8, temperature: 62 },
{ minute: 9, temperature: 63.5 },
{ minute: 10, temperature: 64 },
{ minute: 11, temperature: 62.8 },
{ minute: 12, temperature: 61.5 },
{ minute: 13, temperature: 60.2 },
{ minute: 14, temperature: 59.8 },
{ minute: 15, temperature: 58.6 }
]);
});
});

View file

@ -837,11 +837,169 @@ describe('real world data', () => {
});
describe('classic use', () => {
// Test case for finding elements in a given range
it('@example Find elements in a range', () => {
const tmm = new TreeMultiMap<number>([10, 5, 15, 3, 7, 12, 18]);
expect(tmm.search(new Range(5, 10))).toEqual([5, 7, 10]);
expect(tmm.search(new Range(4, 12))).toEqual([5, 7, 10, 12]);
expect(tmm.search(new Range(15, 20))).toEqual([15, 18]);
// Test suite for TreeMultiMap with player ranking and equipment
it('@example players ranked by score with their equipment', () => {
type Equipment = {
name: string; // Equipment name
quality: 'legendary' | 'epic' | 'rare' | 'common';
level: number;
};
type Player = {
name: string;
score: number;
equipments: Equipment[];
};
// Mock player data with their scores and equipment
const players: Player[] = [
{
name: 'DragonSlayer',
score: 8750,
equipments: [
{ name: 'AWM', quality: 'legendary', level: 85 },
{ name: 'Level 3 Helmet', quality: 'epic', level: 80 },
{ name: 'Extended Quickdraw Mag', quality: 'rare', level: 75 },
{ name: 'Compensator', quality: 'epic', level: 78 },
{ name: 'Vertical Grip', quality: 'rare', level: 72 }
]
},
{
name: 'ShadowNinja',
score: 7200,
equipments: [
{ name: 'M416', quality: 'epic', level: 75 },
{ name: 'Ghillie Suit', quality: 'rare', level: 70 },
{ name: 'Red Dot Sight', quality: 'common', level: 65 },
{ name: 'Extended QuickDraw Mag', quality: 'rare', level: 68 }
]
},
{
name: 'RuneMaster',
score: 9100,
equipments: [
{ name: 'KAR98K', quality: 'legendary', level: 90 },
{ name: 'Level 3 Vest', quality: 'legendary', level: 85 },
{ name: 'Holographic Sight', quality: 'epic', level: 82 },
{ name: 'Suppressor', quality: 'legendary', level: 88 },
{ name: 'Level 3 Backpack', quality: 'epic', level: 80 }
]
},
{
name: 'BattleKing',
score: 8500,
equipments: [
{ name: 'AUG', quality: 'epic', level: 82 },
{ name: 'Red Dot Sight', quality: 'rare', level: 75 },
{ name: 'Extended Mag', quality: 'common', level: 70 },
{ name: 'Tactical Stock', quality: 'rare', level: 76 }
]
},
{
name: 'SniperElite',
score: 7800,
equipments: [
{ name: 'M24', quality: 'legendary', level: 88 },
{ name: 'Compensator', quality: 'epic', level: 80 },
{ name: 'Scope 8x', quality: 'legendary', level: 85 },
{ name: 'Level 2 Helmet', quality: 'rare', level: 75 }
]
},
{
name: 'RushMaster',
score: 7500,
equipments: [
{ name: 'Vector', quality: 'rare', level: 72 },
{ name: 'Level 2 Helmet', quality: 'common', level: 65 },
{ name: 'Quickdraw Mag', quality: 'common', level: 60 },
{ name: 'Laser Sight', quality: 'rare', level: 68 }
]
},
{
name: 'GhostWarrior',
score: 8200,
equipments: [
{ name: 'SCAR-L', quality: 'epic', level: 78 },
{ name: 'Extended Quickdraw Mag', quality: 'rare', level: 70 },
{ name: 'Holographic Sight', quality: 'epic', level: 75 },
{ name: 'Suppressor', quality: 'rare', level: 72 },
{ name: 'Vertical Grip', quality: 'common', level: 65 }
]
},
{
name: 'DeathDealer',
score: 7300,
equipments: [
{ name: 'SKS', quality: 'epic', level: 76 },
{ name: 'Holographic Sight', quality: 'rare', level: 68 },
{ name: 'Extended Mag', quality: 'common', level: 65 }
]
},
{
name: 'StormRider',
score: 8900,
equipments: [
{ name: 'MK14', quality: 'legendary', level: 92 },
{ name: 'Level 3 Backpack', quality: 'legendary', level: 85 },
{ name: 'Scope 8x', quality: 'epic', level: 80 },
{ name: 'Suppressor', quality: 'legendary', level: 88 },
{ name: 'Tactical Stock', quality: 'rare', level: 75 }
]
},
{
name: 'CombatLegend',
score: 7600,
equipments: [
{ name: 'UMP45', quality: 'rare', level: 74 },
{ name: 'Level 2 Vest', quality: 'common', level: 67 },
{ name: 'Red Dot Sight', quality: 'common', level: 62 },
{ name: 'Extended Mag', quality: 'rare', level: 70 }
]
}
];
// Create a TreeMultiMap for player rankings
const playerRankings = new TreeMultiMap<number, Equipment, Player>(players, {
toEntryFn: ({ score, equipments }) => [score, equipments],
isMapMode: false
});
const topPlayersEquipments = playerRankings.rangeSearch([8900, 10000], node => playerRankings.get(node));
expect(topPlayersEquipments).toEqual([
[
{
name: 'MK14',
quality: 'legendary',
level: 92
},
{ name: 'Level 3 Backpack', quality: 'legendary', level: 85 },
{
name: 'Scope 8x',
quality: 'epic',
level: 80
},
{ name: 'Suppressor', quality: 'legendary', level: 88 },
{
name: 'Tactical Stock',
quality: 'rare',
level: 75
}
],
[
{ name: 'KAR98K', quality: 'legendary', level: 90 },
{
name: 'Level 3 Vest',
quality: 'legendary',
level: 85
},
{ name: 'Holographic Sight', quality: 'epic', level: 82 },
{
name: 'Suppressor',
quality: 'legendary',
level: 88
},
{ name: 'Level 3 Backpack', quality: 'epic', level: 80 }
]
]);
});
});