class ShuffleTree {
constructor({
size = 0,
superRoot = { left: undefined, right: undefined, key: undefined },
seed = Math.random()
} = {}) {
this.size = size;
this.superRoot = superRoot;
this.rand = d3.randomUniform.source(d3.randomLcg(seed))();
}
get root() {
return this.superRoot.right;
}
set root(value) {
this.superRoot.right = value;
}
rootLocation() {
return loc(this.root, this.superRoot);
}
insert(key) {
if (this.root === undefined) {
this.size = 1;
this.root = { left: undefined, right: undefined, key };
return this.root;
}
const { child, parent } = this.locateCore(this.rootLocation(), key);
if (key < child.key) {
const newNode = { left: child.left, right: child, key };
child.left = undefined;
this.size++;
return replaceChild(parent, child, newNode);
} else if (child.key < key) {
const newNode = { left: child, right: child.right, key };
child.right = undefined;
this.size++;
return replaceChild(parent, child, newNode);
} else {
// we get here if it's already in the tree; don't add it again.
// todo: we may want to disallow adding duplicate nodes
return child;
}
}
// Removes `key` from the tree if it's there.
// Adapted from "An implementation of top-down splaying", by Daniel Sleator, 1992.
remove(key) {
if (this.root === undefined) return;
const t = this.locateCore(this.rootLocation(), key);
if (key == t.child.key) {
// found it
if (t.child.left === undefined) {
// If the node to be deleted has no left child, we can replace it with its right child.
replaceChild(t.parent, t.child, t.child.right);
} else {
// We want to move the left child up to replace the deleted node, but we need a place to put the previous right child. So we look for an insertion spot for the right child, which we know to be greater than every value in the left subtree.
replaceChild(t.parent, t.child, t.child.left);
const x = this.locateCore(loc(t.child.left, t.child), key, false);
x.child.right = t.child.right;
}
this.size--;
}
}
// Higher-level API for the common `locate` case of searching over a tree and
// needing only the child node rather than the full (child, parent) location.
locate(key, rotate) {
return this.locateCore(this.rootLocation(), key, rotate).child;
}
// Return the node with the specified key if we find it, or the node that would be its parent if not.
// Adapted from the 'splay' function in 'An implementation of top-down splaying' by Daniel Sleator, 1992.
// The optional `rotate` argument controls whether we allow the possibility for rotations during this locate operation.
locateCore(location, key, rotate = true) {
let counter = rotate ? Math.floor(this.rand() * (this.size + 1)) : -1;
let { child, parent } = location;
let grandparent = undefined;
while (child) {
grandparent = parent;
parent = child;
if (key < child.key) {
child = child.left;
if (counter === 0 && child) {
rotateRight(parent, grandparent);
parent = grandparent;
}
} else if (child.key < key) {
child = child.right;
if (counter === 0 && child) {
rotateLeft(parent, grandparent);
parent = grandparent;
}
} else {
break;
}
counter = Math.floor((counter - 1) / 2);
}
return loc(parent, grandparent);
}
extent() {
const leftmost = this.locate(-Infinity, false);
const rightmost = this.locate(Infinity, false);
return [leftmost?.key, rightmost?.key];
}
links() {
if (this.size === 0) return [];
const stack = [this.root];
const links = [{ id: this.root.key, parentId: this.superRoot.key }]; // array of { id, parentId }
while (stack.length) {
const node = stack.pop();
if (node.left) {
links.push({ id: node.left.key, parentId: node.key });
stack.push(node.left);
}
if (node.right) {
links.push({ id: node.right.key, parentId: node.key });
stack.push(node.right);
}
}
return links;
}
stratify() {
return d3.stratify()(this.links());
}
// To do: Be attentive to reproducibility wrt. not just the the random numbers included
// in the tree or used for queries, but also those used by the tree locate operation itself.
//
// Note: Cloning the tree does not clone RNG state – the clone has a new RNG, currently seeded
// with default seed of zero, regardless of what the seed for the original tree was, since this
// at least means that multiple clones of the same tree will align with each other in terms of
// their future trajectories...
clone() {
const { size, superRoot } = JSON.parse(JSON.stringify(this));
// 🌶️ this does not propagate the rng state since there is no way to clone a d3.randomLcg state.
// but we at least make it deterministic
return new ShuffleTree({ size, superRoot, seed: 0 });
}
}