You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
388 lines
8.9 KiB
388 lines
8.9 KiB
3 years ago
|
/*
|
||
|
* Mnemonist StaticIntervalTree
|
||
|
* =============================
|
||
|
*
|
||
|
* JavaScript implementation of a static interval tree. This tree is static in
|
||
|
* that you are required to know all its items beforehand and to built it
|
||
|
* from an iterable.
|
||
|
*
|
||
|
* This implementation represents the interval tree as an augmented balanced
|
||
|
* binary search tree. It works by sorting the intervals by startpoint first
|
||
|
* then proceeds building the augmented balanced BST bottom-up from the
|
||
|
* sorted list.
|
||
|
*
|
||
|
* Note that this implementation considers every given intervals as closed for
|
||
|
* simplicity's sake.
|
||
|
*
|
||
|
* For more information: https://en.wikipedia.org/wiki/Interval_tree
|
||
|
*/
|
||
|
var iterables = require('./utils/iterables.js'),
|
||
|
typed = require('./utils/typed-arrays.js');
|
||
|
|
||
|
var FixedStack = require('./fixed-stack.js');
|
||
|
|
||
|
|
||
|
// TODO: pass index to getters
|
||
|
// TODO: custom comparison
|
||
|
// TODO: possibility to pass offset buffer
|
||
|
|
||
|
// TODO: intervals() => Symbol.iterator
|
||
|
// TODO: dfs()
|
||
|
|
||
|
/**
|
||
|
* Helpers.
|
||
|
*/
|
||
|
|
||
|
/**
|
||
|
* Recursive function building the BST from the sorted list of interval
|
||
|
* indices.
|
||
|
*
|
||
|
* @param {array} intervals - Array of intervals to index.
|
||
|
* @param {function} endGetter - Getter function for end of intervals.
|
||
|
* @param {array} sortedIndices - Sorted indices of the intervals.
|
||
|
* @param {array} tree - BST memory.
|
||
|
* @param {array} augmentations - Array of node augmentations.
|
||
|
* @param {number} i - BST index of current node.
|
||
|
* @param {number} low - Dichotomy low index.
|
||
|
* @param {number} high - Dichotomy high index.
|
||
|
* @return {number} - Created node augmentation value.
|
||
|
*/
|
||
|
function buildBST(
|
||
|
intervals,
|
||
|
endGetter,
|
||
|
sortedIndices,
|
||
|
tree,
|
||
|
augmentations,
|
||
|
i,
|
||
|
low,
|
||
|
high
|
||
|
) {
|
||
|
var mid = (low + (high - low) / 2) | 0,
|
||
|
midMinusOne = ~-mid,
|
||
|
midPlusOne = -~mid;
|
||
|
|
||
|
var current = sortedIndices[mid];
|
||
|
tree[i] = current + 1;
|
||
|
|
||
|
var end = endGetter ? endGetter(intervals[current]) : intervals[current][1];
|
||
|
|
||
|
var left = i * 2 + 1,
|
||
|
right = i * 2 + 2;
|
||
|
|
||
|
var leftEnd = -Infinity,
|
||
|
rightEnd = -Infinity;
|
||
|
|
||
|
if (low <= midMinusOne) {
|
||
|
leftEnd = buildBST(
|
||
|
intervals,
|
||
|
endGetter,
|
||
|
sortedIndices,
|
||
|
tree,
|
||
|
augmentations,
|
||
|
left,
|
||
|
low,
|
||
|
midMinusOne
|
||
|
);
|
||
|
}
|
||
|
|
||
|
if (midPlusOne <= high) {
|
||
|
rightEnd = buildBST(
|
||
|
intervals,
|
||
|
endGetter,
|
||
|
sortedIndices,
|
||
|
tree,
|
||
|
augmentations,
|
||
|
right,
|
||
|
midPlusOne,
|
||
|
high
|
||
|
);
|
||
|
}
|
||
|
|
||
|
var augmentation = Math.max(end, leftEnd, rightEnd);
|
||
|
|
||
|
var augmentationPointer = current;
|
||
|
|
||
|
if (augmentation === leftEnd)
|
||
|
augmentationPointer = augmentations[tree[left] - 1];
|
||
|
else if (augmentation === rightEnd)
|
||
|
augmentationPointer = augmentations[tree[right] - 1];
|
||
|
|
||
|
augmentations[current] = augmentationPointer;
|
||
|
|
||
|
return augmentation;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* StaticIntervalTree.
|
||
|
*
|
||
|
* @constructor
|
||
|
* @param {array} intervals - Array of intervals to index.
|
||
|
* @param {array<function>} getters - Optional getters.
|
||
|
*/
|
||
|
function StaticIntervalTree(intervals, getters) {
|
||
|
|
||
|
// Properties
|
||
|
this.size = intervals.length;
|
||
|
this.intervals = intervals;
|
||
|
|
||
|
var startGetter = null,
|
||
|
endGetter = null;
|
||
|
|
||
|
if (Array.isArray(getters)) {
|
||
|
startGetter = getters[0];
|
||
|
endGetter = getters[1];
|
||
|
}
|
||
|
|
||
|
// Building the indices array
|
||
|
var length = intervals.length;
|
||
|
|
||
|
var IndicesArray = typed.getPointerArray(length + 1);
|
||
|
|
||
|
var indices = new IndicesArray(length);
|
||
|
|
||
|
var i;
|
||
|
|
||
|
for (i = 1; i < length; i++)
|
||
|
indices[i] = i;
|
||
|
|
||
|
// Sorting indices array
|
||
|
// TODO: check if some version of radix sort can outperform this part
|
||
|
indices.sort(function(a, b) {
|
||
|
a = intervals[a];
|
||
|
b = intervals[b];
|
||
|
|
||
|
if (startGetter) {
|
||
|
a = startGetter(a);
|
||
|
b = startGetter(b);
|
||
|
}
|
||
|
else {
|
||
|
a = a[0];
|
||
|
b = b[0];
|
||
|
}
|
||
|
|
||
|
if (a < b)
|
||
|
return -1;
|
||
|
|
||
|
if (a > b)
|
||
|
return 1;
|
||
|
|
||
|
// TODO: use getters
|
||
|
// TODO: this ordering has the following invariant: if query interval
|
||
|
// contains [nodeStart, max], then whole right subtree can be collected
|
||
|
// a = a[1];
|
||
|
// b = b[1];
|
||
|
|
||
|
// if (a < b)
|
||
|
// return 1;
|
||
|
|
||
|
// if (a > b)
|
||
|
// return -1;
|
||
|
|
||
|
return 0;
|
||
|
});
|
||
|
|
||
|
// Building the binary tree
|
||
|
var height = Math.ceil(Math.log2(length + 1)),
|
||
|
treeSize = Math.pow(2, height) - 1;
|
||
|
|
||
|
var tree = new IndicesArray(treeSize);
|
||
|
|
||
|
var augmentations = new IndicesArray(length);
|
||
|
|
||
|
buildBST(
|
||
|
intervals,
|
||
|
endGetter,
|
||
|
indices,
|
||
|
tree,
|
||
|
augmentations,
|
||
|
0,
|
||
|
0,
|
||
|
length - 1
|
||
|
);
|
||
|
|
||
|
// Dropping indices
|
||
|
indices = null;
|
||
|
|
||
|
// Storing necessary information
|
||
|
this.height = height;
|
||
|
this.tree = tree;
|
||
|
this.augmentations = augmentations;
|
||
|
this.startGetter = startGetter;
|
||
|
this.endGetter = endGetter;
|
||
|
|
||
|
// Initializing DFS stack
|
||
|
this.stack = new FixedStack(IndicesArray, this.height);
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Method returning a list of intervals containing the given point.
|
||
|
*
|
||
|
* @param {any} point - Target point.
|
||
|
* @return {array}
|
||
|
*/
|
||
|
StaticIntervalTree.prototype.intervalsContainingPoint = function(point) {
|
||
|
var matches = [];
|
||
|
|
||
|
var stack = this.stack;
|
||
|
|
||
|
stack.clear();
|
||
|
stack.push(0);
|
||
|
|
||
|
var l = this.tree.length;
|
||
|
|
||
|
var bstIndex,
|
||
|
intervalIndex,
|
||
|
interval,
|
||
|
maxInterval,
|
||
|
start,
|
||
|
end,
|
||
|
max,
|
||
|
left,
|
||
|
right;
|
||
|
|
||
|
while (stack.size) {
|
||
|
bstIndex = stack.pop();
|
||
|
intervalIndex = this.tree[bstIndex] - 1;
|
||
|
interval = this.intervals[intervalIndex];
|
||
|
maxInterval = this.intervals[this.augmentations[intervalIndex]];
|
||
|
|
||
|
max = this.endGetter ? this.endGetter(maxInterval) : maxInterval[1];
|
||
|
|
||
|
// No possible match, point is farther right than the max end value
|
||
|
if (point > max)
|
||
|
continue;
|
||
|
|
||
|
// Searching left
|
||
|
left = bstIndex * 2 + 1;
|
||
|
|
||
|
if (left < l && this.tree[left] !== 0)
|
||
|
stack.push(left);
|
||
|
|
||
|
start = this.startGetter ? this.startGetter(interval) : interval[0];
|
||
|
end = this.endGetter ? this.endGetter(interval) : interval[1];
|
||
|
|
||
|
// Checking current node
|
||
|
if (point >= start && point <= end)
|
||
|
matches.push(interval);
|
||
|
|
||
|
// If the point is to the left of the start of the current interval,
|
||
|
// then it cannot be in the right child
|
||
|
if (point < start)
|
||
|
continue;
|
||
|
|
||
|
// Searching right
|
||
|
right = bstIndex * 2 + 2;
|
||
|
|
||
|
if (right < l && this.tree[right] !== 0)
|
||
|
stack.push(right);
|
||
|
}
|
||
|
|
||
|
return matches;
|
||
|
};
|
||
|
|
||
|
/**
|
||
|
* Method returning a list of intervals overlapping the given interval.
|
||
|
*
|
||
|
* @param {any} interval - Target interval.
|
||
|
* @return {array}
|
||
|
*/
|
||
|
StaticIntervalTree.prototype.intervalsOverlappingInterval = function(interval) {
|
||
|
var intervalStart = this.startGetter ? this.startGetter(interval) : interval[0],
|
||
|
intervalEnd = this.endGetter ? this.endGetter(interval) : interval[1];
|
||
|
|
||
|
var matches = [];
|
||
|
|
||
|
var stack = this.stack;
|
||
|
|
||
|
stack.clear();
|
||
|
stack.push(0);
|
||
|
|
||
|
var l = this.tree.length;
|
||
|
|
||
|
var bstIndex,
|
||
|
intervalIndex,
|
||
|
currentInterval,
|
||
|
maxInterval,
|
||
|
start,
|
||
|
end,
|
||
|
max,
|
||
|
left,
|
||
|
right;
|
||
|
|
||
|
while (stack.size) {
|
||
|
bstIndex = stack.pop();
|
||
|
intervalIndex = this.tree[bstIndex] - 1;
|
||
|
currentInterval = this.intervals[intervalIndex];
|
||
|
maxInterval = this.intervals[this.augmentations[intervalIndex]];
|
||
|
|
||
|
max = this.endGetter ? this.endGetter(maxInterval) : maxInterval[1];
|
||
|
|
||
|
// No possible match, start is farther right than the max end value
|
||
|
if (intervalStart > max)
|
||
|
continue;
|
||
|
|
||
|
// Searching left
|
||
|
left = bstIndex * 2 + 1;
|
||
|
|
||
|
if (left < l && this.tree[left] !== 0)
|
||
|
stack.push(left);
|
||
|
|
||
|
start = this.startGetter ? this.startGetter(currentInterval) : currentInterval[0];
|
||
|
end = this.endGetter ? this.endGetter(currentInterval) : currentInterval[1];
|
||
|
|
||
|
// Checking current node
|
||
|
if (intervalEnd >= start && intervalStart <= end)
|
||
|
matches.push(currentInterval);
|
||
|
|
||
|
// If the end is to the left of the start of the current interval,
|
||
|
// then it cannot be in the right child
|
||
|
if (intervalEnd < start)
|
||
|
continue;
|
||
|
|
||
|
// Searching right
|
||
|
right = bstIndex * 2 + 2;
|
||
|
|
||
|
if (right < l && this.tree[right] !== 0)
|
||
|
stack.push(right);
|
||
|
}
|
||
|
|
||
|
return matches;
|
||
|
};
|
||
|
|
||
|
/**
|
||
|
* Convenience known methods.
|
||
|
*/
|
||
|
StaticIntervalTree.prototype.inspect = function() {
|
||
|
var proxy = this.intervals.slice();
|
||
|
|
||
|
// Trick so that node displays the name of the constructor
|
||
|
Object.defineProperty(proxy, 'constructor', {
|
||
|
value: StaticIntervalTree,
|
||
|
enumerable: false
|
||
|
});
|
||
|
|
||
|
return proxy;
|
||
|
};
|
||
|
|
||
|
if (typeof Symbol !== 'undefined')
|
||
|
StaticIntervalTree.prototype[Symbol.for('nodejs.util.inspect.custom')] = StaticIntervalTree.prototype.inspect;
|
||
|
|
||
|
/**
|
||
|
* Static @.from function taking an arbitrary iterable & converting it into
|
||
|
* a structure.
|
||
|
*
|
||
|
* @param {Iterable} iterable - Target iterable.
|
||
|
* @return {StaticIntervalTree}
|
||
|
*/
|
||
|
StaticIntervalTree.from = function(iterable, getters) {
|
||
|
if (iterables.isArrayLike(iterable))
|
||
|
return new StaticIntervalTree(iterable, getters);
|
||
|
|
||
|
return new StaticIntervalTree(Array.from(iterable), getters);
|
||
|
};
|
||
|
|
||
|
/**
|
||
|
* Exporting.
|
||
|
*/
|
||
|
module.exports = StaticIntervalTree;
|