flo-ll-rb-tree
Version:
A simple left leaning red black tree implementation.
667 lines (541 loc) • 14.9 kB
text/typescript
// Concise, Destructive, Left Leaning Red Black Tree implementation.
// See: https://www.cs.princeton.edu/~rs/talks/LLRB/LLRB.pdf
// See: https://en.wikipedia.org/wiki/Left-leaning_red%E2%80%93black_tree
// See: http://www.teachsolaisgames.com/articles/balanced_left_leaning.html
const LEFT = -1;
const RIGHT = 1;
type Dir = -1|1;
const BLACK = 1;
const RED = 0;
type Color = 0|1;
/**
* Red Black Tree node.
*/
class Node<T> {
public color: Color = RED;
public parent: Node<T> | undefined;
public extras?: T[];
"-1": Node<T> | undefined;
"1" : Node<T> | undefined;
constructor(public datum: T) {}
}
function isRed<T>(node: Node<T> | undefined): boolean {
return !!node && node.color === RED;
}
class LlRbTree<T> {
public root: Node<T> | undefined;
/**
* The number of nodes in the tree (that equals the number of values in the
* tree not counting duplicates).
*/
public nodeCount: number;
/**
* The number of values in the tree.
*/
public valueCount: number;
/**
* @param compare a comparator function
* @param duplicatesAllowed defaults to `true`; if `false` then if a
* duplicate is inserted (as per the equivalence relation induced by the
* compare function) then replace it; if `true` then instead still insert
* it (so there can be multiple nodes with the same value in the tree)
* @param data an optional initial array of data
*/
constructor(
public compare: (a: T, b: T) => number,
public duplicatesAllowed = true,
data?: T[]) {
this.root = undefined;
this.nodeCount = 0;
this.valueCount = 0;
if (!data) { return; }
for (const datum of data) {
this.insert(datum);
}
}
public isEmpty() { return !this.root; }
/**
* Find and returns the (first) node in the tree with the given datum using
* the tree compare function. Returns `undefined` if the node was not found.
*/
public find(datum: T): Node<T> | undefined {
const tree = this;
let node = this.root;
while (node) {
const c = tree.compare(datum, node.datum);
if (c === 0) { return node; }
node = node[c > 0 ? RIGHT : LEFT];
}
return undefined;
}
/**
* Returns an ordered (by the tree compare function) array of data as
* contained in the nodes of the tree by doing an in order traversal.
*/
public toArrayInOrder(): T[] {
const values: T[] = [];
f(this.root);
function f(node: Node<T> | undefined): void {
if (!node) { return; }
f(node[LEFT]);
values.push(node.datum);
if (node.extras !== undefined) {
values.push(...node.extras);
}
f(node[RIGHT]);
}
return values;
}
public insertMulti(data: T[]): void {
const tree = this;
for (const datum of data) {
tree.insert(datum);
}
}
/**
* Inserts a node with the given datum into the tree.
*/
public insert(datum: T): void {
const tree = this;
tree.root = f(tree.root, datum);
tree.root.color = BLACK;
tree.root.parent = undefined;
function f(h: Node<T> | undefined, datum: T): Node<T> {
if (h === undefined) {
tree.valueCount++;
tree.nodeCount++;
return new Node(datum);
}
const c = tree.compare(datum, h.datum);
if (c === 0) {
if (tree.duplicatesAllowed) {
tree.valueCount++;
if (h.extras === undefined) {
h.extras = [datum];
} else {
h.extras.push(datum);
}
} else {
h.datum = datum; // replace old value
}
}
if (c !== 0) {
const dir = c > 0 ? RIGHT : LEFT;
h[dir] = f(h[dir], datum);
h[dir]!.parent = h;
}
if (isRed(h[RIGHT]) && !isRed(h[LEFT])) {
h = rotate(LEFT, h);
}
if (isRed(h[LEFT]) && isRed(h[LEFT]![LEFT])) {
h = rotate(RIGHT, h);
}
if (isRed(h[LEFT]) && isRed(h[RIGHT])) {
flipColors(h);
}
return h;
}
}
/**
* Removes an item from the tree based on the given datum and returns the
* item that was removed or `undefined` if nothing was removed.
*
* @param datum
* @param all defaults to `false`; if `true` and duplicates exist, remove all
* @param compareStrict if provided then only delete an item if it passes the
* strict comparison function, i.e. if `compareStrict(item,node_value) === true`.
*/
public remove(
datum: T,
all = false,
compareStrict?: (t1: T, t2: T) => boolean): T | undefined {
const tree = this;
if (tree.root === undefined) { return undefined; }
let removed = undefined;
const root = f(tree.root, datum);
if (root === null) { return undefined; }
tree.root = root;
if (tree.root !== undefined) {
tree.root.color = BLACK;
tree.root.parent = undefined;
}
return removed;
function f(h: Node<T>, datum: T): Node<T> | null | undefined {
const theresExtras = tree.duplicatesAllowed && h.extras !== undefined;
let c = tree.compare(datum, h.datum);
if ((c < 0 && !h[LEFT]) || (c > 0 && !h[RIGHT])) {
return h; // end reached - no match
}
if (c < 0) {
if (!isRed(h[LEFT]) &&
!isRed(h[LEFT]![LEFT])) {
h = moveRedLeft(h);
}
const g = f(h[LEFT]!, datum);
if (g === null) { return null; }
h[LEFT] = g;
if (h[LEFT]) {
h[LEFT]!.parent = h;
}
return fixUp(h);
}
if (isRed(h[LEFT])) {
h = rotate(RIGHT, h);
c = tree.compare(datum, h.datum);
}
if (c === 0 && !h[RIGHT]) {
if (theresExtras && !all) {
// There are multiple items at this node.
if (compareStrict === undefined ||
(compareStrict(datum, h.datum) === true)) {
removed = h.datum;
h.datum = h.extras!.pop()!;
tree.valueCount--;
if (h.extras!.length === 0) {
h.extras = undefined;
}
return h;
} else {
const extras = h.extras!;
for (let i=0; i<extras.length; i++) {
if (compareStrict(datum, extras[i]) === true) {
removed = extras.splice(i,1)[0];
tree.valueCount--;
if (extras.length === 0) {
h.extras = undefined;
}
return h;
}
}
return null;
}
}
if (compareStrict === undefined ||
(compareStrict(datum, h.datum) === true)) {
removed = h.datum;
tree.valueCount -= 1 + (theresExtras ? h.extras!.length : 0);
tree.nodeCount--;
return undefined;
} else {
return null; // no match
}
}
if (!isRed(h[RIGHT]) &&
!isRed(h[RIGHT]![LEFT])) {
h = moveRedRight(h);
c = tree.compare(datum, h.datum);
}
if (c === 0) {
if (theresExtras && !all) {
// There are multiple items at this node.
if (compareStrict === undefined ||
(compareStrict(datum, h.datum) === true)) {
removed = h.datum;
h.datum = h.extras!.pop()!;
tree.valueCount--;
if (h.extras!.length === 0) {
h.extras = undefined;
}
//// const g = f(h[RIGHT]!, datum);
// if (g === null) { return null; }
//// h[RIGHT] = g!;
} else {
const extras = h.extras!;
let found = false;
for (let i=0; i<extras.length; i++) {
if (compareStrict(datum, extras[i]) === true) {
removed = extras.splice(i,1)[0];
tree.valueCount--;
if (extras.length === 0) {
h.extras = undefined;
}
//// const g = f(h[RIGHT]!, extras[i]);
// if (g === null) { return null; }
//// h[RIGHT] = g!;
found = true;
break;
}
}
if (!found) { return null; }
}
} else {
if (compareStrict === undefined ||
(compareStrict(datum, h.datum) === true)) {
tree.valueCount -= 1 + (theresExtras ? h.extras!.length : 0);
removed = h.datum;
const minNode = tree.getMinNode(h[RIGHT]);
// eslint-disable-next-line
h.datum = minNode?.datum!;
// eslint-disable-next-line
if (tree.duplicatesAllowed) { h.extras = minNode?.extras!; }
h[RIGHT] = removeMin(h[RIGHT]!);
tree.nodeCount--;
} else {
return null; // no match
}
}
} else {
const g = f(h[RIGHT]!, datum);
if (g === null) { return null; }
h[RIGHT] = g;
}
if (h[RIGHT]) { h[RIGHT]!.parent = h; }
return fixUp(h);
}
}
/**
* Returns the two ordered nodes bounding the datum.
*
* * If the datum falls on a node, that node and the next (to the right) is
* returned.
* * If the given datum is smaller than all nodes then the first item in the
* bounds array is `undefined` and the next is the smallest node
* * If the given datum is larger than all nodes then the second item in the
* bounds array is `undefined` and the first item is the largest node
*
*/
public findBounds(datum: T): (Node<T> | undefined)[] {
const tree = this;
let node = tree.root;
const bounds: (Node<T> | undefined)[] = [undefined, undefined];
if (node === undefined) {
return bounds;
}
while (node) {
const c = tree.compare(datum, node.datum);
if (c >= 0) {
bounds[0] = node;
} else {
bounds[1] = node;
}
node = node[c >= 0 ? RIGHT : LEFT];
}
return bounds;
}
/**
* Returns the two ordered nodes bounding the datum.
*
* * If the datum falls on a node, returns the nodes before and after this
* one.
* * If the given datum is smaller than all nodes then the first item in the
* bounds array is `undefined` and the next is the smallest node
* * If the given datum is larger than all nodes then the second item in the
* bounds array is `undefined` and the first item is the largest node
*
* @param tree
* @param datum
*/
public findBoundsExcl(datum: T): (Node<T> | undefined)[] {
const tree = this;
const node = tree.root;
const bounds: (Node<T> | undefined)[] = [undefined, undefined];
if (node === undefined) {
return bounds;
}
f(node);
function f(node: Node<T>) {
while (node) {
const c = tree.compare(datum, node.datum);
if (c === 0) {
// Search on both sides
f(node[LEFT]!);
f(node[RIGHT]!);
return;
}
if (c > 0) {
bounds[0] = node;
} else if (c < 0) {
bounds[1] = node;
}
node = node[c > 0 ? RIGHT : LEFT]!;
}
}
return bounds;
}
/**
* Returns an array of all matching data found.
*
* If duplicates are not allowed it's better to just use `find`.
*
* @param datum
*/
public findAll(datum: T): T[] {
const tree = this;
let node = this.root;
while (node) {
const c = tree.compare(datum, node.datum);
if (c === 0) {
return [
node.datum,
...(node.extras ? node.extras : [])
];
}
node = node[c > 0 ? RIGHT : LEFT];
}
return [];
}
/** @internal */
private getMinOrMaxNode(dir: Dir): (node?: Node<T> | undefined | undefined) => Node<T> | undefined {
return (node: Node<T> | undefined | undefined): Node<T> | undefined => {
if (node === undefined) {
node = this.root;
}
if (!node) {
return undefined;
}
while (node[dir]) {
node = node[dir]!;
}
return node;
}
}
// eslint-disable-next-line
public getMinNode = this.getMinOrMaxNode(LEFT);
// eslint-disable-next-line
public getMaxNode = this.getMinOrMaxNode(RIGHT);
/**
* Returns the minimum value in the tree starting at the given node. If the
* tree is empty, `undefined` will be returned.
*
* If the min value is required for the entire tree call this function
* as `tree.min(tree.root)`
*
* @param node
*/
public min(node?: Node<T> | undefined | undefined): T | undefined {
if (node === undefined) {
node = this.root;
}
const minNode = this.getMinNode(node);
if (minNode !== undefined) {
return minNode.datum;
}
return undefined;
}
/**
* Returns the maximum value in the tree starting at the given node. If the
* tree is empty, `undefined` will be returned.
*
* If the max value is required for the entire tree call this function
* as `tree.max(tree.root)`
*
* @param node
*/
public max(node?: Node<T> | undefined | undefined): T | undefined {
if (node === undefined) {
node = this.root;
}
const maxNode = this.getMaxNode(node);
if (maxNode !== undefined) {
return maxNode.datum;
}
return undefined;
}
}
/**
* Returns the node that is at the top after the rotation.
*
* Destructively rotates the given node, say h, in the
* given direction as far as tree rotations go.
*
* @param dir
* @param h
*
* @internal
*/
function rotate<T>(dir: Dir, h: Node<T>): Node<T> {
const x = h[-dir as Dir]!;
h[-dir as Dir] = x[dir];
if (x[dir]) { x[dir]!.parent = h; }
x[dir] = h;
h.parent = x;
x.color = h.color;
h.color = RED;
return x;
}
/**
* @param h
*
* @internal
*/
function removeMin<T>(h: Node<T>): Node<T> | undefined {
if (!h[LEFT]) {
return undefined;
}
if (!isRed(h[LEFT]) &&
!isRed(h[LEFT]![LEFT])) {
h = moveRedLeft(h);
}
h[LEFT] = removeMin(h[LEFT]!);
if (h[LEFT]) { h[LEFT]!.parent = h; }
return fixUp(h);
}
/**
* Destructively flips the color of the given node and both
* it's childrens' colors.
*
* @param h
*
* @internal
*/
function flipColors<T>(h: Node<T>): void {
h.color = (h.color + 1)%2 as Color;
h[LEFT ]!.color = (h[LEFT ]!.color + 1)%2 as Color;
h[RIGHT]!.color = (h[RIGHT]!.color + 1)%2 as Color;
}
/**
* @param h
*
* @internal
*/
function moveRedLeft<T>(h: Node<T>): Node<T> {
flipColors(h);
if (isRed(h[RIGHT]![LEFT])) {
const a = rotate(RIGHT, h[RIGHT]!);
h[RIGHT] = a;
a.parent = h;
h = rotate(LEFT, h);
flipColors(h);
}
return h;
}
/**
* Returns the node that is at the top after the move.
*
* @param h
*
* @internal
*/
function moveRedRight<T>(h: Node<T>): Node<T> {
flipColors(h);
if (isRed(h[LEFT]![LEFT])) {
h = rotate(RIGHT, h);
flipColors(h);
}
return h;
}
/**
* Returns the node that is at the top after the fix.
*
* Fix right-leaning red nodes.
*
* @internal
*/
function fixUp<T>(h: Node<T>): Node<T> {
if (isRed(h[RIGHT])) {
h = rotate(LEFT, h);
}
if (isRed(h[LEFT]) && isRed(h[LEFT]![LEFT])) {
h = rotate(RIGHT, h);
}
// Split 4-nodes.
if (isRed(h[LEFT]) && isRed(h[RIGHT])) {
flipColors(h);
}
return h;
}
export {
LlRbTree, Node,
LEFT, RIGHT, RED, BLACK,
isRed
}