max-priority-queue-typed
Version:
Max Priority Queue
1,354 lines (1,227 loc) • 84.4 kB
text/typescript
/**
* data-structure-typed
*
* @author Pablo Zeng
* @copyright Copyright (c) 2022 Pablo Zeng <zrwusa@gmail.com>
* @license MIT License
*/
import type {
BinaryTreeDeleteResult,
BinaryTreeOptions,
BinaryTreePrintOptions,
BTNEntry,
DFSOrderPattern,
DFSStackItem,
EntryCallback,
FamilyPosition,
IterationType,
NodeCallback,
NodeDisplayLayout,
NodePredicate,
OptNodeOrNull,
RBTNColor,
ToEntryFn,
Trampoline
} from '../../types';
import { IBinaryTree } from '../../interfaces';
import { isComparable, makeTrampoline, makeTrampolineThunk } from '../../utils';
import { Queue } from '../queue';
import { IterableEntryBase } from '../base';
import { DFSOperation, Range } from '../../common';
/**
* @template K - The type of the key.
* @template V - The type of the value.
*/
export class BinaryTreeNode<K = any, V = any> {
key: K;
value?: V;
parent?: BinaryTreeNode<K, V> = undefined;
/**
* Creates an instance of BinaryTreeNode.
* @remarks Time O(1), Space O(1)
*
* @param key - The key of the node.
* @param [value] - The value associated with the key.
*/
constructor(key: K, value?: V) {
this.key = key;
this.value = value;
}
_left?: BinaryTreeNode<K, V> | null | undefined = undefined;
/**
* Gets the left child of the node.
* @remarks Time O(1), Space O(1)
*
* @returns The left child.
*/
get left(): BinaryTreeNode<K, V> | null | undefined {
return this._left;
}
/**
* Sets the left child of the node and updates its parent reference.
* @remarks Time O(1), Space O(1)
*
* @param v - The node to set as the left child.
*/
set left(v: BinaryTreeNode<K, V> | null | undefined) {
if (v) {
v.parent = this as unknown as BinaryTreeNode<K, V>;
}
this._left = v;
}
_right?: BinaryTreeNode<K, V> | null | undefined = undefined;
/**
* Gets the right child of the node.
* @remarks Time O(1), Space O(1)
*
* @returns The right child.
*/
get right(): BinaryTreeNode<K, V> | null | undefined {
return this._right;
}
/**
* Sets the right child of the node and updates its parent reference.
* @remarks Time O(1), Space O(1)
*
* @param v - The node to set as the right child.
*/
set right(v: BinaryTreeNode<K, V> | null | undefined) {
if (v) {
v.parent = this;
}
this._right = v;
}
_height: number = 0;
/**
* Gets the height of the node (used in self-balancing trees).
* @remarks Time O(1), Space O(1)
*
* @returns The height.
*/
get height(): number {
return this._height;
}
/**
* Sets the height of the node.
* @remarks Time O(1), Space O(1)
*
* @param value - The new height.
*/
set height(value: number) {
this._height = value;
}
_color: RBTNColor = 'BLACK';
/**
* Gets the color of the node (used in Red-Black trees).
* @remarks Time O(1), Space O(1)
*
* @returns The node's color.
*/
get color(): RBTNColor {
return this._color;
}
/**
* Sets the color of the node.
* @remarks Time O(1), Space O(1)
*
* @param value - The new color.
*/
set color(value: RBTNColor) {
this._color = value;
}
_count: number = 1;
/**
* Gets the count of nodes in the subtree rooted at this node (used in order-statistic trees).
* @remarks Time O(1), Space O(1)
*
* @returns The subtree node count.
*/
get count(): number {
return this._count;
}
/**
* Sets the count of nodes in the subtree.
* @remarks Time O(1), Space O(1)
*
* @param value - The new count.
*/
set count(value: number) {
this._count = value;
}
/**
* Gets the position of the node relative to its parent.
* @remarks Time O(1), Space O(1)
*
* @returns The family position (e.g., 'ROOT', 'LEFT', 'RIGHT').
*/
get familyPosition(): FamilyPosition {
if (!this.parent) {
return this.left || this.right ? 'ROOT' : 'ISOLATED';
}
if (this.parent.left === this) {
return this.left || this.right ? 'ROOT_LEFT' : 'LEFT';
} else if (this.parent.right === this) {
return this.left || this.right ? 'ROOT_RIGHT' : 'RIGHT';
}
return 'MAL_NODE';
}
}
/**
* A general Binary Tree implementation.
*
* @remarks
* This class implements a basic Binary Tree, not a Binary Search Tree.
* The `add` operation inserts nodes level-by-level (BFS) into the first available slot.
*
* @template K - The type of the key.
* @template V - The type of the value.
* @template R - The type of the raw data object (if using `toEntryFn`).
* 1. Two Children Maximum: Each node has at most two children.
* 2. Left and Right Children: Nodes have distinct left and right children.
* 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 extends object = object>
extends IterableEntryBase<K, V | undefined>
implements IBinaryTree<K, V, R>
{
iterationType: IterationType = 'ITERATIVE';
/**
* Creates an instance of BinaryTree.
* @remarks Time O(N * M), where N is the number of items in `keysNodesEntriesOrRaws` and M is the tree size at insertion time (due to O(M) `add` operation). Space O(N) for storing the nodes.
*
* @param [keysNodesEntriesOrRaws=[]] - An iterable of items to add.
* @param [options] - Configuration options for the tree.
*/
constructor(
keysNodesEntriesOrRaws: Iterable<
K | BinaryTreeNode<K, V> | [K | null | undefined, V | undefined] | null | undefined | R
> = [],
options?: BinaryTreeOptions<K, V, R>
) {
super();
if (options) {
const { iterationType, toEntryFn, isMapMode, isDuplicate } = options;
if (iterationType) this.iterationType = iterationType;
if (isMapMode !== undefined) this._isMapMode = isMapMode;
if (isDuplicate !== undefined) this._isDuplicate = isDuplicate;
if (typeof toEntryFn === 'function') this._toEntryFn = toEntryFn;
else if (toEntryFn) throw TypeError('toEntryFn must be a function type');
}
if (keysNodesEntriesOrRaws) this.addMany(keysNodesEntriesOrRaws);
}
protected _isMapMode = true;
/**
* Gets whether the tree is in Map mode.
* @remarks In Map mode (default), values are stored in an external Map, and nodes only hold keys. If false, values are stored directly on the nodes. Time O(1)
*
* @returns True if in Map mode, false otherwise.
*/
get isMapMode() {
return this._isMapMode;
}
protected _isDuplicate = false;
/**
* Gets whether the tree allows duplicate keys.
* @remarks Time O(1)
*
* @returns True if duplicates are allowed, false otherwise.
*/
get isDuplicate() {
return this._isDuplicate;
}
protected _store = new Map<K, V | undefined>();
/**
* Gets the external value store (used in Map mode).
* @remarks Time O(1)
*
* @returns The map storing key-value pairs.
*/
get store() {
return this._store;
}
protected _root?: BinaryTreeNode<K, V> | null | undefined;
/**
* Gets the root node of the tree.
* @remarks Time O(1)
*
* @returns The root node.
*/
get root(): BinaryTreeNode<K, V> | null | undefined {
return this._root;
}
protected _size: number = 0;
/**
* Gets the number of nodes in the tree.
* @remarks Time O(1)
*
* @returns The size of the tree.
*/
get size(): number {
return this._size;
}
protected _NIL: BinaryTreeNode<K, V> = new BinaryTreeNode<K, V>(NaN as K) as unknown as BinaryTreeNode<K, V>;
/**
* Gets the sentinel NIL node (used in self-balancing trees like Red-Black Tree).
* @remarks Time O(1)
*
* @returns The NIL node.
*/
get NIL(): BinaryTreeNode<K, V> {
return this._NIL;
}
protected _toEntryFn?: ToEntryFn<K, V, R>;
/**
* Gets the function used to convert raw data objects (R) into [key, value] entries.
* @remarks Time O(1)
*
* @returns The conversion function.
*/
get toEntryFn() {
return this._toEntryFn;
}
/**
* (Protected) Creates a new node.
* @remarks Time O(1), Space O(1)
*
* @param key - The key for the new node.
* @param [value] - The value for the new node (used if not in Map mode).
* @returns The newly created node.
*/
_createNode(key: K, value?: V): BinaryTreeNode<K, V> {
return new BinaryTreeNode<K, V>(key, this._isMapMode ? undefined : value);
}
/**
* Creates a new, empty tree of the same type and configuration.
* @remarks Time O(1) (excluding options cloning), Space O(1)
*
* @param [options] - Optional overrides for the new tree's options.
* @returns A new, empty tree instance.
*/
createTree(options?: Partial<BinaryTreeOptions<K, V, R>>): this {
return this._createInstance<K, V, R>(options);
}
/**
* Ensures the input is a node. If it's a key or entry, it searches for the node.
* @remarks Time O(1) if a node is passed. O(N) if a key or entry is passed (due to `getNode` performing a full search). Space O(1) if iterative search, O(H) if recursive (where H is height, O(N) worst-case).
*
* @param keyNodeOrEntry - The item to resolve to a node.
* @param [iterationType=this.iterationType] - The traversal method to use if searching.
* @returns The resolved node, or null/undefined if not found or input is null/undefined.
*/
ensureNode(
keyNodeOrEntry: K | BinaryTreeNode<K, V> | [K | null | undefined, V | undefined] | null | undefined,
iterationType: IterationType = this.iterationType
): BinaryTreeNode<K, V> | null | undefined {
if (keyNodeOrEntry === null) return null;
if (keyNodeOrEntry === undefined) return;
if (keyNodeOrEntry === this._NIL) return;
if (this.isNode(keyNodeOrEntry)) return keyNodeOrEntry;
if (this.isEntry(keyNodeOrEntry)) {
const key = keyNodeOrEntry[0];
if (key === null) return null;
if (key === undefined) return;
return this.getNode(key, this._root, iterationType);
}
return this.getNode(keyNodeOrEntry, this._root, iterationType);
}
/**
* Checks if the given item is a `BinaryTreeNode` instance.
* @remarks Time O(1), Space O(1)
*
* @param keyNodeOrEntry - The item to check.
* @returns True if it's a node, false otherwise.
*/
isNode(
keyNodeOrEntry: K | BinaryTreeNode<K, V> | [K | null | undefined, V | undefined] | null | undefined
): keyNodeOrEntry is BinaryTreeNode<K, V> {
return keyNodeOrEntry instanceof BinaryTreeNode;
}
/**
* Checks if the given item is a raw data object (R) that needs conversion via `toEntryFn`.
* @remarks Time O(1), Space O(1)
*
* @param keyNodeEntryOrRaw - The item to check.
* @returns True if it's a raw object, false otherwise.
*/
isRaw(
keyNodeEntryOrRaw: K | BinaryTreeNode<K, V> | [K | null | undefined, V | undefined] | null | undefined | R
): keyNodeEntryOrRaw is R {
return this._toEntryFn !== undefined && typeof keyNodeEntryOrRaw === 'object';
}
/**
* Checks if the given item is a "real" node (i.e., not null, undefined, or NIL).
* @remarks Time O(1), Space O(1)
*
* @param keyNodeOrEntry - The item to check.
* @returns True if it's a real node, false otherwise.
*/
isRealNode(
keyNodeOrEntry: K | BinaryTreeNode<K, V> | [K | null | undefined, V | undefined] | null | undefined
): keyNodeOrEntry is BinaryTreeNode<K, V> {
if (keyNodeOrEntry === this._NIL || keyNodeOrEntry === null || keyNodeOrEntry === undefined) return false;
return this.isNode(keyNodeOrEntry);
}
/**
* Checks if the given item is either a "real" node or null.
* @remarks Time O(1), Space O(1)
*
* @param keyNodeOrEntry - The item to check.
* @returns True if it's a real node or null, false otherwise.
*/
isRealNodeOrNull(
keyNodeOrEntry: K | BinaryTreeNode<K, V> | [K | null | undefined, V | undefined] | null | undefined
): keyNodeOrEntry is BinaryTreeNode<K, V> | null {
return keyNodeOrEntry === null || this.isRealNode(keyNodeOrEntry);
}
/**
* Checks if the given item is the sentinel NIL node.
* @remarks Time O(1), Space O(1)
*
* @param keyNodeOrEntry - The item to check.
* @returns True if it's the NIL node, false otherwise.
*/
isNIL(keyNodeOrEntry: K | BinaryTreeNode<K, V> | [K | null | undefined, V | undefined] | null | undefined): boolean {
return keyNodeOrEntry === this._NIL;
}
/**
* Checks if the given item is a `Range` object.
* @remarks Time O(1), Space O(1)
*
* @param keyNodeEntryOrPredicate - The item to check.
* @returns True if it's a Range, false otherwise.
*/
isRange(
keyNodeEntryOrPredicate:
| K
| BinaryTreeNode<K, V>
| [K | null | undefined, V | undefined]
| null
| undefined
| NodePredicate<BinaryTreeNode<K, V>>
| Range<K>
): keyNodeEntryOrPredicate is Range<K> {
return keyNodeEntryOrPredicate instanceof Range;
}
/**
* Checks if a node is a leaf (has no real children).
* @remarks Time O(N) if a key/entry is passed (due to `ensureNode`). O(1) if a node is passed. Space O(1) or O(H) (from `ensureNode`).
*
* @param keyNodeOrEntry - The node to check.
* @returns True if the node is a leaf, false otherwise.
*/
isLeaf(keyNodeOrEntry: K | BinaryTreeNode<K, V> | [K | null | undefined, V | undefined] | null | undefined): boolean {
keyNodeOrEntry = this.ensureNode(keyNodeOrEntry);
if (keyNodeOrEntry === undefined) return false;
if (keyNodeOrEntry === null) return true; // A null spot is considered a leaf
return !this.isRealNode(keyNodeOrEntry.left) && !this.isRealNode(keyNodeOrEntry.right);
}
/**
* Checks if the given item is a [key, value] entry pair.
* @remarks Time O(1), Space O(1)
*
* @param keyNodeOrEntry - The item to check.
* @returns True if it's an entry, false otherwise.
*/
isEntry(
keyNodeOrEntry: K | BinaryTreeNode<K, V> | [K | null | undefined, V | undefined] | null | undefined
): keyNodeOrEntry is BTNEntry<K, V> {
return Array.isArray(keyNodeOrEntry) && keyNodeOrEntry.length === 2;
}
/**
* Checks if the given key is valid (comparable or null).
* @remarks Time O(1), Space O(1)
*
* @param key - The key to validate.
* @returns True if the key is valid, false otherwise.
*/
isValidKey(key: any): key is K {
if (key === null) return true;
return isComparable(key);
}
/**
* Adds a new node to the tree.
* @remarks Time O(log N), For BST, Red-Black Tree, and AVL Tree subclasses, the worst-case time is O(log N). This implementation adds the node at the first available position in a level-order (BFS) traversal. This is NOT a Binary Search Tree insertion. Time O(N), where N is the number of nodes. It must traverse level-by-level to find an empty slot. Space O(N) in the worst case for the BFS queue (e.g., a full last level).
*
* @param keyNodeOrEntry - The key, node, or entry to add.
* @param [value] - The value, if providing just a key.
* @returns True if the addition was successful, false otherwise.
*/
add(
keyNodeOrEntry: K | BinaryTreeNode<K, V> | [K | null | undefined, V | undefined] | null | undefined,
value?: V
): boolean {
const [newNode, newValue] = this._keyValueNodeOrEntryToNodeAndValue(keyNodeOrEntry, value);
if (newNode === undefined) return false;
if (!this._root) {
this._setRoot(newNode);
if (this._isMapMode) this._setValue(newNode?.key, newValue);
this._size = 1;
return true;
}
const queue = new Queue<BinaryTreeNode<K, V>>([this._root]);
let potentialParent: BinaryTreeNode<K, V> | undefined;
while (queue.length > 0) {
const cur = queue.shift();
if (!cur) continue;
if (!this._isDuplicate) {
if (newNode !== null && cur.key === newNode.key) {
this._replaceNode(cur, newNode);
if (this._isMapMode) this._setValue(cur.key, newValue);
return true; // Replaced existing node
}
}
if (potentialParent === undefined && (cur.left === undefined || cur.right === undefined)) {
potentialParent = cur;
}
if (cur.left !== null) {
if (cur.left) queue.push(cur.left);
}
if (cur.right !== null) {
if (cur.right) queue.push(cur.right);
}
}
if (potentialParent) {
if (potentialParent.left === undefined) {
potentialParent.left = newNode;
} else if (potentialParent.right === undefined) {
potentialParent.right = newNode;
}
if (this._isMapMode) this._setValue(newNode?.key, newValue);
this._size++;
return true;
}
return false; // Should not happen if tree is not full?
}
/**
* Adds multiple items to the tree.
* @remarks Time O(N * M), where N is the number of items to add and M is the size of the tree at insertion (due to O(M) `add` operation). Space O(M) (from `add`) + O(N) (for the `inserted` array).
*
* @param keysNodesEntriesOrRaws - An iterable of items to add.
* @param [values] - An optional parallel iterable of values.
* @returns An array of booleans indicating the success of each individual `add` operation.
*/
addMany(
keysNodesEntriesOrRaws: Iterable<
K | BinaryTreeNode<K, V> | [K | null | undefined, V | undefined] | null | undefined | R
>,
values?: Iterable<V | undefined>
): boolean[] {
const inserted: boolean[] = [];
let valuesIterator: Iterator<V | undefined> | undefined;
if (values) {
valuesIterator = values[Symbol.iterator]();
}
for (let keyNodeEntryOrRaw of keysNodesEntriesOrRaws) {
let value: V | undefined | null = undefined;
if (valuesIterator) {
const valueResult = valuesIterator.next();
if (!valueResult.done) {
value = valueResult.value;
}
}
if (this.isRaw(keyNodeEntryOrRaw)) keyNodeEntryOrRaw = this._toEntryFn!(keyNodeEntryOrRaw);
inserted.push(this.add(keyNodeEntryOrRaw, value));
}
return inserted;
}
/**
* Merges another tree into this one by adding all its nodes.
* @remarks Time O(N * M), same as `addMany`, where N is the size of `anotherTree` and M is the size of this tree. Space O(M) (from `add`).
*
* @param anotherTree - The tree to merge.
*/
merge(anotherTree: BinaryTree<K, V, R>) {
this.addMany(anotherTree, []);
}
/**
* Clears the tree and refills it with new items.
* @remarks Time O(N) (for `clear`) + O(N * M) (for `addMany`) = O(N * M). Space O(M) (from `addMany`).
*
* @param keysNodesEntriesOrRaws - An iterable of items to add.
* @param [values] - An optional parallel iterable of values.
*/
refill(
keysNodesEntriesOrRaws: Iterable<
K | BinaryTreeNode<K, V> | [K | null | undefined, V | undefined] | null | undefined | R
>,
values?: Iterable<V | undefined>
): void {
this.clear();
this.addMany(keysNodesEntriesOrRaws, values);
}
/**
* Deletes a node from the tree.
* @remarks Time O(log N), For BST, Red-Black Tree, and AVL Tree subclasses, the worst-case time is O(log N). This implementation finds the node, and if it has two children, swaps it with the rightmost node of its left subtree (in-order predecessor) before deleting. Time O(N) in the worst case. O(N) to find the node (`getNode`) and O(H) (which is O(N) worst-case) to find the rightmost node. Space O(1) (if `getNode` is iterative, which it is).
*
* @param keyNodeOrEntry - The node to delete.
* @returns An array containing deletion results (for compatibility with self-balancing trees).
*/
delete(
keyNodeOrEntry: K | BinaryTreeNode<K, V> | [K | null | undefined, V | undefined] | null | undefined
): BinaryTreeDeleteResult<BinaryTreeNode<K, V>>[] {
const deletedResult: BinaryTreeDeleteResult<BinaryTreeNode<K, V>>[] = [];
if (!this._root) return deletedResult;
const curr = this.getNode(keyNodeOrEntry);
if (!curr) return deletedResult;
const parent: BinaryTreeNode<K, V> | undefined = curr?.parent;
let needBalanced: BinaryTreeNode<K, V> | undefined;
let orgCurrent: BinaryTreeNode<K, V> | undefined = curr;
if (!curr.left && !curr.right && !parent) {
// Deleting the root with no children
this._setRoot(undefined);
} else if (curr.left) {
// Node has a left child (or two children)
// Find the rightmost node in the left subtree
const leftSubTreeRightMost = this.getRightMost(node => node, curr.left);
if (leftSubTreeRightMost) {
const parentOfLeftSubTreeMax = leftSubTreeRightMost.parent;
// Swap properties
orgCurrent = this._swapProperties(curr, leftSubTreeRightMost);
// `orgCurrent` is now the node to be physically deleted (which was the rightmost)
if (parentOfLeftSubTreeMax) {
// Unlink the rightmost node
if (parentOfLeftSubTreeMax.right === leftSubTreeRightMost)
parentOfLeftSubTreeMax.right = leftSubTreeRightMost.left;
else parentOfLeftSubTreeMax.left = leftSubTreeRightMost.left;
needBalanced = parentOfLeftSubTreeMax;
}
}
} else if (parent) {
// Node has no left child, but has a parent
// Promote the right child (which could be null)
const { familyPosition: fp } = curr;
if (fp === 'LEFT' || fp === 'ROOT_LEFT') {
parent.left = curr.right;
} else if (fp === 'RIGHT' || fp === 'ROOT_RIGHT') {
parent.right = curr.right;
}
needBalanced = parent;
} else {
// Deleting the root, which has no left child
// Promote the right child as the new root
this._setRoot(curr.right);
curr.right = undefined;
}
this._size = this._size - 1;
deletedResult.push({ deleted: orgCurrent, needBalanced });
if (this._isMapMode && orgCurrent) this._store.delete(orgCurrent.key);
return deletedResult;
}
/**
* Searches the tree for nodes matching a predicate.
* @remarks Time O(log N), For BST, Red-Black Tree, and AVL Tree subclasses, the worst-case time is O(log N). Performs a full DFS (pre-order) scan of the tree. Time O(N), as it may visit every node. Space O(H) for the call stack (recursive) or explicit stack (iterative), where H is the tree height (O(N) worst-case).
*
* @template C - The type of the callback function.
* @param keyNodeEntryOrPredicate - The key, node, entry, or predicate function to search for.
* @param [onlyOne=false] - If true, stops after finding the first match.
* @param [callback=this._DEFAULT_NODE_CALLBACK] - A function to call on matching nodes.
* @param [startNode=this._root] - The node to start the search from.
* @param [iterationType=this.iterationType] - Whether to use 'RECURSIVE' or 'ITERATIVE' search.
* @returns An array of results from the callback function for each matching node.
*/
search<C extends NodeCallback<BinaryTreeNode<K, V> | null>>(
keyNodeEntryOrPredicate:
| K
| BinaryTreeNode<K, V>
| [K | null | undefined, V | undefined]
| null
| undefined
| NodePredicate<BinaryTreeNode<K, V> | null>,
onlyOne = false,
callback: C = this._DEFAULT_NODE_CALLBACK as C,
startNode: K | BinaryTreeNode<K, V> | [K | null | undefined, V | undefined] | null | undefined = this._root,
iterationType: IterationType = this.iterationType
): ReturnType<C>[] {
if (keyNodeEntryOrPredicate === undefined) return [];
if (keyNodeEntryOrPredicate === null) return [];
startNode = this.ensureNode(startNode);
if (!startNode) return [];
const predicate = this._ensurePredicate(keyNodeEntryOrPredicate);
const ans: ReturnType<C>[] = [];
if (iterationType === 'RECURSIVE') {
const dfs = (cur: BinaryTreeNode<K, V>) => {
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;
}
/**
* Gets all nodes matching a predicate.
* @remarks Time O(N) (via `search`). Space O(H) or O(N) (via `search`).
*
* @param keyNodeEntryOrPredicate - The key, node, entry, or predicate function to search for.
* @param [onlyOne=false] - If true, stops after finding the first match.
* @param [startNode=this._root] - The node to start the search from.
* @param [iterationType=this.iterationType] - The traversal method.
* @returns An array of matching nodes.
*/
getNodes(
keyNodeEntryOrPredicate:
| K
| BinaryTreeNode<K, V>
| [K | null | undefined, V | undefined]
| null
| undefined
| NodePredicate<BinaryTreeNode<K, V>>,
onlyOne?: boolean,
startNode?: K | BinaryTreeNode<K, V> | [K | null | undefined, V | undefined] | null | undefined,
iterationType?: IterationType
): BinaryTreeNode<K, V>[];
getNodes(
keyNodeEntryOrPredicate:
| K
| BinaryTreeNode<K, V>
| [K | null | undefined, V | undefined]
| null
| undefined
| NodePredicate<BinaryTreeNode<K, V> | null>,
onlyOne = false,
startNode: K | BinaryTreeNode<K, V> | [K | null | undefined, V | undefined] | null | undefined = this._root,
iterationType: IterationType = this.iterationType
): (BinaryTreeNode<K, V> | null)[] {
return this.search(keyNodeEntryOrPredicate, onlyOne, node => node, startNode, iterationType);
}
/**
* Gets the first node matching a predicate.
* @remarks Time O(log N), For BST, Red-Black Tree, and AVL Tree subclasses, the worst-case time is O(log N). Time O(N) in the worst case (via `search`). Space O(H) or O(N) (via `search`).
*
* @param keyNodeEntryOrPredicate - The key, node, entry, or predicate function to search for.
* @param [startNode=this._root] - The node to start the search from.
* @param [iterationType=this.iterationType] - The traversal method.
* @returns The first matching node, or undefined if not found.
*/
getNode(
keyNodeEntryOrPredicate:
| K
| BinaryTreeNode<K, V>
| [K | null | undefined, V | undefined]
| null
| undefined
| NodePredicate<BinaryTreeNode<K, V> | null>,
startNode: K | BinaryTreeNode<K, V> | [K | null | undefined, V | undefined] | null | undefined = this._root,
iterationType: IterationType = this.iterationType
): BinaryTreeNode<K, V> | null | undefined {
return this.search(keyNodeEntryOrPredicate, true, node => node, startNode, iterationType)[0];
}
/**
* Gets the value associated with a key.
* @remarks Time O(log N), For BST, Red-Black Tree, and AVL Tree subclasses, the worst-case time is O(log N). Time O(1) if in Map mode. O(N) if not in Map mode (uses `getNode`). Space O(1) if in Map mode. O(H) or O(N) otherwise.
*
* @param keyNodeEntryOrPredicate - The key, node, or entry to get the value for.
* @param [startNode=this._root] - The node to start searching from (if not in Map mode).
* @param [iterationType=this.iterationType] - The traversal method (if not in Map mode).
* @returns The associated value, or undefined.
*/
override get(
keyNodeEntryOrPredicate: K | BinaryTreeNode<K, V> | [K | null | undefined, V | undefined] | null | undefined,
startNode: K | BinaryTreeNode<K, V> | [K | null | undefined, V | undefined] | null | undefined = this._root,
iterationType: IterationType = this.iterationType
): V | undefined {
if (this._isMapMode) {
const key = this._extractKey(keyNodeEntryOrPredicate);
if (key === null || key === undefined) return;
return this._store.get(key);
}
return this.getNode(keyNodeEntryOrPredicate, startNode, iterationType)?.value;
}
/**
* Checks if a node matching the predicate exists in the tree.
* @remarks Time O(log N), For BST, Red-Black Tree, and AVL Tree subclasses, the worst-case time is O(log N). Time O(N) in the worst case (via `search`). Space O(H) or O(N) (via `search`).
*
* @param [keyNodeEntryOrPredicate] - The key, node, entry, or predicate to check for.
* @param [startNode] - The node to start the search from.
* @param [iterationType] - The traversal method.
* @returns True if a matching node exists, false otherwise.
*/
override has(
keyNodeEntryOrPredicate?:
| K
| BinaryTreeNode<K, V>
| [K | null | undefined, V | undefined]
| null
| undefined
| NodePredicate<BinaryTreeNode<K, V>>,
startNode?: K | BinaryTreeNode<K, V> | [K | null | undefined, V | undefined] | null | undefined,
iterationType?: IterationType
): boolean;
override has(
keyNodeEntryOrPredicate:
| K
| BinaryTreeNode<K, V>
| [K | null | undefined, V | undefined]
| null
| undefined
| NodePredicate<BinaryTreeNode<K, V> | null>,
startNode: K | BinaryTreeNode<K, V> | [K | null | undefined, V | undefined] | null | undefined = this._root,
iterationType: IterationType = this.iterationType
): boolean {
return this.search(keyNodeEntryOrPredicate, true, node => node, startNode, iterationType).length > 0;
}
/**
* Clears the tree of all nodes and values.
* @remarks Time O(N) if in Map mode (due to `_store.clear()`), O(1) otherwise. Space O(1)
*/
clear() {
this._clearNodes();
if (this._isMapMode) this._clearValues();
}
/**
* Checks if the tree is empty.
* @remarks Time O(1), Space O(1)
*
* @returns True if the tree has no nodes, false otherwise.
*/
isEmpty(): boolean {
return this._size === 0;
}
/**
* Checks if the tree is perfectly balanced.
* @remarks A tree is perfectly balanced if the difference between min and max height is at most 1. Time O(N), as it requires two full traversals (`getMinHeight` and `getHeight`). Space O(H) or O(N) (from height calculation).
*
* @param [startNode=this._root] - The node to start checking from.
* @returns True if perfectly balanced, false otherwise.
*/
isPerfectlyBalanced(
startNode: K | BinaryTreeNode<K, V> | [K | null | undefined, V | undefined] | null | undefined = this._root
): boolean {
return this.getMinHeight(startNode) + 1 >= this.getHeight(startNode);
}
/**
* Checks if the tree is a valid Binary Search Tree (BST).
* @remarks Time O(N), as it must visit every node. Space O(H) for the call stack (recursive) or explicit stack (iterative), where H is the tree height (O(N) worst-case).
*
* @param [startNode=this._root] - The node to start checking from.
* @param [iterationType=this.iterationType] - The traversal method.
* @returns True if it's a valid BST, false otherwise.
*/
isBST(
startNode: K | BinaryTreeNode<K, V> | [K | null | undefined, V | undefined] | null | undefined = this._root,
iterationType: IterationType = this.iterationType
): boolean {
const startNodeSired = this.ensureNode(startNode);
if (!startNodeSired) return true;
if (iterationType === 'RECURSIVE') {
const dfs = (cur: BinaryTreeNode<K, V> | null | undefined, min: number, max: number): boolean => {
if (!this.isRealNode(cur)) return true;
const numKey = Number(cur.key);
if (numKey <= min || numKey >= max) return false;
return dfs(cur.left, min, numKey) && dfs(cur.right, numKey, max);
};
const isStandardBST = dfs(startNodeSired, Number.MIN_SAFE_INTEGER, Number.MAX_SAFE_INTEGER);
const isInverseBST = dfs(startNodeSired, Number.MAX_SAFE_INTEGER, Number.MIN_SAFE_INTEGER); // Check for reverse BST
return isStandardBST || isInverseBST;
} else {
// Iterative in-order traversal check
const checkBST = (checkMax = false) => {
const stack: BinaryTreeNode<K, V>[] = [];
let prev = checkMax ? Number.MAX_SAFE_INTEGER : Number.MIN_SAFE_INTEGER;
let curr: BinaryTreeNode<K, V> | null | undefined = startNodeSired;
while (this.isRealNode(curr) || stack.length > 0) {
while (this.isRealNode(curr)) {
stack.push(curr);
curr = curr.left;
}
curr = stack.pop()!;
const numKey = Number(curr.key);
if (!this.isRealNode(curr) || (!checkMax && prev >= numKey) || (checkMax && prev <= numKey)) return false;
prev = numKey;
curr = curr.right;
}
return true;
};
const isStandardBST = checkBST(false);
const isInverseBST = checkBST(true);
return isStandardBST || isInverseBST;
}
}
/**
* Gets the depth of a node (distance from `startNode`).
* @remarks Time O(H), where H is the depth of the `dist` node relative to `startNode`. O(N) worst-case. Space O(1).
*
* @param dist - The node to find the depth of.
* @param [startNode=this._root] - The node to measure depth from (defaults to root).
* @returns The depth (0 if `dist` is `startNode`).
*/
getDepth(
dist: K | BinaryTreeNode<K, V> | [K | null | undefined, V | undefined] | null | undefined,
startNode: K | BinaryTreeNode<K, V> | [K | null | undefined, V | undefined] | null | undefined = this._root
): number {
let distEnsured = this.ensureNode(dist);
const beginRootEnsured = this.ensureNode(startNode);
let depth = 0;
while (distEnsured?.parent) {
if (distEnsured === beginRootEnsured) {
return depth;
}
depth++;
distEnsured = distEnsured.parent;
}
return depth;
}
/**
* Gets the maximum height of the tree (longest path from startNode to a leaf).
* @remarks Time O(N), as it must visit every node. Space O(H) for recursive stack (O(N) worst-case) or O(N) for iterative stack (storing node + depth).
*
* @param [startNode=this._root] - The node to start measuring from.
* @param [iterationType=this.iterationType] - The traversal method.
* @returns The height ( -1 for an empty tree, 0 for a single-node tree).
*/
getHeight(
startNode: K | BinaryTreeNode<K, V> | [K | null | undefined, V | undefined] | null | undefined = this._root,
iterationType: IterationType = this.iterationType
): number {
startNode = this.ensureNode(startNode);
if (!this.isRealNode(startNode)) return -1;
if (iterationType === 'RECURSIVE') {
const _getMaxHeight = (cur: BinaryTreeNode<K, V> | null | undefined): number => {
if (!this.isRealNode(cur)) return -1;
const leftHeight = _getMaxHeight(cur.left);
const rightHeight = _getMaxHeight(cur.right);
return Math.max(leftHeight, rightHeight) + 1;
};
return _getMaxHeight(startNode);
} else {
// Iterative (using DFS)
const stack: { node: BinaryTreeNode<K, V>; depth: number }[] = [{ node: startNode, depth: 0 }];
let maxHeight = 0;
while (stack.length > 0) {
const { node, depth } = stack.pop()!;
if (this.isRealNode(node.left)) stack.push({ node: node.left, depth: depth + 1 });
if (this.isRealNode(node.right)) stack.push({ node: node.right, depth: depth + 1 });
maxHeight = Math.max(maxHeight, depth);
}
return maxHeight;
}
}
/**
* Gets the minimum height of the tree (shortest path from startNode to a leaf).
* @remarks Time O(N), as it must visit every node. Space O(H) for recursive stack (O(N) worst-case) or O(N) for iterative (due to `depths` Map).
*
* @param [startNode=this._root] - The node to start measuring from.
* @param [iterationType=this.iterationType] - The traversal method.
* @returns The minimum height (-1 for empty, 0 for single node).
*/
getMinHeight(
startNode: K | BinaryTreeNode<K, V> | [K | null | undefined, V | undefined] | null | undefined = this._root,
iterationType: IterationType = this.iterationType
): number {
startNode = this.ensureNode(startNode);
if (!startNode) return -1;
if (iterationType === 'RECURSIVE') {
const _getMinHeight = (cur: BinaryTreeNode<K, V> | null | undefined): number => {
if (!this.isRealNode(cur)) return 0;
if (!this.isRealNode(cur.left) && !this.isRealNode(cur.right)) return 0; // Leaf node
const leftMinHeight = _getMinHeight(cur.left);
const rightMinHeight = _getMinHeight(cur.right);
return Math.min(leftMinHeight, rightMinHeight) + 1;
};
return _getMinHeight(startNode);
} else {
// Iterative (using post-order DFS)
const stack: BinaryTreeNode<K, V>[] = [];
let node: BinaryTreeNode<K, V> | null | undefined = startNode,
last: BinaryTreeNode<K, V> | null | undefined = null;
const depths: Map<BinaryTreeNode<K, V>, number> = new Map();
while (stack.length > 0 || node) {
if (this.isRealNode(node)) {
stack.push(node);
node = node.left;
} else {
node = stack[stack.length - 1];
if (!this.isRealNode(node.right) || last === node.right) {
node = stack.pop();
if (this.isRealNode(node)) {
const leftMinHeight = this.isRealNode(node.left) ? depths.get(node.left)! : -1;
const rightMinHeight = this.isRealNode(node.right) ? depths.get(node.right)! : -1;
depths.set(node, 1 + Math.min(leftMinHeight, rightMinHeight));
last = node;
node = null;
}
} else node = node.right;
}
}
return depths.get(startNode)!;
}
}
/**
* Gets the path from a given node up to the root.
* @remarks Time O(H), where H is the depth of the `beginNode`. O(N) worst-case. Space O(H) for the result array.
*
* @template C - The type of the callback function.
* @param beginNode - The node to start the path from.
* @param [callback=this._DEFAULT_NODE_CALLBACK] - A function to call on each node in the path.
* @param [isReverse=false] - If true, returns the path from root-to-node.
* @returns An array of callback results.
*/
getPathToRoot<C extends NodeCallback<BinaryTreeNode<K, V> | undefined>>(
beginNode: K | BinaryTreeNode<K, V> | [K | null | undefined, V | undefined] | null | undefined,
callback: C = this._DEFAULT_NODE_CALLBACK as C,
isReverse = false
): ReturnType<C>[] {
const result: ReturnType<C>[] = [];
let beginNodeEnsured = this.ensureNode(beginNode);
if (!beginNodeEnsured) return result;
while (beginNodeEnsured.parent) {
result.push(callback(beginNodeEnsured));
beginNodeEnsured = beginNodeEnsured.parent;
}
result.push(callback(beginNodeEnsured)); // Add the root
return isReverse ? result.reverse() : result;
}
/**
* Finds the leftmost node in a subtree (the node with the smallest key in a BST).
* @remarks Time O(H), where H is the height of the left spine. O(N) worst-case. Space O(H) for recursive/trampoline stack.
*
* @template C - The type of the callback function.
* @param [callback=this._DEFAULT_NODE_CALLBACK] - A function to call on the leftmost node.
* @param [startNode=this._root] - The subtree root to search from.
* @param [iterationType=this.iterationType] - The traversal method.
* @returns The callback result for the leftmost node.
*/
getLeftMost<C extends NodeCallback<BinaryTreeNode<K, V> | undefined>>(
callback: C = this._DEFAULT_NODE_CALLBACK as C,
startNode: K | BinaryTreeNode<K, V> | [K | null | undefined, V | undefined] | null | undefined = this._root,
iterationType: IterationType = this.iterationType
): ReturnType<C> {
if (this.isNIL(startNode)) return callback(undefined);
startNode = this.ensureNode(startNode);
if (!this.isRealNode(startNode)) return callback(undefined);
if (iterationType === 'RECURSIVE') {
const dfs = (cur: BinaryTreeNode<K, V>): BinaryTreeNode<K, V> => {
const { left } = cur;
if (!this.isRealNode(left)) return cur;
return dfs(left);
};
return callback(dfs(startNode));
} else {
// Iterative (trampolined to prevent stack overflow, though 'ITERATIVE' usually means a loop)
const dfs = makeTrampoline((cur: BinaryTreeNode<K, V>): Trampoline<BinaryTreeNode<K, V>> => {
const { left } = cur;
if (!this.isRealNode(left)) return cur;
return makeTrampolineThunk(() => dfs(left));
});
return callback(dfs(startNode));
}
}
/**
* Finds the rightmost node in a subtree (the node with the largest key in a BST).
* @remarks Time O(H), where H is the height of the right spine. O(N) worst-case. Space O(H) for recursive/trampoline stack.
*
* @template C - The type of the callback function.
* @param [callback=this._DEFAULT_NODE_CALLBACK] - A function to call on the rightmost node.
* @param [startNode=this._root] - The subtree root to search from.
* @param [iterationType=this.iterationType] - The traversal method.
* @returns The callback result for the rightmost node.
*/
getRightMost<C extends NodeCallback<BinaryTreeNode<K, V> | undefined>>(
callback: C = this._DEFAULT_NODE_CALLBACK as C,
startNode: K | BinaryTreeNode<K, V> | [K | null | undefined, V | undefined] | null | undefined = this._root,
iterationType: IterationType = this.iterationType
): ReturnType<C> {
if (this.isNIL(startNode)) return callback(undefined);
startNode = this.ensureNode(startNode);
if (!startNode) return callback(undefined);
if (iterationType === 'RECURSIVE') {
const dfs = (cur: BinaryTreeNode<K, V>): BinaryTreeNode<K, V> => {
const { right } = cur;
if (!this.isRealNode(right)) return cur;
return dfs(right);
};
return callback(dfs(startNode));
} else {
const dfs = makeTrampoline((cur: BinaryTreeNode<K, V>): Trampoline<BinaryTreeNode<K, V>> => {
const { right } = cur;
if (!this.isRealNode(right)) return cur;
return makeTrampolineThunk(() => dfs(right));
});
return callback(dfs(startNode));
}
}
/**
* Gets the Morris traversal predecessor (rightmost node in the left subtree, or node itself).
* @remarks This is primarily a helper for Morris traversal. Time O(H), where H is the height of the left subtree. O(N) worst-case. Space O(1).
*
* @param node - The node to find the predecessor for.
* @returns The Morris predecessor.
*/
getPredecessor(node: BinaryTreeNode<K, V>): BinaryTreeNode<K, V> {
if (this.isRealNode(node.left)) {
let predecessor: BinaryTreeNode<K, V> | null | undefined = node.left;
while (!this.isRealNode(predecessor) || (this.isRealNode(predecessor.right) && predecessor.right !== node)) {
if (this.isRealNode(predecessor)) {
predecessor = predecessor.right;
}
}
return predecessor;
} else {
return node;
}
}
/**
* Gets the in-order successor of a node in a BST.
* @remarks Time O(H), where H is the tree height. O(N) worst-case. Space O(H) (due to `getLeftMost` stack).
*
* @param [x] - The node to find the successor of.
* @returns The successor node, or null/undefined if none exists.
*/
getSuccessor(x?: K | BinaryTreeNode<K, V> | null): BinaryTreeNode<K, V> | null | undefined {
x = this.ensureNode(x);
if (!this.isRealNode(x)) return undefined;
if (this.isRealNode(x.right)) {
return this.getLeftMost(node => node, x.right);
}
let y: BinaryTreeNode<K, V> | null | undefined = x.parent;
while (this.isRealNode(y) && x === y.right) {
x = y;
y = y.parent;
}
return y;
}
dfs<C extends NodeCallback<BinaryTreeNode<K, V>>>(
callback?: C,
pattern?: DFSOrderPattern,
onlyOne?: boolean,
startNode?: K | BinaryTreeNode<K, V> | [K | null | undefined, V | undefined] | null | undefined,
iterationType?: IterationType
): ReturnType<C>[];
dfs<C extends NodeCallback<BinaryTreeNode<K, V> | null>>(
callback?: C,
pattern?: DFSOrderPattern,
onlyOne?: boolean,
startNode?: K | BinaryTreeNode<K, V> | [K | null | undefined, V | undefined] | null | undefined,
iterationType?: IterationType,
includeNull?: boolean
): ReturnType<C>[];
/**
* Performs a Depth-First Search (DFS) traversal.
* @remarks Time O(N), visits every node. Space O(H) for the call/explicit stack. O(N) worst-case.
*
* @template C - The type of the callback function.
* @param [callback=this._DEFAULT_NODE_CALLBACK] - Function to call on each node.
* @param [pattern='IN'] - The traversal order ('IN', 'PRE', 'POST').
* @param [onlyOne=false] - If true, stops after the first callback.
* @param [startNode=this._root] - The node to start from.
* @param [iterationType=this.iterationType] - The traversal method.
* @param [includeNull=false] - If true, includes null nodes in the traversal.
* @returns An array of callback results.
*/
dfs<C extends NodeCallback<BinaryTreeNode<K, V> | undefined>>(
callback: C = this._DEFAULT_NODE_CALLBACK as C,
pattern: DFSOrderPattern = 'IN',
onlyOne: boolean = false,
startNode: K | BinaryTreeNode<K, V> | [K | null | undefined, V | undefined] | null | undefined = this._root,
iterationType: IterationType = this.iterationType,
includeNull = false
): ReturnType<C>[] {
startNode = this.ensureNode(startNode);
if (!startNode) return [];
return this._dfs(callback, pattern, onlyOne, startNode, iterationType, includeNull);
}
bfs<C extends NodeCallback<BinaryTreeNode<K, V>>>(
callback?: C,
startNode?: K | BinaryTreeNode<K, V> | [K | null | undefined, V | undefined] | null | undefined,
iterationType?: IterationType,
includeNull?: false
): ReturnType<C>[];
bfs<C extends NodeCallback<BinaryTreeNode<K, V> | null>>(
callback?: C,
startNode?: K | BinaryTreeNode<K, V> | [K | null | undefined, V | undefined] | null | undefined,
iterationType?: IterationType,
includeNull?: true
): ReturnType<C>[];
/**
* Performs a Breadth-First Search (BFS) or Level-Order traversal.
* @remarks Time O(N), visits every node. Space O(N) in the worst case for the queue (e.g., a full last level).
*
* @template C - The type of the callback function.
* @param [callback=this._DEFAULT_NODE_CALLBACK] - Function to call on each node.
* @param [startNode=this._root] - The node to start from.
* @param [iterationType=this.iterationType] - The traversal method ('RECURSIVE' BFS is less common but supported here).
* @param [includeNull=false] - If true, includes null nodes in the traversal.
* @returns An array of callback results.
*/
bfs<C extends