UNPKG

@flatten-js/interval-tree

Version:
878 lines (873 loc) 30.3 kB
/** * Created by Alex Bol on 4/1/2017. */ // Abstract base for intervals. Concrete variants extend this. class IntervalBase { constructor(low, high) { this.low = low; this.high = high; } get max() { return this.clone(); } // Default numeric/date comparison (lexicographic by low then high) less_than(other_interval) { return this.low < other_interval.low || (this.low === other_interval.low && this.high < other_interval.high); } equal_to(other_interval) { return this.low === other_interval.low && this.high === other_interval.high; } intersect(other_interval) { return !this.not_intersect(other_interval); } not_intersect(other_interval) { return (this.high < other_interval.low || other_interval.high < this.low); } merge(other_interval) { // By default choose min low, max high using < and > const low = (this.low === undefined) ? other_interval.low : ((this.low < other_interval.low) ? this.low : other_interval.low); const high = (this.high === undefined) ? other_interval.high : ((this.high > other_interval.high) ? this.high : other_interval.high); // Return instance of the same concrete class const cloned = this.clone(); cloned.low = low; cloned.high = high; return cloned; } output() { return [this.low, this.high]; } // Instance-level comparator so child classes can customize value comparison semantics comparable_less_than(val1, val2) { return val1 < val2; } } // 1D numeric/date interval (default) class Interval extends IntervalBase { clone() { return new Interval(this.low, this.high); } } // 2D interval with lexicographic comparison for points [x, y] class Interval2D extends IntervalBase { constructor(low, high) { super(low, high); } static pointLess(a, b) { return a[0] < b[0] || (a[0] === b[0] && a[1] < b[1]); } static pointEq(a, b) { return a[0] === b[0] && a[1] === b[1]; } clone() { return new Interval2D(this.low, this.high); } less_than(other) { const a = this.low; const b = other.low; if (Interval2D.pointLess(a, b)) return true; if (Interval2D.pointEq(a, b)) { const ah = this.high; const bh = other.high; return Interval2D.pointLess(ah, bh); } return false; } equal_to(other) { return Interval2D.pointEq(this.low, other.low) && Interval2D.pointEq(this.high, other.high); } not_intersect(other) { // Non-intersection in lexicographic 2D ordering (simplistic): treat ranges in the ordered space const highLess = Interval2D.pointLess(this.high, other.low); const otherHighLess = Interval2D.pointLess(other.high, this.low); return highLess || otherHighLess; } merge(other) { const lowA = this.low; const lowB = other.low; const highA = this.high; const highB = other.high; const low = Interval2D.pointLess(lowA, lowB) ? lowA : lowB; const high = Interval2D.pointLess(highA, highB) ? highB : highA; return new Interval2D(low, high); } // Override value comparator to handle 2D points lexicographically comparable_less_than(val1, val2) { return Interval2D.pointLess(val1, val2); } output() { return [this.low, this.high]; } } /** * Created by Alex Bol on 3/28/2017. */ // module.exports = { // RB_TREE_COLOR_RED: 0, // RB_TREE_COLOR_BLACK: 1 // }; /** * Red-Black Tree color constants */ const RB_TREE_COLOR_RED = 1; const RB_TREE_COLOR_BLACK = 0; /** * Created by Alex Bol on 4/1/2017. */ class Node { constructor(key, value, left = null, right = null, parent = null, color = RB_TREE_COLOR_BLACK) { this.left = left; this.right = right; this.parent = parent; this.color = color; this.item = { key: undefined, values: [] }; if (value !== undefined) { this.item.values.push(value); } // Initialize key if provided if (key !== undefined) { if (Array.isArray(key)) { const [rawLow, rawHigh] = key; if (!Number.isNaN(rawLow) && !Number.isNaN(rawHigh)) { let low = rawLow; let high = rawHigh; if (low > high) [low, high] = [high, low]; this.item.key = new Interval(low, high); } } else { // Assume a concrete IntervalBase implementation was passed this.item.key = key; } } this.max = this.item.key ? this.item.key.max : undefined; } isNil() { return (this.item.key === undefined && this.item.values.length === 0 && this.left === null && this.right === null && this.color === RB_TREE_COLOR_BLACK); } requireKey() { if (!this.item.key) { throw new Error('Node key is undefined (nil/sentinel). Operation is not applicable.'); } return this.item.key; } less_than(other_node) { // Compare nodes by key only; values are stored in a bucket const a = this.requireKey(); const b = other_node.requireKey(); return a.less_than(b); } _value_equal(other_node) { // Deprecated in bucket mode; kept for backward compatibility if ever used // Compare first elements if exist const a = this.item.values[0]; const b = other_node.item.values[0]; return a && b && a.equal_to ? a.equal_to(b) : a === b; } equal_to(other_node) { // Nodes are equal if keys are equal; values are kept in a bucket const a = this.requireKey(); const b = other_node.requireKey(); return a.equal_to(b); } intersect(other_node) { const a = this.requireKey(); const b = other_node.requireKey(); return a.intersect(b); } copy_data(other_node) { this.item.key = other_node.item.key; this.item.values = other_node.item.values.slice(); } update_max() { // use key (Interval) max property instead of key.high this.max = this.item.key ? this.item.key.max : undefined; if (this.right && this.right.max) { this.max = this.max ? this.max.merge(this.right.max) : this.right.max; } if (this.left && this.left.max) { this.max = this.max ? this.max.merge(this.left.max) : this.left.max; } } // Other_node does not intersect any node of left subtree not_intersect_left_subtree(search_node) { if (!this.left) return true; const high = this.left.max ? this.left.max.high : this.left.item.key.high; const selfKey = this.requireKey(); const searchKey = search_node.requireKey(); return selfKey.comparable_less_than(high, searchKey.low); } // Other_node does not intersect right subtree not_intersect_right_subtree(search_node) { if (!this.right) return true; const low = this.right.max ? this.right.max.low : this.right.item.key.low; const selfKey = this.requireKey(); const searchKey = search_node.requireKey(); return selfKey.comparable_less_than(searchKey.high, low); } } /** * Created by Alex Bol on 3/31/2017. */ /** * Implementation of interval binary search tree * Interval tree stores items which are couples of {key:interval, value: value} * Interval is an object with high and low properties or simply pair [low,high] of numeric values */ class IntervalTree { /** * Construct new empty instance of IntervalTree */ constructor() { this.root = null; this.nil_node = new Node(); } /** * Returns number of items stored in the interval tree * @returns {number} */ get size() { let count = 0; this.tree_walk(this.root, (node) => count += node.item.values.length); return count; } /** * Returns array of sorted keys in the ascending order * @returns {Array} */ get keys() { const res = []; this.tree_walk(this.root, (node) => res.push(node.item.key.output())); return res; } /** * Return array of values in the ascending keys order * @returns {Array} */ get values() { const res = []; this.tree_walk(this.root, (node) => { for (const v of node.item.values) res.push(v); }); return res; } /** * Returns array of items (<key,value> pairs) in the ascended keys order * @returns {Array} */ get items() { const res = []; this.tree_walk(this.root, (node) => { const keyOut = node.item.key.output(); for (const v of node.item.values) { res.push({ key: keyOut, value: v }); } }); return res; } /** * Returns true if tree is empty * @returns {boolean} */ isEmpty() { return this.root == null || this.root === this.nil_node; } /** * Clear tree */ clear() { this.root = null; } /** * Insert new item into interval tree * @param key - interval object or array of two numbers [low, high] * @param value - value representing any object (optional) * @returns returns reference to inserted node */ insert(key, value = key) { if (key === undefined) return; // If node with the same key exists, append value to its bucket const existing = this.tree_search(this.root, new Node(key)); if (existing) { existing.item.values.push(value); return existing; } const insert_node = new Node(key, value, this.nil_node, this.nil_node, null, RB_TREE_COLOR_RED); this.tree_insert(insert_node); this.recalc_max(insert_node); return insert_node; } /** * Returns true if item {key,value} exist in the tree * @param key - interval correspondent to keys stored in the tree * @param value - value object to be checked * @returns true if item {key, value} exist in the tree, false otherwise */ exist(key, value = key) { const node = this.tree_search(this.root, new Node(key)); if (!node) return false; // If value is omitted (or equals key by default), treat as key existence if (arguments.length < 2 || value === key) return true; // Check if value exists in the bucket return node.item.values.some((v) => (v && v.equal_to ? v.equal_to(value) : v === value)); } /** * Remove entry {key, value} from the tree * @param key - interval correspondent to keys stored in the tree * @param value - value object * @returns deleted node or undefined if not found */ remove(key, value = key) { const node = this.tree_search(this.root, new Node(key)); if (!node) return undefined; // If value omitted, remove entire node if (arguments.length < 2) { this.tree_delete(node); return node; } // Remove one matching value from bucket const idx = node.item.values.findIndex((v) => (v && v.equal_to ? v.equal_to(value) : v === value)); if (idx >= 0) { node.item.values.splice(idx, 1); // If bucket is now empty, remove node from tree if (node.item.values.length === 0) { this.tree_delete(node); } return node; } return undefined; } search(interval, outputMapperFn = (value, key) => value === key ? key.output() : value) { const search_node = new Node(interval); const resp_nodes = []; this.tree_search_interval(this.root, search_node, resp_nodes); const res = []; for (const node of resp_nodes) { for (const v of node.item.values) { res.push(outputMapperFn(v, node.item.key)); } } return res; } /** * Returns true if intersection between given and any interval stored in the tree found * @param interval - search interval or tuple [low, high] * @returns {boolean} */ intersect_any(interval) { const search_node = new Node(interval); return this.tree_find_any_interval(this.root, search_node); } /** * Tree visitor. For each node implement a callback function. * Method calls a callback function with two parameters (key, value) * @param visitor - function to be called for each tree item */ forEach(visitor) { this.tree_walk(this.root, (node) => { for (const v of node.item.values) visitor(node.item.key, v); }); } /** * Value Mapper. Walk through every node and map node value to another value * @param callback - function to be called for each tree item */ map(callback) { const tree = new IntervalTree(); this.tree_walk(this.root, (node) => { for (const v of node.item.values) { tree.insert(node.item.key, callback(v, node.item.key)); } }); return tree; } *iterate(interval, outputMapperFn = (value, key) => value === key ? key.output() : value) { let node = null; if (interval) { node = this.tree_search_nearest_forward(this.root, new Node(interval)); } else if (this.root) { node = this.local_minimum(this.root); } while (node) { for (const v of node.item.values) { yield outputMapperFn(v, node.item.key); } node = this.tree_successor(node); } } /** * Recalculate max property upward from given node to root * @param node - starting node */ recalc_max(node) { let node_current = node; while (node_current.parent != null) { node_current.parent.update_max(); node_current = node_current.parent; } } /** * Insert node into tree and rebalance * @param insert_node - node to insert */ tree_insert(insert_node) { let current_node = this.root; let parent_node = null; if (this.root == null || this.root === this.nil_node) { this.root = insert_node; } else { while (current_node !== this.nil_node) { parent_node = current_node; if (insert_node.less_than(current_node)) { current_node = current_node.left; } else { current_node = current_node.right; } } insert_node.parent = parent_node; if (insert_node.less_than(parent_node)) { parent_node.left = insert_node; } else { parent_node.right = insert_node; } } this.insert_fixup(insert_node); } /** * Restore red-black tree properties after insertion * @param insert_node - inserted node */ insert_fixup(insert_node) { let current_node; let uncle_node; current_node = insert_node; while (current_node !== this.root && current_node.parent.color === RB_TREE_COLOR_RED) { if (current_node.parent === current_node.parent.parent.left) { uncle_node = current_node.parent.parent.right; if (uncle_node.color === RB_TREE_COLOR_RED) { current_node.parent.color = RB_TREE_COLOR_BLACK; uncle_node.color = RB_TREE_COLOR_BLACK; current_node.parent.parent.color = RB_TREE_COLOR_RED; current_node = current_node.parent.parent; } else { if (current_node === current_node.parent.right) { current_node = current_node.parent; this.rotate_left(current_node); } current_node.parent.color = RB_TREE_COLOR_BLACK; current_node.parent.parent.color = RB_TREE_COLOR_RED; this.rotate_right(current_node.parent.parent); } } else { uncle_node = current_node.parent.parent.left; if (uncle_node.color === RB_TREE_COLOR_RED) { current_node.parent.color = RB_TREE_COLOR_BLACK; uncle_node.color = RB_TREE_COLOR_BLACK; current_node.parent.parent.color = RB_TREE_COLOR_RED; current_node = current_node.parent.parent; } else { if (current_node === current_node.parent.left) { current_node = current_node.parent; this.rotate_right(current_node); } current_node.parent.color = RB_TREE_COLOR_BLACK; current_node.parent.parent.color = RB_TREE_COLOR_RED; this.rotate_left(current_node.parent.parent); } } } this.root.color = RB_TREE_COLOR_BLACK; } /** * Delete node from tree and rebalance * @param delete_node - node to delete */ tree_delete(delete_node) { let cut_node; let fix_node; if (delete_node.left === this.nil_node || delete_node.right === this.nil_node) { cut_node = delete_node; } else { cut_node = this.tree_successor(delete_node); } if (cut_node.left !== this.nil_node) { fix_node = cut_node.left; } else { fix_node = cut_node.right; } fix_node.parent = cut_node.parent; if (cut_node === this.root) { this.root = fix_node; } else { if (cut_node === cut_node.parent.left) { cut_node.parent.left = fix_node; } else { cut_node.parent.right = fix_node; } cut_node.parent.update_max(); } this.recalc_max(fix_node); if (cut_node !== delete_node) { delete_node.copy_data(cut_node); delete_node.update_max(); this.recalc_max(delete_node); } if (cut_node.color === RB_TREE_COLOR_BLACK) { this.delete_fixup(fix_node); } } /** * Restore red-black tree properties after deletion * @param fix_node - node to fix from */ delete_fixup(fix_node) { let current_node = fix_node; let brother_node; while (current_node !== this.root && current_node.parent != null && current_node.color === RB_TREE_COLOR_BLACK) { if (current_node === current_node.parent.left) { brother_node = current_node.parent.right; if (brother_node.color === RB_TREE_COLOR_RED) { brother_node.color = RB_TREE_COLOR_BLACK; current_node.parent.color = RB_TREE_COLOR_RED; this.rotate_left(current_node.parent); brother_node = current_node.parent.right; } if (brother_node.left.color === RB_TREE_COLOR_BLACK && brother_node.right.color === RB_TREE_COLOR_BLACK) { brother_node.color = RB_TREE_COLOR_RED; current_node = current_node.parent; } else { if (brother_node.right.color === RB_TREE_COLOR_BLACK) { brother_node.color = RB_TREE_COLOR_RED; brother_node.left.color = RB_TREE_COLOR_BLACK; this.rotate_right(brother_node); brother_node = current_node.parent.right; } brother_node.color = current_node.parent.color; current_node.parent.color = RB_TREE_COLOR_BLACK; brother_node.right.color = RB_TREE_COLOR_BLACK; this.rotate_left(current_node.parent); current_node = this.root; } } else { brother_node = current_node.parent.left; if (brother_node.color === RB_TREE_COLOR_RED) { brother_node.color = RB_TREE_COLOR_BLACK; current_node.parent.color = RB_TREE_COLOR_RED; this.rotate_right(current_node.parent); brother_node = current_node.parent.left; } if (brother_node.left.color === RB_TREE_COLOR_BLACK && brother_node.right.color === RB_TREE_COLOR_BLACK) { brother_node.color = RB_TREE_COLOR_RED; current_node = current_node.parent; } else { if (brother_node.left.color === RB_TREE_COLOR_BLACK) { brother_node.color = RB_TREE_COLOR_RED; brother_node.right.color = RB_TREE_COLOR_BLACK; this.rotate_left(brother_node); brother_node = current_node.parent.left; } brother_node.color = current_node.parent.color; current_node.parent.color = RB_TREE_COLOR_BLACK; brother_node.left.color = RB_TREE_COLOR_BLACK; this.rotate_right(current_node.parent); current_node = this.root; } } } current_node.color = RB_TREE_COLOR_BLACK; } /** * Search for node with given key and value * @param node - starting node * @param search_node - node to search for * @returns found node or undefined */ tree_search(node, search_node) { if (node == null || node === this.nil_node) return undefined; if (search_node.equal_to(node)) { return node; } if (search_node.less_than(node)) { return this.tree_search(node.left, search_node); } else { return this.tree_search(node.right, search_node); } } /** * Find nearest forward node from given interval * @param node - starting node * @param search_node - search interval as node * @returns nearest forward node or null */ tree_search_nearest_forward(node, search_node) { let best = null; let curr = node; while (curr && curr !== this.nil_node) { if (curr.less_than(search_node)) { if (curr.intersect(search_node)) { best = curr; curr = curr.left; } else { curr = curr.right; } } else { if (!best || curr.less_than(best)) best = curr; curr = curr.left; } } return best || null; } /** * Search all intervals intersecting given interval * @param node - starting node * @param search_node - search interval as node * @param res - result array to collect found nodes */ tree_search_interval(node, search_node, res) { if (node != null && node !== this.nil_node) { if (node.left !== this.nil_node && !node.not_intersect_left_subtree(search_node)) { this.tree_search_interval(node.left, search_node, res); } if (node.intersect(search_node)) { res.push(node); } if (node.right !== this.nil_node && !node.not_intersect_right_subtree(search_node)) { this.tree_search_interval(node.right, search_node, res); } } } /** * Check if any interval intersects with given interval * @param node - starting node * @param search_node - search interval as node * @returns true if intersection found */ tree_find_any_interval(node, search_node) { let found = false; if (node != null && node !== this.nil_node) { if (node.left !== this.nil_node && !node.not_intersect_left_subtree(search_node)) { found = this.tree_find_any_interval(node.left, search_node); } if (!found) { found = node.intersect(search_node); } if (!found && node.right !== this.nil_node && !node.not_intersect_right_subtree(search_node)) { found = this.tree_find_any_interval(node.right, search_node); } } return found; } /** * Find node with minimum key in subtree * @param node - root of subtree * @returns node with minimum key */ local_minimum(node) { let node_min = node; while (node_min.left != null && node_min.left !== this.nil_node) { node_min = node_min.left; } return node_min; } /** * Find node with maximum key in subtree * @param node - root of subtree * @returns node with maximum key */ local_maximum(node) { let node_max = node; while (node_max.right != null && node_max.right !== this.nil_node) { node_max = node_max.right; } return node_max; } /** * Find successor node (next in sorted order) * @param node - current node * @returns successor node or null */ tree_successor(node) { let node_successor; let current_node; let parent_node; if (node.right !== this.nil_node) { node_successor = this.local_minimum(node.right); } else { current_node = node; parent_node = node.parent; while (parent_node != null && parent_node.right === current_node) { current_node = parent_node; parent_node = parent_node.parent; } node_successor = parent_node; } return node_successor; } /** * Left rotation around node x * @param x - node to rotate */ rotate_left(x) { const y = x.right; x.right = y.left; if (y.left !== this.nil_node) { y.left.parent = x; } y.parent = x.parent; if (x === this.root) { this.root = y; } else { if (x === x.parent.left) { x.parent.left = y; } else { x.parent.right = y; } } y.left = x; x.parent = y; if (x !== null && x !== this.nil_node) { x.update_max(); } if (y != null && y !== this.nil_node) { y.update_max(); } } /** * Right rotation around node y * @param y - node to rotate */ rotate_right(y) { const x = y.left; y.left = x.right; if (x.right !== this.nil_node) { x.right.parent = y; } x.parent = y.parent; if (y === this.root) { this.root = x; } else { if (y === y.parent.left) { y.parent.left = x; } else { y.parent.right = x; } } x.right = y; y.parent = x; if (y !== null && y !== this.nil_node) { y.update_max(); } if (x != null && x !== this.nil_node) { x.update_max(); } } /** * Performs in-order traversal of the tree * Applies action callback to each node in ascending order of keys * @param node - starting node for traversal (typically root) * @param action - callback function to be executed for each node */ tree_walk(node, action) { if (node != null && node !== this.nil_node) { this.tree_walk(node.left, action); action(node); this.tree_walk(node.right, action); } } /** * Test red-black tree property: all red nodes have exactly two black child nodes * @returns true if property holds */ testRedBlackProperty() { let res = true; this.tree_walk(this.root, function (node) { if (node.color === RB_TREE_COLOR_RED) { if (!(node.left.color === RB_TREE_COLOR_BLACK && node.right.color === RB_TREE_COLOR_BLACK)) { res = false; } } }); return res; } /** * Test red-black tree property: every path from root to leaf has same black height * @param node - starting node * @returns black height * @throws Error if property is violated */ testBlackHeightProperty(node) { let height = 0; let heightLeft = 0; let heightRight = 0; if (node.color === RB_TREE_COLOR_BLACK) { height++; } if (node.left !== this.nil_node) { heightLeft = this.testBlackHeightProperty(node.left); } else { heightLeft = 1; } if (node.right !== this.nil_node) { heightRight = this.testBlackHeightProperty(node.right); } else { heightRight = 1; } if (heightLeft !== heightRight) { throw new Error('Red-black height property violated'); } height += heightLeft; return height; } } export { Interval, Interval2D, IntervalBase, IntervalTree, Node, IntervalTree as default }; //# sourceMappingURL=main.mjs.map