├── .eslintignore ├── .gitignore ├── .gitattributes ├── .editorconfig ├── .prettierrc ├── tsconfig.json ├── LICENSE ├── demo ├── dataintervaltree.ts └── intervaltree.ts ├── package.json ├── README.md ├── .eslintrc ├── test └── test.ts └── index.ts /.eslintignore: -------------------------------------------------------------------------------- 1 | lib 2 | node_modules 3 | typings -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | npm-debug.log 3 | typings 4 | lib 5 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # Force unix style line endings for everything 2 | * text eol=lf 3 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | end_of_line = lf 6 | indent_size = 2 7 | indent_style = space 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "arrowParens": "avoid", 3 | "bracketSameLine": true, 4 | "endOfLine": "lf", 5 | "jsxSingleQuote": true, 6 | "printWidth": 100, 7 | "semi": false, 8 | "singleQuote": true, 9 | "trailingComma": "all" 10 | } 11 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "target": "es5", 5 | "downlevelIteration": true, 6 | "lib": [ 7 | "dom", 8 | "es2020", 9 | "scripthost", 10 | "es2015.collection", 11 | "es2015.iterable" 12 | ], 13 | "noImplicitAny": true, 14 | "strictNullChecks": true, 15 | "sourceMap": true, 16 | "declaration": true, 17 | "moduleResolution": "node", 18 | "outDir": "lib" 19 | }, 20 | "include": ["./"], 21 | "exclude": [ 22 | "node_modules", 23 | "lib" 24 | ] 25 | } 26 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Marko Žarković 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /demo/dataintervaltree.ts: -------------------------------------------------------------------------------- 1 | import cuid = require('cuid') 2 | import IntervalTree from '../index' 3 | 4 | const intervalTree = new IntervalTree() 5 | 6 | /* Usage: 7 | insert - intervalTree.insert(low: number, high: number, data: T) => 8 | inserts based on shallow equality 9 | true if success, false if nothing inserted (duplicate item) 10 | search - intervalTree.search(low: number, high: number) => [ data, data, data, ... ] => 11 | empty array if no result 12 | remove - intervalTree.remove(low: number, high: number, data: T) => 13 | removes based on shallow equality 14 | true if success, false if nothing removed 15 | */ 16 | 17 | function getRandomInt(min: number, max: number) { 18 | return Math.floor(Math.random() * (max - min + 1)) + min 19 | } 20 | 21 | for (let i = 1; i <= 100; i++) { 22 | let low = getRandomInt(0, 100) 23 | let high = getRandomInt(0, 100) 24 | 25 | if (high < low) { 26 | const temp = high 27 | high = low 28 | low = temp 29 | } 30 | 31 | intervalTree.insert(low, high, cuid()) 32 | } 33 | 34 | console.log('Number of the records in the tree: ' + intervalTree.count) 35 | 36 | const results = intervalTree.search(10, 15) 37 | if (!results || results.length === 0) { 38 | console.log('No overlapping intervals') 39 | } else { 40 | console.log('Found ' + results.length + ' overlapping intervals') 41 | 42 | for (let i = 0; i < results.length; i++) { 43 | console.log(results[i]) 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "node-interval-tree", 3 | "version": "2.1.2", 4 | "description": "Implementation of interval tree data structure.", 5 | "main": "./lib/index.js", 6 | "types": "./lib/index.d.ts", 7 | "engines": { 8 | "node": ">= 14.0.0" 9 | }, 10 | "scripts": { 11 | "autotest": "npm run test -- --watch", 12 | "build": "tsc", 13 | "clean": "rimraf lib", 14 | "lint": "eslint --ext .js,.ts ./", 15 | "prepublishOnly": "npm run lint && npm run clean && npm run test && npm run build", 16 | "test": "ts-mocha -R spec test/**.ts" 17 | }, 18 | "repository": { 19 | "type": "git", 20 | "url": "https://github.com/ShieldBattery/node-interval-tree" 21 | }, 22 | "keywords": [ 23 | "interval", 24 | "AVL", 25 | "tree", 26 | "data structure" 27 | ], 28 | "author": "Marko Žarković ", 29 | "license": "MIT", 30 | "bugs": { 31 | "url": "https://github.com/ShieldBattery/node-interval-tree/issues" 32 | }, 33 | "homepage": "https://github.com/ShieldBattery/node-interval-tree", 34 | "dependencies": { 35 | "shallowequal": "^1.1.0" 36 | }, 37 | "files": [ 38 | "lib/index.js", 39 | "lib/index.js.map", 40 | "lib/index.d.ts", 41 | "index.ts" 42 | ], 43 | "devDependencies": { 44 | "@types/chai": "^4.3.1", 45 | "@types/mocha": "^10.0.1", 46 | "@types/shallowequal": "^1.1.1", 47 | "@typescript-eslint/eslint-plugin": "^5.30.7", 48 | "@typescript-eslint/parser": "^5.30.7", 49 | "chai": "^4.3.6", 50 | "cuid": "^2.1.8", 51 | "eslint": "^8.20.0", 52 | "eslint-config-prettier": "^8.5.0", 53 | "eslint-plugin-prettier": "^4.2.1", 54 | "mocha": "^10.0.0", 55 | "prettier": "^2.7.1", 56 | "rimraf": "^3.0.2", 57 | "ts-mocha": "^10.0.0", 58 | "typescript": "^4.7.4" 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /demo/intervaltree.ts: -------------------------------------------------------------------------------- 1 | import cuid = require('cuid') 2 | import { Interval, IntervalTree } from '../index' 3 | 4 | interface StringInterval extends Interval { 5 | data: string 6 | } 7 | 8 | const intervalTree = new IntervalTree() 9 | 10 | /* Usage: 11 | interface Interval { 12 | readonly low: number 13 | readonly high: number 14 | } 15 | 16 | Intervals can be extended which allows you to add any data you need to the stored object. 17 | Do not modify low or high after the interval has been inserted as this will ruin the collection. 18 | 19 | insert - intervalTree.insert(interval: Interval) => 20 | inserts based on shallow equality 21 | true if success, false if nothing inserted (duplicate item) 22 | search - intervalTree.search(low: number, high: number) => [ Interval, Interval, Interval ] => 23 | empty array if no result 24 | remove - intervalTree.remove(interval: Interval) => 25 | removes based on shallow equality 26 | true if success, false if nothing removed 27 | */ 28 | 29 | function getRandomInt(min: number, max: number) { 30 | return Math.floor(Math.random() * (max - min + 1)) + min 31 | } 32 | 33 | for (let i = 1; i <= 100; i++) { 34 | let low = getRandomInt(0, 100) 35 | let high = getRandomInt(0, 100) 36 | 37 | if (high < low) { 38 | const temp = high 39 | high = low 40 | low = temp 41 | } 42 | 43 | intervalTree.insert({ low, high, data: cuid() }) 44 | } 45 | 46 | console.log('Number of the records in the tree: ' + intervalTree.count) 47 | 48 | const results = intervalTree.search(10, 15) 49 | if (!results || results.length === 0) { 50 | console.log('No overlapping intervals') 51 | } else { 52 | console.log('Found ' + results.length + ' overlapping intervals') 53 | 54 | for (let i = 0; i < results.length; i++) { 55 | console.log(results[i]) 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # node-interval-tree 2 | An [Interval Tree](https://en.wikipedia.org/wiki/Interval_tree) data structure implemented as an augmented AVL Tree where each node maintains a list of records and their search intervals. Record is composed of an interval and its underlying data, sent by a client. This allows the interval tree to have the same interval inserted multiple times, as long as its data is different. Both insertion and deletion require `O(log n)` time. Searching requires `O(min(n, k * log n))` time, where `k` is the number of intervals in the output list. 3 | 4 | [![NPM](https://img.shields.io/npm/v/node-interval-tree.svg?style=flat)](https://www.npmjs.org/package/node-interval-tree) 5 | 6 | [![NPM](https://nodei.co/npm/node-interval-tree.png)](https://nodei.co/npm/node-interval-tree/) 7 | 8 | ## Usage 9 | ```ts 10 | import IntervalTree from 'node-interval-tree' 11 | const intervalTree = new IntervalTree() 12 | ``` 13 | 14 | ### Insert 15 | ```ts 16 | intervalTree.insert(low, high, 'foo') 17 | ``` 18 | 19 | Insert an interval with associated data into the tree. Intervals with the same low and high value can be inserted, as long as their data is different. 20 | Data can be any JS primitive or object. 21 | `low` and `high` have to be numbers where `low <= high` (also the case for all other operations with `low` and `high`). 22 | Returns true if successfully inserted, false if nothing inserted. 23 | 24 | ### Search 25 | ```ts 26 | intervalTree.search(low, high) 27 | ``` 28 | 29 | Search all intervals that overlap low and high arguments, both of them inclusive. Low and high values don't need to be in the tree themselves. 30 | Returns an array of all data objects of the intervals in the range [low, high]; doesn't return the intervals themselves. 31 | 32 | ### Remove 33 | ```ts 34 | intervalTree.remove(low, high, 'foo') 35 | ``` 36 | 37 | Remove an interval from the tree. To remove an interval, all arguments must match the one in the tree. 38 | Returns true if successfully removed, false if nothing removed. 39 | 40 | ## Advanced usage 41 | The default export is convenient to use but can be too limiting for some. 42 | `exports.IntervalTree` operate on `Interval` directly, giving you access to the `low` and `high` values in the results. 43 | You can attach extra properties to `Interval` but they should not be modified after insertion as objects in the tree are compared according to shallow equality. 44 | 45 | ```ts 46 | import { Interval, IntervalTree } from 'node-interval-tree' 47 | 48 | interface StringInterval extends Interval { 49 | name: string 50 | } 51 | 52 | const intervalTree = new IntervalTree() 53 | ``` 54 | ### Insert 55 | ```ts 56 | intervalTree.insert({ low, high }) 57 | intervalTree.insert({ low, high, name: 'foo' }) 58 | ``` 59 | Insert an interval into the tree. Intervals are compared according to shallow equality and only inserted if unique. 60 | Returns true if successfully inserted, false if nothing inserted. 61 | 62 | ### Search 63 | ```ts 64 | intervalTree.search(low, high) 65 | ``` 66 | 67 | Search all intervals that overlap low and high arguments, both of them inclusive. Low and high values don't need to be in the tree themselves. 68 | Returns an array of all intervals in the range [low, high]. 69 | 70 | ### Remove 71 | ```ts 72 | intervalTree.remove({ low, high }) 73 | intervalTree.remove({ low, high, name: 'foo' }) 74 | ``` 75 | 76 | Remove an interval from the tree. Intervals are compared according to shallow equality and only removed if all properties match. 77 | Returns true if successfully removed, false if nothing removed. 78 | 79 | ## BigInt support 80 | The `low` and `high` values of the interval are of type `number` by default. However, the library 81 | offers support to use `bigint` type for interval keys instead. 82 | 83 | With default export: 84 | ```ts 85 | import IntervalTree from 'node-interval-tree' 86 | const intervalTree = new IntervalTree() 87 | ``` 88 | 89 | With advanced export: 90 | ```ts 91 | import { Interval, IntervalTree } from 'node-interval-tree' 92 | 93 | interface StringInterval extends Interval { 94 | name: string 95 | } 96 | 97 | const intervalTree = new IntervalTree() 98 | ``` 99 | 100 | ## Example 101 | ```ts 102 | import IntervalTree from 'node-interval-tree' 103 | 104 | const intervalTree = new IntervalTree() 105 | intervalTree.insert(10, 15, 'foo') // -> true 106 | intervalTree.insert(35, 50, 'baz') // -> true 107 | 108 | intervalTree.search(12, 20) // -> ['foo'] 109 | 110 | intervalTree.remove(35, 50, 'baz') // -> true 111 | intervalTree.insert(10, 15, 'baz') // -> true 112 | 113 | intervalTree.search(12, 20) // -> ['foo', 'baz'] 114 | ``` 115 | 116 | More examples can be found in the demo folder. 117 | 118 | ## License 119 | 120 | MIT 121 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "parser": "@typescript-eslint/parser", 3 | "parserOptions": { 4 | "ecmaVersion": 2020, 5 | "sourceType": "module" 6 | }, 7 | 8 | "extends": ["prettier", "plugin:@typescript-eslint/recommended"], 9 | 10 | "plugins": ["@typescript-eslint", "prettier"], 11 | 12 | "env": { 13 | "browser": true, 14 | "es6": true, 15 | "mocha": true, 16 | "node": true 17 | }, 18 | 19 | "globals": { 20 | "document": "readonly", 21 | "navigator": "readonly", 22 | "window": "readonly" 23 | }, 24 | 25 | "rules": { 26 | "accessor-pairs": 2, 27 | "array-bracket-spacing": 0, 28 | "block-scoped-var": 0, 29 | "brace-style": 0, 30 | "camelcase": [2, { "properties": "always" }], 31 | "comma-dangle": 0, 32 | "comma-spacing": [2, { "before": false, "after": true }], 33 | "comma-style": [2, "last", { "exceptions": { "VariableDeclaration": true } }], 34 | "complexity": 0, 35 | "computed-property-spacing": 0, 36 | "consistent-return": 2, 37 | "consistent-this": [2, "self"], 38 | "constructor-super": 2, 39 | "curly": [2, "multi-line"], 40 | "default-case": 0, 41 | "dot-location": [2, "property"], 42 | "dot-notation": 2, 43 | "eol-last": 2, 44 | "eqeqeq": [2, "smart"], 45 | "func-names": 0, 46 | "func-style": 0, 47 | "generator-star-spacing": 0, 48 | "global-strict": 0, 49 | "guard-for-in": 0, 50 | "handle-callback-err": [2, "^(err|error)$"], 51 | "key-spacing": [2, { "beforeColon": false, "afterColon": true }], 52 | "keyword-spacing": 2, 53 | "linebreak-style": [2, "unix"], 54 | "lines-around-comment": 0, 55 | "max-depth": 0, 56 | "max-len": [2, 100], 57 | "max-nested-callbacks": [2, 5], 58 | "max-params": [2, 8], 59 | "max-statements": 0, 60 | "new-cap": 0, 61 | "new-parens": 2, 62 | "newline-after-var": 0, 63 | "no-alert": 2, 64 | "no-array-constructor": 2, 65 | "no-bitwise": 0, 66 | "no-caller": 2, 67 | "no-catch-shadow": 0, 68 | "no-comma-dangle": 0, 69 | "no-cond-assign": 2, 70 | "no-console": 0, 71 | "no-constant-condition": 0, 72 | "no-continue": 0, 73 | "no-control-regex": 2, 74 | "no-debugger": 2, 75 | "no-delete-var": 2, 76 | "no-div-regex": 0, 77 | "no-dupe-args": 2, 78 | "no-dupe-keys": 2, 79 | "no-duplicate-case": 2, 80 | "no-else-return": 0, 81 | "no-empty": 2, 82 | "no-empty-character-class": 2, 83 | "no-empty-class": 0, 84 | "no-eq-null": 0, 85 | "no-eval": 2, 86 | "no-ex-assign": 2, 87 | "no-extend-native": 2, 88 | "no-extra-bind": 2, 89 | "no-extra-boolean-cast": 2, 90 | "no-extra-parens": [2, "functions"], 91 | "no-extra-semi": 0, 92 | "no-extra-strict": 0, 93 | "no-fallthrough": 0, 94 | "no-floating-decimal": 2, 95 | "no-func-assign": 2, 96 | "no-implied-eval": 2, 97 | "no-inline-comments": 0, 98 | "no-inner-declarations": [2, "both"], 99 | "no-invalid-regexp": 2, 100 | "no-irregular-whitespace": 2, 101 | "no-iterator": 2, 102 | "no-label-var": 2, 103 | "no-labels": 2, 104 | "no-lone-blocks": 2, 105 | "no-lonely-if": 0, 106 | "no-loop-func": 0, 107 | "no-mixed-requires": 0, 108 | "no-mixed-spaces-and-tabs": 2, 109 | "no-multi-spaces": 2, 110 | "no-multi-str": 2, 111 | "no-multiple-empty-lines": [2, { "max": 2 }], 112 | "no-native-reassign": 2, 113 | "no-negated-in-lhs": 2, 114 | "no-nested-ternary": 2, 115 | "no-new": 2, 116 | "no-new-func": 2, 117 | "no-new-object": 2, 118 | "no-new-require": 2, 119 | "no-new-wrappers": 2, 120 | "no-obj-calls": 2, 121 | "no-octal": 2, 122 | "no-octal-escape": 2, 123 | "no-param-reassign": 0, 124 | "no-path-concat": 0, 125 | "no-plusplus": 0, 126 | "no-process-env": 0, 127 | "no-process-exit": 0, 128 | "no-proto": 2, 129 | "no-redeclare": 2, 130 | "no-regex-spaces": 2, 131 | "no-reserved-keys": 0, 132 | "no-restricted-modules": 0, 133 | "no-return-assign": 2, 134 | "no-script-url": 0, 135 | "no-self-compare": 2, 136 | "no-sequences": 2, 137 | "no-shadow": 0, 138 | "no-shadow-restricted-names": 2, 139 | "no-space-before-semi": 0, 140 | "no-spaced-func": 2, 141 | "no-sparse-arrays": 2, 142 | "no-sync": 0, 143 | "no-ternary": 0, 144 | "no-this-before-super": 2, 145 | "no-throw-literal": 2, 146 | "no-trailing-spaces": 2, 147 | "no-undef": 2, 148 | "no-undef-init": 2, 149 | "no-undefined": 0, 150 | "no-underscore-dangle": 0, 151 | "no-unexpected-multiline": 2, 152 | "no-unneeded-ternary": 2, 153 | "no-unreachable": 2, 154 | "no-unused-expressions": [2, { "allowTaggedTemplates": true }], 155 | "no-unused-vars": 0, 156 | "no-use-before-define": 0, 157 | "no-var": 2, 158 | "no-void": 2, 159 | "no-warning-comments": [2, { "terms": ["fixme", "do not submit", "xxx"], "location": "start" }], 160 | "no-with": 2, 161 | "object-curly-spacing": 0, 162 | "object-shorthand": 2, 163 | "one-var": 0, 164 | "operator-assignment": 0, 165 | "padded-blocks": [2, "never"], 166 | "prefer-const": 2, 167 | "quote-props": [2, "as-needed"], 168 | "quotes": [2, "single", "avoid-escape"], 169 | "radix": 0, 170 | "semi": [2, "never"], 171 | "semi-spacing": 0, 172 | "sort-vars": 0, 173 | "space-after-function-name": 0, 174 | "space-before-blocks": [2, "always"], 175 | "space-in-brackets": 0, 176 | "space-in-parens": [2, "never"], 177 | "space-infix-ops": [2, { "int32Hint": true }], 178 | "space-unary-ops": [2, { "words": true, "nonwords": false }], 179 | "spaced-comment": [ 180 | 2, 181 | "always", 182 | { "markers": ["global", "globals", "eslint", "eslint-disable", "*package", "!", ","] } 183 | ], 184 | "spaced-line-comment": 0, 185 | "strict": 0, 186 | "use-isnan": 2, 187 | "valid-jsdoc": 0, 188 | "valid-typeof": 2, 189 | "vars-on-top": 0, 190 | "wrap-iife": [2, "any"], 191 | "wrap-regex": 0, 192 | "yoda": 0, 193 | 194 | "prettier/prettier": 2, 195 | 196 | "@typescript-eslint/explicit-module-boundary-types": 0, 197 | "@typescript-eslint/no-empty-function": 0, 198 | "@typescript-eslint/no-explicit-any": 0, 199 | "@typescript-eslint/no-extra-semi": 0, 200 | "@typescript-eslint/no-non-null-assertion": 0, 201 | "@typescript-eslint/no-unused-vars": [2, { "vars": "all", "args": "none" }], 202 | "@typescript-eslint/no-var-requires": 0 203 | } 204 | } 205 | -------------------------------------------------------------------------------- /test/test.ts: -------------------------------------------------------------------------------- 1 | import { expect } from 'chai' 2 | import cuid = require('cuid') 3 | import { Interval, IntervalTree, Node } from '../index' 4 | 5 | interface StringInterval extends Interval { 6 | data: string 7 | } 8 | 9 | function getInterval(low: number, high: number, data: string) { 10 | return { low, high, data } 11 | } 12 | 13 | const randomTree = new IntervalTree() 14 | 15 | function getRandomInt(min: number, max: number) { 16 | return Math.floor(Math.random() * (max - min + 1)) + min 17 | } 18 | 19 | for (let i = 1; i <= 100; i++) { 20 | let intervalLow = getRandomInt(0, 100) 21 | let intervalHigh = getRandomInt(0, 100) 22 | 23 | if (intervalHigh < intervalLow) { 24 | const temp = intervalHigh 25 | intervalHigh = intervalLow 26 | intervalLow = temp 27 | } 28 | 29 | randomTree.insert(getInterval(intervalLow, intervalHigh, cuid())) 30 | } 31 | 32 | function treeToArray(currentNode: Node | undefined, treeArray: Node[]) { 33 | if (currentNode === undefined) { 34 | return 35 | } 36 | 37 | treeToArray(currentNode.left, treeArray) 38 | for (let i = 0; i < currentNode.records.length; i++) { 39 | treeArray.push(currentNode) 40 | } 41 | treeToArray(currentNode.right, treeArray) 42 | } 43 | 44 | function iteratorToArray(it: Iterator) { 45 | const acc: T[] = [] 46 | let last = it.next() 47 | while (!last.done) { 48 | acc.push(last.value as T) 49 | last = it.next() 50 | } 51 | return acc 52 | } 53 | 54 | function isSorted(tree: IntervalTree) { 55 | const treeArray: Node[] = [] 56 | treeToArray(tree.root, treeArray) 57 | 58 | for (let i = 0; i < treeArray.length - 1; i++) { 59 | if (treeArray[i].key > treeArray[i + 1].key) { 60 | return false 61 | } 62 | } 63 | 64 | return true 65 | } 66 | 67 | function highestMaxValue(tree: IntervalTree): Node { 68 | const treeArray: Node[] = [] 69 | treeToArray(tree.root, treeArray) 70 | 71 | let highest = treeArray[0] 72 | for (let i = 0; i < treeArray.length; i++) { 73 | if (treeArray[i].max > highest.max) { 74 | highest = treeArray[i] 75 | } 76 | } 77 | 78 | return highest 79 | } 80 | 81 | describe('Interval tree', () => { 82 | describe('insert', () => { 83 | it('should correctly insert into an empty tree', () => { 84 | const tree = new IntervalTree() 85 | 86 | tree.insert(getInterval(50, 100, 'data')) 87 | 88 | const searchResult = tree.search(50, 100) 89 | expect(searchResult[0].data).to.eql('data') 90 | }) 91 | 92 | it('should correctly insert into a node with the same key', () => { 93 | const tree = new IntervalTree() 94 | 95 | tree.insert(getInterval(50, 150, 'data1')) 96 | tree.insert(getInterval(50, 100, 'data2')) 97 | 98 | let searchResult = tree.search(75, 100) 99 | expect(searchResult.length).to.eql(2) 100 | expect(searchResult[0].data).to.eql('data1') 101 | expect(searchResult[1].data).to.eql('data2') 102 | 103 | searchResult = tree.search(125, 150) 104 | expect(searchResult.length).to.eql(1) 105 | expect(searchResult[0].data).to.eql('data1') 106 | }) 107 | 108 | it('should correctly insert into a left subtree', () => { 109 | const tree = new IntervalTree() 110 | 111 | tree.insert(getInterval(50, 150, 'data1')) 112 | tree.insert(getInterval(25, 100, 'data2')) 113 | 114 | const searchResult = tree.search(75, 100) 115 | expect(searchResult.length).to.eql(2) 116 | expect(searchResult[0].data).to.eql('data2') 117 | expect(searchResult[1].data).to.eql('data1') 118 | }) 119 | 120 | it('should correctly insert into a right subtree', () => { 121 | const tree = new IntervalTree() 122 | 123 | tree.insert(getInterval(50, 150, 'data1')) 124 | tree.insert(getInterval(75, 100, 'data2')) 125 | 126 | const searchResult = tree.search(85, 100) 127 | expect(searchResult.length).to.eql(2) 128 | expect(searchResult[0].data).to.eql('data1') 129 | expect(searchResult[1].data).to.eql('data2') 130 | }) 131 | 132 | it('should reject intervals where low > high', () => { 133 | const tree = new IntervalTree() 134 | const high = 10 135 | const low = 15 136 | 137 | expect(() => tree.insert({ low, high })).to.throw(Error) 138 | }) 139 | }) 140 | 141 | describe('search', () => { 142 | it('should return an empty array when searching an empty tree', () => { 143 | const tree = new IntervalTree() 144 | 145 | const searchResult = tree.search(75, 150) 146 | expect(searchResult.length).to.eql(0) 147 | }) 148 | 149 | it('should be inclusive', () => { 150 | const tree = new IntervalTree() 151 | 152 | tree.insert(getInterval(50, 150, 'data1')) 153 | tree.insert(getInterval(75, 200, 'data2')) 154 | 155 | const search1 = tree.search(50, 100) 156 | expect(search1[0].data).to.eql('data1') 157 | expect(search1[1].data).to.eql('data2') 158 | expect(search1.length).to.eql(2) 159 | 160 | const search2 = tree.search(0, 50) 161 | expect(search2[0].data).to.eql('data1') 162 | expect(search2.length).to.eql(1) 163 | 164 | const search3 = tree.search(200, 300) 165 | expect(search3[0].data).to.eql('data2') 166 | expect(search3.length).to.eql(1) 167 | }) 168 | }) 169 | 170 | describe('delete', () => { 171 | it('should return false when trying to delete from an empty tree', () => { 172 | const tree = new IntervalTree() 173 | 174 | const isRemoved = tree.remove({ low: 50, high: 100 }) 175 | expect(isRemoved).to.eql(false) 176 | }) 177 | 178 | it('should correctly delete the root node', () => { 179 | const tree = new IntervalTree() 180 | 181 | tree.insert(getInterval(75, 150, 'data')) 182 | 183 | const isRemoved = tree.remove(getInterval(75, 150, 'data')) 184 | expect(isRemoved).to.eql(true) 185 | expect(tree.root).to.eql(undefined) 186 | }) 187 | 188 | it('should correctly delete the data object on a node with multiple data objects', () => { 189 | const tree = new IntervalTree() 190 | 191 | tree.insert(getInterval(50, 120, 'data1')) 192 | tree.insert(getInterval(75, 100, 'data2')) 193 | tree.insert(getInterval(75, 200, 'firstDataToRemove')) 194 | tree.insert(getInterval(75, 150, 'secondDataToRemove')) 195 | 196 | let searchResult = tree.search(50, 200) 197 | expect(searchResult.length).to.eql(4) 198 | expect(searchResult[0].data).to.eql('data1') 199 | expect(searchResult[1].data).to.eql('data2') 200 | expect(searchResult[2].data).to.eql('firstDataToRemove') 201 | expect(searchResult[3].data).to.eql('secondDataToRemove') 202 | 203 | let isRemoved = tree.remove(getInterval(75, 200, 'firstDataToRemove')) 204 | expect(isRemoved).to.eql(true) 205 | 206 | searchResult = tree.search(50, 200) 207 | expect(searchResult.length).to.eql(3) 208 | expect(searchResult[0].data).to.eql('data1') 209 | expect(searchResult[1].data).to.eql('data2') 210 | expect(searchResult[2].data).to.eql('secondDataToRemove') 211 | 212 | isRemoved = tree.remove(getInterval(75, 150, 'secondDataToRemove')) 213 | expect(isRemoved).to.eql(true) 214 | 215 | searchResult = tree.search(50, 200) 216 | expect(searchResult.length).to.eql(2) 217 | expect(searchResult[0].data).to.eql('data1') 218 | expect(searchResult[1].data).to.eql('data2') 219 | }) 220 | }) 221 | 222 | describe('implementation details', () => { 223 | it('should be sorted with in-order traversal', () => { 224 | const sorted = isSorted(randomTree) 225 | expect(sorted).to.eql(true) 226 | }) 227 | 228 | it('should have highest `max` value in root node', () => { 229 | const highest = highestMaxValue(randomTree) 230 | expect((randomTree.root as Node).max).to.eql(highest.max) 231 | }) 232 | 233 | it('should work with BigInts', () => { 234 | const tree = new IntervalTree, bigint>() 235 | const range = { 236 | low: BigInt('0x456787654567876545'), 237 | high: BigInt('0x876545678987654567'), 238 | } 239 | 240 | tree.insert(range) 241 | 242 | const results = tree.search(BigInt('0x456787654567876560'), BigInt('0x4567876545678765FF')) 243 | 244 | expect(results).to.eql([range]) 245 | }) 246 | }) 247 | 248 | describe('InOrder', () => { 249 | it('should traverse in order', () => { 250 | const tree = new IntervalTree() 251 | 252 | const values: [number, number, string][] = [ 253 | [50, 150, 'data1'], 254 | [75, 100, 'data2'], 255 | [40, 100, 'data3'], 256 | [60, 150, 'data4'], 257 | [80, 90, 'data5'], 258 | ] 259 | 260 | values.map(([low, high, value]) => ({ low, high, data: value })).forEach(i => tree.insert(i)) 261 | 262 | const order = ['data3', 'data1', 'data4', 'data2', 'data5'] 263 | const data = iteratorToArray(tree.inOrder()).map(v => v.data) 264 | expect(data).to.eql(order) 265 | }) 266 | }) 267 | 268 | describe('ReverseInOrder', () => { 269 | it('should traverse in reverse order', () => { 270 | const tree = new IntervalTree() 271 | 272 | const values: [number, number, string][] = [ 273 | [50, 150, 'data1'], 274 | [75, 100, 'data2'], 275 | [40, 100, 'data3'], 276 | [60, 150, 'data4'], 277 | [80, 90, 'data5'], 278 | [85, 88, 'data6'], 279 | [88, 89, 'data7'], 280 | ] 281 | 282 | values.map(([low, high, value]) => ({ low, high, data: value })).forEach(i => tree.insert(i)) 283 | 284 | const order = ['data7', 'data6', 'data5', 'data2', 'data4', 'data1', 'data3'] 285 | const data = iteratorToArray(tree.reverseInOrder()).map(v => v.data) 286 | expect(data).to.eql(order) 287 | }) 288 | 289 | it('should traverse in reverse order even with equal elements at right of tree', () => { 290 | const tree = new IntervalTree() 291 | 292 | const values: [number, number, string][] = [ 293 | [50, 150, 'data1'], 294 | [75, 100, 'data2'], 295 | [40, 100, 'data3'], 296 | [60, 150, 'data4'], 297 | [80, 90, 'data5'], 298 | [85, 88, 'data6'], 299 | [88, 89, 'data7'], 300 | [88, 90, 'data8'], 301 | ] 302 | 303 | values.map(([low, high, value]) => ({ low, high, data: value })).forEach(i => tree.insert(i)) 304 | 305 | const order = ['data8', 'data7', 'data6', 'data5', 'data2', 'data4', 'data1', 'data3'] 306 | const data = iteratorToArray(tree.reverseInOrder()).map(v => v.data) 307 | expect(data).to.eql(order) 308 | }) 309 | }) 310 | 311 | describe('PreOrder', () => { 312 | it('should traverse pre order', () => { 313 | const tree = new IntervalTree() 314 | 315 | const values: [number, number, string][] = [ 316 | [50, 150, 'data1'], 317 | [75, 100, 'data2'], 318 | [40, 100, 'data3'], 319 | [60, 150, 'data4'], 320 | [80, 90, 'data5'], 321 | ] 322 | 323 | values.map(([low, high, value]) => ({ low, high, data: value })).forEach(i => tree.insert(i)) 324 | 325 | const order = ['data1', 'data3', 'data2', 'data4', 'data5'] 326 | const data = iteratorToArray(tree.preOrder()).map(v => v.data) 327 | expect(data).to.eql(order) 328 | }) 329 | }) 330 | }) 331 | -------------------------------------------------------------------------------- /index.ts: -------------------------------------------------------------------------------- 1 | // An augmented AVL Tree where each node maintains a list of records and their search intervals. 2 | // Record is composed of an interval and its underlying data, sent by a client. This allows the 3 | // interval tree to have the same interval inserted multiple times, as long its data is different. 4 | // Both insertion and deletion require O(log n) time. Searching requires O(k*logn) time, where `k` 5 | // is the number of intervals in the output list. 6 | 7 | import isSame = require('shallowequal') 8 | 9 | export interface Interval { 10 | readonly low: N 11 | readonly high: N 12 | } 13 | 14 | function max(a: N, b: N): N { 15 | return a < b ? b : a 16 | } 17 | 18 | function height, N extends number | bigint = number>(node?: Node) { 19 | if (node === undefined) { 20 | return -1 21 | } else { 22 | return node.height 23 | } 24 | } 25 | 26 | export class Node, N extends number | bigint = number> { 27 | public key: N 28 | public max: N 29 | public records: T[] = [] 30 | public parent?: Node 31 | public height = 0 32 | public left?: Node 33 | public right?: Node 34 | 35 | constructor(public intervalTree: IntervalTree, record: T) { 36 | this.key = record.low 37 | this.max = record.high 38 | 39 | // Save the array of all records with the same key for this node 40 | this.records.push(record) 41 | } 42 | 43 | // Gets the highest record.high value for this node 44 | public getNodeHigh() { 45 | let high = this.records[0].high 46 | 47 | for (let i = 1; i < this.records.length; i++) { 48 | if (this.records[i].high > high) { 49 | high = this.records[i].high 50 | } 51 | } 52 | 53 | return high 54 | } 55 | 56 | // Updates height value of the node. Called during insertion, rebalance, removal 57 | public updateHeight() { 58 | this.height = max(height(this.left), height(this.right)) + 1 59 | } 60 | 61 | // Updates the max value of all the parents after inserting into already existing node, as well as 62 | // removing the node completely or removing the record of an already existing node. Starts with 63 | // the parent of an affected node and bubbles up to root 64 | public updateMaxOfParents() { 65 | if (this === undefined) { 66 | return 67 | } 68 | 69 | const thisHigh = this.getNodeHigh() 70 | if (this.left !== undefined && this.right !== undefined) { 71 | this.max = max(max(this.left.max, this.right.max), thisHigh) 72 | } else if (this.left !== undefined && this.right === undefined) { 73 | this.max = max(this.left.max, thisHigh) 74 | } else if (this.left === undefined && this.right !== undefined) { 75 | this.max = max(this.right.max, thisHigh) 76 | } else { 77 | this.max = thisHigh 78 | } 79 | 80 | if (this.parent) { 81 | this.parent.updateMaxOfParents() 82 | } 83 | } 84 | 85 | /* 86 | Left-Left case: 87 | 88 | z y 89 | / \ / \ 90 | y T4 Right Rotate (z) x z 91 | / \ - - - - - - - - -> / \ / \ 92 | x T3 T1 T2 T3 T4 93 | / \ 94 | T1 T2 95 | 96 | Left-Right case: 97 | 98 | z z x 99 | / \ / \ / \ 100 | y T4 Left Rotate (y) x T4 Right Rotate(z) y z 101 | / \ - - - - - - - - -> / \ - - - - - - - -> / \ / \ 102 | T1 x y T3 T1 T2 T3 T4 103 | / \ / \ 104 | T2 T3 T1 T2 105 | */ 106 | 107 | // Handles Left-Left case and Left-Right case after rebalancing AVL tree 108 | private _updateMaxAfterRightRotate() { 109 | const parent = this.parent! 110 | const left = parent.left! 111 | // Update max of left sibling (x in first case, y in second) 112 | const thisParentLeftHigh = left.getNodeHigh() 113 | if (left.left === undefined && left.right !== undefined) { 114 | left.max = max(thisParentLeftHigh, left.right.max) 115 | } else if (left.left !== undefined && left.right === undefined) { 116 | left.max = max(thisParentLeftHigh, left.left.max) 117 | } else if (left.left === undefined && left.right === undefined) { 118 | left.max = thisParentLeftHigh 119 | } else { 120 | left.max = max(max(left.left!.max, left.right!.max), thisParentLeftHigh) 121 | } 122 | 123 | // Update max of itself (z) 124 | const thisHigh = this.getNodeHigh() 125 | if (this.left === undefined && this.right !== undefined) { 126 | this.max = max(thisHigh, this.right.max) 127 | } else if (this.left !== undefined && this.right === undefined) { 128 | this.max = max(thisHigh, this.left.max) 129 | } else if (this.left === undefined && this.right === undefined) { 130 | this.max = thisHigh 131 | } else { 132 | this.max = max(max(this.left!.max, this.right!.max), thisHigh) 133 | } 134 | 135 | // Update max of parent (y in first case, x in second) 136 | parent.max = max(max(parent.left!.max, parent.right!.max), parent.getNodeHigh()) 137 | } 138 | 139 | /* 140 | Right-Right case: 141 | 142 | z y 143 | / \ / \ 144 | T1 y Left Rotate(z) z x 145 | / \ - - - - - - - -> / \ / \ 146 | T2 x T1 T2 T3 T4 147 | / \ 148 | T3 T4 149 | 150 | Right-Left case: 151 | 152 | z z x 153 | / \ / \ / \ 154 | T1 y Right Rotate (y) T1 x Left Rotate(z) z y 155 | / \ - - - - - - - - -> / \ - - - - - - - -> / \ / \ 156 | x T4 T2 y T1 T2 T3 T4 157 | / \ / \ 158 | T2 T3 T3 T4 159 | */ 160 | 161 | // Handles Right-Right case and Right-Left case in rebalancing AVL tree 162 | private _updateMaxAfterLeftRotate() { 163 | const parent = this.parent! 164 | const right = parent.right! 165 | // Update max of right sibling (x in first case, y in second) 166 | const thisParentRightHigh = right.getNodeHigh() 167 | if (right.left === undefined && right.right !== undefined) { 168 | right.max = max(thisParentRightHigh, right.right.max) 169 | } else if (right.left !== undefined && right.right === undefined) { 170 | right.max = max(thisParentRightHigh, right.left.max) 171 | } else if (right.left === undefined && right.right === undefined) { 172 | right.max = thisParentRightHigh 173 | } else { 174 | right.max = max(max(right.left!.max, right.right!.max), thisParentRightHigh) 175 | } 176 | 177 | // Update max of itself (z) 178 | const thisHigh = this.getNodeHigh() 179 | if (this.left === undefined && this.right !== undefined) { 180 | this.max = max(thisHigh, this.right.max) 181 | } else if (this.left !== undefined && this.right === undefined) { 182 | this.max = max(thisHigh, this.left.max) 183 | } else if (this.left === undefined && this.right === undefined) { 184 | this.max = thisHigh 185 | } else { 186 | this.max = max(max(this.left!.max, this.right!.max), thisHigh) 187 | } 188 | 189 | // Update max of parent (y in first case, x in second) 190 | parent.max = max(max(parent.left!.max, right.max), parent.getNodeHigh()) 191 | } 192 | 193 | private _leftRotate() { 194 | const rightChild = this.right! 195 | rightChild.parent = this.parent 196 | 197 | if (rightChild.parent === undefined) { 198 | this.intervalTree.root = rightChild 199 | } else { 200 | if (rightChild.parent.left === this) { 201 | rightChild.parent.left = rightChild 202 | } else if (rightChild.parent.right === this) { 203 | rightChild.parent.right = rightChild 204 | } 205 | } 206 | 207 | this.right = rightChild.left 208 | if (this.right !== undefined) { 209 | this.right.parent = this 210 | } 211 | rightChild.left = this 212 | this.parent = rightChild 213 | this.updateHeight() 214 | rightChild.updateHeight() 215 | } 216 | 217 | private _rightRotate() { 218 | const leftChild = this.left! 219 | leftChild.parent = this.parent 220 | 221 | if (leftChild.parent === undefined) { 222 | this.intervalTree.root = leftChild 223 | } else { 224 | if (leftChild.parent.left === this) { 225 | leftChild.parent.left = leftChild 226 | } else if (leftChild.parent.right === this) { 227 | leftChild.parent.right = leftChild 228 | } 229 | } 230 | 231 | this.left = leftChild.right 232 | if (this.left !== undefined) { 233 | this.left.parent = this 234 | } 235 | leftChild.right = this 236 | this.parent = leftChild 237 | this.updateHeight() 238 | leftChild.updateHeight() 239 | } 240 | 241 | // Rebalances the tree if the height value between two nodes of the same parent is greater than 242 | // two. There are 4 cases that can happen which are outlined in the graphics above 243 | private _rebalance() { 244 | if (height(this.left) >= 2 + height(this.right)) { 245 | const left = this.left! 246 | if (height(left.left) >= height(left.right)) { 247 | // Left-Left case 248 | this._rightRotate() 249 | this._updateMaxAfterRightRotate() 250 | } else { 251 | // Left-Right case 252 | left._leftRotate() 253 | this._rightRotate() 254 | this._updateMaxAfterRightRotate() 255 | } 256 | } else if (height(this.right) >= 2 + height(this.left)) { 257 | const right = this.right! 258 | if (height(right.right) >= height(right.left)) { 259 | // Right-Right case 260 | this._leftRotate() 261 | this._updateMaxAfterLeftRotate() 262 | } else { 263 | // Right-Left case 264 | right._rightRotate() 265 | this._leftRotate() 266 | this._updateMaxAfterLeftRotate() 267 | } 268 | } 269 | } 270 | 271 | public insert(record: T) { 272 | if (record.low < this.key) { 273 | // Insert into left subtree 274 | if (this.left === undefined) { 275 | this.left = new Node(this.intervalTree, record) 276 | this.left.parent = this 277 | } else { 278 | this.left.insert(record) 279 | } 280 | } else { 281 | // Insert into right subtree 282 | if (this.right === undefined) { 283 | this.right = new Node(this.intervalTree, record) 284 | this.right.parent = this 285 | } else { 286 | this.right.insert(record) 287 | } 288 | } 289 | 290 | // Update the max value of this ancestor if needed 291 | if (this.max < record.high) { 292 | this.max = record.high 293 | } 294 | 295 | // Update height of each node 296 | this.updateHeight() 297 | 298 | // Rebalance the tree to ensure all operations are executed in O(logn) time. This is especially 299 | // important in searching, as the tree has a high chance of degenerating without the rebalancing 300 | this._rebalance() 301 | } 302 | 303 | private _getOverlappingRecords(currentNode: Node, low: N, high: N) { 304 | if (currentNode.key <= high && low <= currentNode.getNodeHigh()) { 305 | // Nodes are overlapping, check if individual records in the node are overlapping 306 | const tempResults: T[] = [] 307 | for (let i = 0; i < currentNode.records.length; i++) { 308 | if (currentNode.records[i].high >= low) { 309 | tempResults.push(currentNode.records[i]) 310 | } 311 | } 312 | return tempResults 313 | } 314 | return [] 315 | } 316 | 317 | public search(low: N, high: N) { 318 | // Don't search nodes that don't exist 319 | if (this === undefined) { 320 | return [] 321 | } 322 | 323 | let leftSearch: T[] = [] 324 | let ownSearch: T[] = [] 325 | let rightSearch: T[] = [] 326 | 327 | // If interval is to the right of the rightmost point of any interval in this node and all its 328 | // children, there won't be any matches 329 | if (low > this.max) { 330 | return [] 331 | } 332 | 333 | // Search left children 334 | if (this.left !== undefined && this.left.max >= low) { 335 | leftSearch = this.left.search(low, high) 336 | } 337 | 338 | // Check this node 339 | ownSearch = this._getOverlappingRecords(this, low, high) 340 | 341 | // If interval is to the left of the start of this interval, then it can't be in any child to 342 | // the right 343 | if (high < this.key) { 344 | return leftSearch.concat(ownSearch) 345 | } 346 | 347 | // Otherwise, search right children 348 | if (this.right !== undefined) { 349 | rightSearch = this.right.search(low, high) 350 | } 351 | 352 | // Return accumulated results, if any 353 | return leftSearch.concat(ownSearch, rightSearch) 354 | } 355 | 356 | // Searches for a node by a `key` value 357 | public searchExisting(low: N): Node | undefined { 358 | if (this === undefined) { 359 | return undefined 360 | } 361 | 362 | if (this.key === low) { 363 | return this 364 | } else if (low < this.key) { 365 | if (this.left !== undefined) { 366 | return this.left.searchExisting(low) 367 | } 368 | } else { 369 | if (this.right !== undefined) { 370 | return this.right.searchExisting(low) 371 | } 372 | } 373 | 374 | return undefined 375 | } 376 | 377 | // Returns the smallest node of the subtree 378 | private _minValue(): Node { 379 | if (this.left === undefined) { 380 | return this 381 | } else { 382 | return this.left._minValue() 383 | } 384 | } 385 | 386 | public remove(node: Node): Node | undefined { 387 | const parent = this.parent! 388 | 389 | if (node.key < this.key) { 390 | // Node to be removed is on the left side 391 | if (this.left !== undefined) { 392 | return this.left.remove(node) 393 | } else { 394 | return undefined 395 | } 396 | } else if (node.key > this.key) { 397 | // Node to be removed is on the right side 398 | if (this.right !== undefined) { 399 | return this.right.remove(node) 400 | } else { 401 | return undefined 402 | } 403 | } else { 404 | if (this.left !== undefined && this.right !== undefined) { 405 | // Node has two children 406 | const minValue = this.right._minValue() 407 | this.key = minValue.key 408 | this.records = minValue.records 409 | return this.right.remove(this) 410 | } else if (parent.left === this) { 411 | // One child or no child case on left side 412 | if (this.right !== undefined) { 413 | parent.left = this.right 414 | this.right.parent = parent 415 | } else { 416 | parent.left = this.left 417 | if (this.left !== undefined) { 418 | this.left.parent = parent 419 | } 420 | } 421 | parent.updateMaxOfParents() 422 | parent.updateHeight() 423 | parent._rebalance() 424 | return this 425 | } else if (parent.right === this) { 426 | // One child or no child case on right side 427 | if (this.right !== undefined) { 428 | parent.right = this.right 429 | this.right.parent = parent 430 | } else { 431 | parent.right = this.left 432 | if (this.left !== undefined) { 433 | this.left.parent = parent 434 | } 435 | } 436 | parent.updateMaxOfParents() 437 | parent.updateHeight() 438 | parent._rebalance() 439 | return this 440 | } 441 | } 442 | 443 | // Make linter happy 444 | return undefined 445 | } 446 | } 447 | 448 | export class IntervalTree, N extends number | bigint = number> { 449 | public root?: Node 450 | public count = 0 451 | 452 | public insert(record: T) { 453 | if (record.low > record.high) { 454 | throw new Error('`low` value must be lower or equal to `high` value') 455 | } 456 | 457 | if (this.root === undefined) { 458 | // Base case: Tree is empty, new node becomes root 459 | this.root = new Node(this, record) 460 | this.count++ 461 | return true 462 | } else { 463 | // Otherwise, check if node already exists with the same key 464 | const node = this.root.searchExisting(record.low) 465 | if (node !== undefined) { 466 | // Check the records in this node if there already is the one with same low, high, data 467 | for (let i = 0; i < node.records.length; i++) { 468 | if (isSame(node.records[i], record)) { 469 | // This record is same as the one we're trying to insert; return false to indicate 470 | // nothing has been inserted 471 | return false 472 | } 473 | } 474 | 475 | // Add the record to the node 476 | node.records.push(record) 477 | 478 | // Update max of the node and its parents if necessary 479 | if (record.high > node.max) { 480 | node.max = record.high 481 | if (node.parent) { 482 | node.parent.updateMaxOfParents() 483 | } 484 | } 485 | this.count++ 486 | return true 487 | } else { 488 | // Node with this key doesn't already exist. Call insert function on root's node 489 | this.root.insert(record) 490 | this.count++ 491 | return true 492 | } 493 | } 494 | } 495 | 496 | public search(low: N, high: N) { 497 | if (this.root === undefined) { 498 | // Tree is empty; return empty array 499 | return [] 500 | } else { 501 | return this.root.search(low, high) 502 | } 503 | } 504 | 505 | public remove(record: T) { 506 | if (this.root === undefined) { 507 | // Tree is empty; nothing to remove 508 | return false 509 | } else { 510 | const node = this.root.searchExisting(record.low) 511 | if (node === undefined) { 512 | return false 513 | } else if (node.records.length > 1) { 514 | let removedRecord: T | undefined 515 | // Node with this key has 2 or more records. Find the one we need and remove it 516 | for (let i = 0; i < node.records.length; i++) { 517 | if (isSame(node.records[i], record)) { 518 | removedRecord = node.records[i] 519 | node.records.splice(i, 1) 520 | break 521 | } 522 | } 523 | 524 | if (removedRecord) { 525 | removedRecord = undefined 526 | // Update max of that node and its parents if necessary 527 | if (record.high === node.max) { 528 | const nodeHigh = node.getNodeHigh() 529 | if (node.left !== undefined && node.right !== undefined) { 530 | node.max = max(max(node.left.max, node.right.max), nodeHigh) 531 | } else if (node.left !== undefined && node.right === undefined) { 532 | node.max = max(node.left.max, nodeHigh) 533 | } else if (node.left === undefined && node.right !== undefined) { 534 | node.max = max(node.right.max, nodeHigh) 535 | } else { 536 | node.max = nodeHigh 537 | } 538 | if (node.parent) { 539 | node.parent.updateMaxOfParents() 540 | } 541 | } 542 | this.count-- 543 | return true 544 | } else { 545 | return false 546 | } 547 | } else if (node.records.length === 1) { 548 | // Node with this key has only 1 record. Check if the remaining record in this node is 549 | // actually the one we want to remove 550 | if (isSame(node.records[0], record)) { 551 | // The remaining record is the one we want to remove. Remove the whole node from the tree 552 | if (this.root.key === node.key) { 553 | // We're removing the root element. Create a dummy node that will temporarily take 554 | // root's parent role 555 | const rootParent = new Node(this, { low: record.low, high: record.low } as T) 556 | rootParent.left = this.root 557 | this.root.parent = rootParent 558 | let removedNode = this.root.remove(node) 559 | this.root = rootParent.left 560 | if (this.root !== undefined) { 561 | this.root.parent = undefined 562 | } 563 | if (removedNode) { 564 | removedNode = undefined 565 | this.count-- 566 | return true 567 | } else { 568 | return false 569 | } 570 | } else { 571 | let removedNode = this.root.remove(node) 572 | if (removedNode) { 573 | removedNode = undefined 574 | this.count-- 575 | return true 576 | } else { 577 | return false 578 | } 579 | } 580 | } else { 581 | // The remaining record is not the one we want to remove 582 | return false 583 | } 584 | } else { 585 | // No records at all in this node?! Shouldn't happen 586 | return false 587 | } 588 | } 589 | } 590 | 591 | public inOrder() { 592 | return new InOrder(this.root) 593 | } 594 | 595 | public reverseInOrder() { 596 | return new ReverseInOrder(this.root) 597 | } 598 | 599 | public preOrder() { 600 | return new PreOrder(this.root) 601 | } 602 | } 603 | 604 | export interface DataInterval extends Interval { 605 | data: T 606 | } 607 | 608 | /** 609 | * The default export just wraps the `IntervalTree`, while providing a simpler API. Check out the 610 | * README for description on how to use each. 611 | */ 612 | export default class DataIntervalTree { 613 | private tree = new IntervalTree, N>() 614 | 615 | public insert(low: N, high: N, data: T) { 616 | return this.tree.insert({ low, high, data }) 617 | } 618 | 619 | public remove(low: N, high: N, data: T) { 620 | return this.tree.remove({ low, high, data }) 621 | } 622 | 623 | public search(low: N, high: N) { 624 | return this.tree.search(low, high).map(v => v.data) 625 | } 626 | 627 | public inOrder() { 628 | return this.tree.inOrder() 629 | } 630 | 631 | public reverseInOrder() { 632 | return this.tree.reverseInOrder() 633 | } 634 | 635 | public preOrder() { 636 | return this.tree.preOrder() 637 | } 638 | 639 | get count() { 640 | return this.tree.count 641 | } 642 | } 643 | 644 | export class InOrder, N extends number | bigint = number> 645 | implements IterableIterator 646 | { 647 | private stack: Node[] = [] 648 | 649 | private currentNode?: Node 650 | private i: number 651 | 652 | constructor(startNode?: Node) { 653 | if (startNode !== undefined) { 654 | this.push(startNode) 655 | } 656 | } 657 | 658 | [Symbol.iterator]() { 659 | return this 660 | } 661 | 662 | public next(): IteratorResult { 663 | // Will only happen if stack is empty and pop is called 664 | if (this.currentNode === undefined) { 665 | return { 666 | done: true, 667 | value: undefined, 668 | } as any as IteratorResult 669 | } 670 | 671 | // Process this node 672 | if (this.i < this.currentNode.records.length) { 673 | return { 674 | done: false, 675 | value: this.currentNode.records[this.i++], 676 | } 677 | } 678 | 679 | if (this.currentNode.right !== undefined) { 680 | // Can we go right? 681 | this.push(this.currentNode.right) 682 | } else { 683 | // Otherwise go up 684 | // Might pop the last and set this.currentNode = undefined 685 | this.pop() 686 | } 687 | return this.next() 688 | } 689 | 690 | private push(node: Node) { 691 | this.currentNode = node 692 | this.i = 0 693 | 694 | while (this.currentNode.left !== undefined) { 695 | this.stack.push(this.currentNode) 696 | this.currentNode = this.currentNode.left 697 | } 698 | } 699 | 700 | private pop() { 701 | this.currentNode = this.stack.pop() 702 | this.i = 0 703 | } 704 | } 705 | 706 | export class ReverseInOrder, N extends number | bigint = number> 707 | implements IterableIterator 708 | { 709 | private stack: Node[] = [] 710 | 711 | private currentNode?: Node 712 | private i: number 713 | 714 | constructor(startNode?: Node) { 715 | if (startNode !== undefined) { 716 | this.push(startNode) 717 | } 718 | } 719 | 720 | [Symbol.iterator]() { 721 | return this 722 | } 723 | 724 | public next(): IteratorResult { 725 | // Will only happen if stack is empty and pop is called 726 | if (this.currentNode === undefined) { 727 | return { 728 | done: true, 729 | value: undefined, 730 | } as any as IteratorResult 731 | } 732 | 733 | // Process this node 734 | if (this.currentNode.records.length && this.i >= 0) { 735 | return { 736 | done: false, 737 | value: this.currentNode.records[this.i--], 738 | } 739 | } 740 | 741 | if (this.currentNode.left !== undefined) { 742 | // Can we go left? 743 | this.push(this.currentNode.left) 744 | } else { 745 | // Otherwise go up 746 | // Might pop the last and set this.currentNode = undefined 747 | this.pop() 748 | } 749 | return this.next() 750 | } 751 | 752 | private push(node: Node) { 753 | this.currentNode = node 754 | this.i = (this.currentNode?.records.length ?? 0) - 1 755 | 756 | while (this.currentNode.right !== undefined) { 757 | this.stack.push(this.currentNode) 758 | this.currentNode = this.currentNode.right 759 | this.i = (this.currentNode?.records.length ?? 0) - 1 760 | } 761 | } 762 | 763 | private pop() { 764 | this.currentNode = this.stack.pop() 765 | this.i = (this.currentNode?.records.length ?? 0) - 1 766 | } 767 | } 768 | 769 | export class PreOrder, N extends number | bigint = number> 770 | implements IterableIterator 771 | { 772 | private stack: Node[] = [] 773 | 774 | private currentNode?: Node 775 | private i = 0 776 | 777 | constructor(startNode?: Node) { 778 | this.currentNode = startNode 779 | } 780 | 781 | [Symbol.iterator]() { 782 | return this 783 | } 784 | 785 | public next(): IteratorResult { 786 | // Will only happen if stack is empty and pop is called, 787 | // which only happens if there is no right node (i.e we are done) 788 | if (this.currentNode === undefined) { 789 | return { 790 | done: true, 791 | value: undefined, 792 | } as any as IteratorResult 793 | } 794 | 795 | // Process this node 796 | if (this.i < this.currentNode.records.length) { 797 | return { 798 | done: false, 799 | value: this.currentNode.records[this.i++], 800 | } 801 | } 802 | 803 | if (this.currentNode.right !== undefined) { 804 | this.push(this.currentNode.right) 805 | } 806 | if (this.currentNode.left !== undefined) { 807 | this.push(this.currentNode.left) 808 | } 809 | this.pop() 810 | return this.next() 811 | } 812 | 813 | private push(node: Node) { 814 | this.stack.push(node) 815 | } 816 | 817 | private pop() { 818 | this.currentNode = this.stack.pop() 819 | this.i = 0 820 | } 821 | } 822 | --------------------------------------------------------------------------------