UNPKG

tiny-tree

Version:

Efficient, no-dependency b-tree and binary search tree for node or the browser

914 lines (745 loc) 25.6 kB
(function (global, factory) { typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports) : typeof define === 'function' && define.amd ? define(['exports'], factory) : (global = typeof globalThis !== 'undefined' ? globalThis : global || self, factory(global.tinyTree = {})); }(this, (function (exports) { 'use strict'; class BTreeNode { constructor(degree, keys, values, children) { this.size = 0; this.keys = new Array(degree - 1); this.values = new Array(degree - 1); this.children = null; if (keys) { for (let i = 0; i < keys.length; i++) { this.keys[i] = keys[i]; this.values[i] = values[i]; this.size += 1; } if (children) { this.children = new Array(degree); for (let i = 0; i < children.length; i++) { this.children[i] = children[i]; this.size += children[i].size; } } } } set(key, value, bulkMode) { let i; for (i = 0; i < this.keys.length; i++) { if (this.keys[i] == null || this.keys[i] > key) { break; } if (this.keys[i] === key) { this.values[i] = value; return null; } } if (this.isLeafNode) { let insertResult = this._splice(i, key, value, null, bulkMode); if (insertResult) return insertResult; return null; } let oldSize = this.children[i].size; let insertResult = this.children[i].set(key, value, bulkMode); if (insertResult) { this.size += this.children[i].size - oldSize; return this._splice(i, insertResult.keys[0], insertResult.values[0], insertResult.children, bulkMode); } else { this.size += this.children[i].size - oldSize; } return null; } completeBulkLoad() { // after bulk loading, the right-hand nodes may need merging if (this.children) { for (let i = this.children.length - 1; i >= 0; i--) { if (this.children[i]) { if (this.children[i].needsMerge) { this._mergeChild(i); } if (!this.children[i].isLeafNode) { this.children[i].completeBulkLoad(); } return; } } } } _splice(i, key, value, children, bulkMode) { if (this.hasRoom) { this._inject(i, "LEFT", {key: key, value: value, child: children ? children[0] : null}); if (children) { if (this.children[i + 1]) { this.size -= this.children[i + 1].size; } this.children[i + 1] = children[1]; this.size += children[1].size; } return null; } this.keys.splice(i, 0, key); this.values.splice(i, 0, value); this.size += 1; if (children) { this.size -= this.children[i].size; this.size += children[0].size; this.size += children[1].size; this.children.splice(i, 1, children[0], children[1]); } return this._split(bulkMode); } _split(bulkMode) { // n.b.: all arrays are oversized by one const degree = this.keys.length; const splitPoint = bulkMode ? this.keys.length - 1 : Math.floor(this.keys.length / 2); let rightChildren = null; if (!this.isLeafNode) { rightChildren = this.children.slice(splitPoint + 1); for (let i = splitPoint + 1; i <= degree; i++) { this.size -= this.children[i].size; this.children[i] = null; } this.children.length = degree; } let newRightNode = new this.constructor(degree, this.keys.slice(splitPoint + 1), this.values.slice(splitPoint + 1), rightChildren); let parentKey = this.keys[splitPoint]; let parentValue = this.values[splitPoint]; for (let i = splitPoint; i <= degree - 1; i++) { this.size -= 1; this.keys[i] = null; this.values[i] = null; } this.keys.length = degree - 1; this.values.length = degree - 1; return new this.constructor(degree, [parentKey], [parentValue], [ this, newRightNode ]); } delete(key) { let i; for (i = 0; i < this.keys.length; i++) { if (this.keys[i] == null || this.keys[i] > key) { break; } if (this.keys[i] === key) { let oldSize = this.size; if (this.isLeafNode) { this._extract(i, "NULL", true); } else { this._removeSplittingKey(i); } this.size = oldSize - 1; return; } } if (this.isLeafNode) return; let child = this.children[i]; let oldSize = child.size; child.delete(key); this.size += child.size - oldSize; if (child.needsMerge) this._mergeChild(i); } _removeSplittingKey(iKey) { let promote = this.children[iKey]._promoteMax(); this.keys[iKey] = promote.key; this.values[iKey] = promote.value; if (!promote.needsMerge) return; if (this.children[iKey].isLeafNode) { this._mergeChild(iKey); } else { this.children[iKey]._mergeMax(); if (this.children[iKey].needsMerge) { this._mergeChild(iKey); } } } _mergeMax() { let nFilled = this.filledKeys; if (this.children[nFilled].isLeafNode) { this._mergeChild(nFilled); } else { this.children[nFilled]._mergeMax(); if (this.children[nFilled].needsMerge) { this._mergeChild(nFilled); } } } _mergeChild(iChild) { let left, right, current = this.children[iChild]; const mergeNodes = (target, source, iParentKey) => { let iKey = target.filledKeys; target.keys[iKey] = this.keys[iParentKey]; target.values[iKey] = this.values[iParentKey]; target.size += 1; iKey++; let i; for (i = 0; i < source.filledKeys; i++) { target.keys[iKey] = source.keys[i]; target.values[iKey] = source.values[i]; target.size += 1; if (source.children) { target.children[iKey] = source.children[i]; target.size += source.children[i].size; } iKey++; } if (source.children) { target.children[iKey] = source.children[i]; target.size += source.children[i].size; } }; if (iChild > 0) left = this.children[iChild - 1]; if (iChild + 1 < this.children.length) right = this.children[iChild + 1]; if (left != null && left.filledKeys > left.minimumFilledKeys) { while (current.needsMerge) { let iParentKey = iChild - 1; let extract = left._extractRight(); current._inject(0, "LEFT", { key: this.keys[iParentKey], value: this.values[iParentKey], child: extract.child }); this.keys[iParentKey] = extract.key; this.values[iParentKey] = extract.value; } return false; } if (right != null && right.filledKeys > right.minimumFilledKeys) { while (current.needsMerge) { let iParentKey = iChild; let extract = right._extract(0, "LEFT"); current._injectRight({ key: this.keys[iParentKey], value: this.values[iParentKey], child: extract.child }); this.keys[iParentKey] = extract.key; this.values[iParentKey] = extract.value; } return false; } if (left) { mergeNodes(left, current, iChild - 1); this._extract(iChild - 1, "RIGHT", true); } else { mergeNodes(current, right, iChild); this._extract(iChild, "RIGHT", true); } return this.needsMerge; } _inject(iKey, side, info) { let nFilled = this.filledKeys; arrayShiftInsert(this.keys, iKey, info.key, nFilled); arrayShiftInsert(this.values, iKey, info.value, nFilled); this.size += 1; if (info.child) { arrayShiftInsert(this.children, side === "LEFT" ? iKey : iKey + 1, info.child, nFilled + 1); this.size += info.child.size; } } _injectRight(info) { let nFilled = this.filledKeys; arrayShiftInsert(this.keys, nFilled, info.key, nFilled); arrayShiftInsert(this.values, nFilled, info.value, nFilled); this.size += 1; if (info.child) { arrayShiftInsert(this.children, nFilled + 1, info.child, nFilled + 1); this.size += info.child.size; } } _extract(iKey, side, preserveSize) { let nFilled = this.filledKeys; let result = { key: arrayShiftDelete(this.keys, iKey, nFilled), value: arrayShiftDelete(this.values, iKey, nFilled), child: this.isLeafNode ? null : arrayShiftDelete(this.children, side === "LEFT" ? iKey : iKey + 1, nFilled + 1) }; if (!preserveSize) { this.size -= 1; if (result.child) this.size -= result.child.size; } return result; } _extractRight(preserveSize) { let nFilled = this.filledKeys; let result = { key: arrayShiftDelete(this.keys, nFilled - 1, nFilled), value: arrayShiftDelete(this.values, nFilled - 1, nFilled), child: this.isLeafNode ? null : arrayShiftDelete(this.children, nFilled, nFilled + 1) }; if (!preserveSize) { this.size -= 1; if (result.child) this.size -= result.child.size; } return result; } _promoteMax() { if (this.isLeafNode) { let result = this._extractRight(); result.needsMerge = this.needsMerge; return result; } let child = this.children[this.filledKeys]; let oldSize = child.size; let result = child._promoteMax(); this.size += child.size - oldSize; return result; } _updateStats(stats, depth) { depth++; stats.depth = Math.max(stats.depth, depth); stats.nodes++; stats.keySlots += this.keys.length; stats.filledKeySlots += this.filledKeys; if (!this.hasRoom) stats.saturatedNodes++; if (this.children) { for (let i = 0; i < this.children.length; i++) { if (!this.children[i]) break; this.children[i]._updateStats(stats, depth); } } } get isLeafNode() {return this.children == null} get hasRoom() {return this.keys[this.keys.length - 1] == null} get filledKeys() { for (let i = this.keys.length - 1; i >= 0; i--) { if (this.keys[i] != null) return i + 1; } return 0; } get minimumFilledKeys() { return Math.floor(this.keys.length / 2) } get needsMerge() { return this.keys[this.minimumFilledKeys - 1] == null } } function arrayShiftInsert(arr, i, value, nFilled) { for (let j = nFilled - 1; j >= i; j--) { arr[j + 1] = arr[j]; } arr[i] = value; } function arrayShiftDelete(arr, i, nFilled) { let result = arr[i]; for (let j = i + 1; j < nFilled; j++) { arr[j - 1] = arr[j]; } arr[nFilled - 1] = null; return result; } class BTree { constructor(options) { options = options || {}; this._degree = options.degree >= 3 ? options.degree : 15; this.clear(); } clear() { this._root = new BTreeNode(this._degree); } get(key) { let current = this._root; while (current) { let i; for (i = 0; i < current.keys.length; i++) { if (current.keys[i] == null || current.keys[i] > key) { break; } if (current.keys[i] === key) { return current.values[i]; } } current = current.children ? current.children[i] : null; } return undefined; } getByIndex(index) { let current = this._root, iThis = 0; if (index < 0) return undefined; if (index >= this.size) return undefined; while (true) { let i; if (current.isLeafNode) { return current.values[index - iThis]; } for (i = 0; i < current.keys.length; i++) { if (current.keys[i] == null) break; if (index < iThis + current.children[i].size) { break; } else { iThis += current.children[i].size; } if (iThis === index) { return current.values[i]; } else { iThis++; } } current = current.children[i]; } } _indexAtOrAboveKey(key) { let current = this._root; let index = 0; while (current) { let i; for (i = 0; i < current.keys.length; i++) { if (current.keys[i] == null) { break; } else if (current.keys[i] < key) { if (current.children) { index += current.children[i].size; } index += 1; } else if (current.keys[i] === key) { if (current.children) { index += current.children[i].size; } return {index: index, key: current.keys[i]}; } else { if (current.children) { break; } else { return {index: index, key: current.keys[i] }; } } } current = current.children ? current.children[i] : null; } return null; } _indexAtOrBelowKey(key) { let current = this._root; let index = this.size - 1; while (current) { let i; for (i = current.keys.length - 1; i >= 0; i--) { if (current.keys[i] == null) ; else if (current.keys[i] > key) { if (current.children) { index -= current.children[i + 1].size; } index -= 1; } else if (current.keys[i] === key) { if (current.children) { index -= current.children[i + 1].size; } return {index: index, key: current.keys[i]}; } else { if (current.children) { break; } else { return {index: index, key: current.keys[i] }; } } } current = current.children ? current.children[i + 1] : null; } return null; } toArray(bounds, valuesOnly) { let indexInfo = this._boundsToIndex(bounds); return this.toArrayByIndex(indexInfo[0], indexInfo[1] - indexInfo[0] + 1, valuesOnly); } _boundsToIndex(bounds) { let start = 0, end = this.size - 1; if (bounds) { if (bounds.min != null) { let indexInfo = this._indexAtOrAboveKey(bounds.min); start = indexInfo.index; } else if (bounds.minInclusive != null) { let indexInfo = this._indexAtOrAboveKey(bounds.minInclusive); start = indexInfo.index; } else if (bounds.minExclusive != null) { let indexInfo = this._indexAtOrAboveKey(bounds.minExclusive); start = indexInfo.index; if (indexInfo.key === bounds.minExclusive) start += 1; } if (bounds.max != null) { let indexInfo = this._indexAtOrBelowKey(bounds.max); end = indexInfo.index; if (indexInfo.key === bounds.max) end -= 1; } else if (bounds.maxExclusive != null) { let indexInfo = this._indexAtOrBelowKey(bounds.maxExclusive); end = indexInfo.index; if (indexInfo.key === bounds.maxExclusive) end -= 1; } else if (bounds.maxInclusive != null) { let indexInfo = this._indexAtOrBelowKey(bounds.maxInclusive); end = indexInfo.index; } } return [start, end] } toArrayByIndex(start, count, valuesOnly) { let stack = [], ctx, index, result = [], end = start + count - 1; if (start < 0) { count += start; start = 0; } if (count < 1) return []; ctx = {node: this._root, iKey: 0, nKeys: this._root.filledKeys, isLeafNode: this._root.isLeafNode, phase: 0}; index = 0; while (ctx) { if (index > end) return result; if (ctx.isLeafNode) { if (index + ctx.node.size - 1 < start) { // skip this node index += ctx.node.size; } else { for (let i = 0; i < ctx.nKeys; i++) { if (index < start) { // skip this key index += 1; } else { if (index >= start && index <= end) { result.push(valuesOnly ? ctx.node.values[i] : [ctx.node.keys[i], ctx.node.values[i]]); } index++; } } } ctx = stack.pop(); } else if (ctx.phase === 0) { // Child segment let child = ctx.node.children[ctx.iKey]; ctx.phase = 1; if (index + child.size - 1 < start) { // skip this child index += child.size; } else { // traverse into child stack.push(ctx); ctx = {node: child, iKey: 0, nKeys: child.filledKeys, isLeafNode: child.isLeafNode, phase: 0}; } } else { // Key segment if (ctx.iKey === ctx.nKeys) { ctx = stack.pop(); } else { if (index < start) { // skip this key index += 1; } else { if (index >= start && index <= end) { result.push(valuesOnly ? ctx.node.values[ctx.iKey] : [ctx.node.keys[ctx.iKey], ctx.node.values[ctx.iKey]]); } index++; } ctx.iKey++; ctx.phase = 0; } } } return result; } set(key, value) { let result = this._root.set(key, value, false); if (result) this._root = result; } bulkLoad(data) { if (this.size) throw new Error("Bulk load not allowed on non-empty trees"); if (!data.length) return; let lastKey = data[0][0]; for (let item of data) { if (!(item[0] >= lastKey)) throw new Error("Keys must be sorted in ascending order"); lastKey = item[0]; let result = this._root.set(item[0], item[1], true); if (result) this._root = result; } this._root.completeBulkLoad(); } delete(key) { this._root.delete(key); if (!this._root.isLeafNode && this._root.keys[0] == null) { this._root = this._root.children[0]; } } getStats() { let result = { degree: this._degree, size: this.size, depth: 0, fillFactor: null, nodes: 0, saturationFactor: null, keySlots: 0, filledKeySlots: 0, saturatedNodes: 0, }; this._root._updateStats(result, 0); result.fillFactor = result.filledKeySlots / result.keySlots; result.saturationFactor = result.saturatedNodes / result.nodes; return result; } get size() {return this._root.size} } // Returns the largest `i` from the sorted array `arr` such that `arr[i] <= value`; function indexAtOrBelow(arr, value) { let i, lo, hi; if (!arr.length || arr[0] > value) { return -1; } if (arr[arr.length - 1] <= value) { return arr.length - 1; } lo = 0; hi = arr.length - 1; while (hi - lo > 1) { i = lo + ((hi - lo) >> 1); if (arr[i] > value) { hi = i; } else { lo = i; } } return lo; } // Returns the smallest `i` from the sorted array `arr` such that `arr[i] >= value`; function indexAtOrAbove(arr, value) { if (!arr.length) return -1; if (arr[0] > value) return 0; if (arr[arr.length - 1] < value) return -1; let lo = 0; let hi = arr.length - 1; while (hi - lo > 1) { let i = lo + ((hi - lo) >> 1); if (arr[i] > value) { hi = i; } else { lo = i; } } if (arr[lo] === value) return lo; return hi; } // Returns `i` from the sorted array `arr` such that `arr[i] === value`; function indexOf(arr, value) { if (!arr.length) return -1; let lo = 0; let hi = arr.length - 1; while (hi - lo > 1) { let i = lo + ((hi - lo) >> 1); if (arr[i] > value) { hi = i; } else { lo = i; } } if (arr[lo] === value) return lo; if (arr[hi] === value) return hi; return -1; } class ArrayTree { constructor() { this.clear(); } clear() { this._keys = []; this._values = []; } get(key) { let index = indexOf(this._keys, key); return index >= 0 ? this._values[index] : undefined; } getByIndex(index) { if (index < 0) return undefined; if (index >= this.size) return undefined; return this._values[index]; } _indexAtOrAboveKey(key) { let index = indexAtOrAbove(this._keys, key); if (index < 0) return null; return {index: index, key: this._keys[index]}; } _indexAtOrBelowKey(key) { let index = indexAtOrBelow(this._keys, key); if (index < 0) return null; return {index: index, key: this._keys[index]}; } toArray(bounds, valuesOnly) { let indexInfo = this._boundsToIndex(bounds); return this.toArrayByIndex(indexInfo[0], indexInfo[1] - indexInfo[0] + 1, valuesOnly); } _boundsToIndex(bounds) { let start = 0, end = this.size - 1; if (bounds) { if (bounds.min != null) { let indexInfo = this._indexAtOrAboveKey(bounds.min); start = indexInfo.index; } else if (bounds.minInclusive != null) { let indexInfo = this._indexAtOrAboveKey(bounds.minInclusive); start = indexInfo.index; } else if (bounds.minExclusive != null) { let indexInfo = this._indexAtOrAboveKey(bounds.minExclusive); start = indexInfo.index; if (indexInfo.key === bounds.minExclusive) start += 1; } if (bounds.max != null) { let indexInfo = this._indexAtOrBelowKey(bounds.max); end = indexInfo.index; if (indexInfo.key === bounds.max) end -= 1; } else if (bounds.maxExclusive != null) { let indexInfo = this._indexAtOrBelowKey(bounds.maxExclusive); end = indexInfo.index; if (indexInfo.key === bounds.maxExclusive) end -= 1; } else if (bounds.maxInclusive != null) { let indexInfo = this._indexAtOrBelowKey(bounds.maxInclusive); end = indexInfo.index; } } return [start, end] } toArrayByIndex(start, count, valuesOnly) { if (start < 0) { count += start; start = 0; } if (count < 1) return []; if (valuesOnly) return this._values.slice(start, start + count); let result = []; for (let i = start; i < start + count; i++) { result.push([this._keys[i], this._values[i]]); } return result; } set(key, value) { let index = indexAtOrBelow(this._keys, key); if (this._keys[index] === key) { this._values[index] = value; return; } this._keys.splice(index + 1, 0, key); this._values.splice(index + 1, 0, value); } bulkLoad(data) { let index = 0; if (this.size) throw new Error("Bulk load not allowed on non-empty trees"); if (!data.length) return; this._keys.push(data[0][0]); this._values.push(data[0][1]); let lastKey = data[0][0]; for (let i = 1; i < data.length; i++) { let item = data[i]; if (!(item[0] >= lastKey)) throw new Error("Keys must be sorted in ascending order"); if (item[0] === lastKey) { this._values[index] = item[1]; } else { this._keys.push(item[0]); this._values.push(item[1]); index++; } } } delete(key) { let index = indexOf(this._keys, key); if (index >= 0) { this._keys.splice(index, 1); this._values.splice(index, 1); } } getStats() { return { size: this.size }; } get size() {return this._keys.length} } exports.ArrayTree = ArrayTree; exports.BTree = BTree; Object.defineProperty(exports, '__esModule', { value: true }); })));