【问题标题】:How to optimally implement Stack and Queue operations in this B+tree?如何在这个 B+tree 中优化实现 Stack 和 Queue 操作?
【发布时间】:2021-01-13 10:05:13
【问题描述】:

在我学习理解 B+树的过程中,我现在想看看如何修改 this 现有的 B+ 索引树(它具有使每个数组的长度为 2 的幂的附加限制:1, 2, 4, 8, 16, 或 32), 并把它变成一个 Stack, 把它变成一个 Queue。这是来自精彩链接答案的原始 B+tree 代码:

class Node {
    constructor(capacity) {
        // Mimic fixed-size array (avoid accidentally growing it)
        this.children = Object.seal(Array(capacity).fill(null));
        this.childCount = 0; // Number of used slots in children array
        this.treeSize = 0; // Total number of values in this subtree
        // Maintain back-link to parent.
        this.parent = null;
        // Per level in the tree, maintain a doubly linked list
        this.prev = this.next = null;
    }
    setCapacity(capacity) {
        if (capacity < 1) return;
        // Here we make a new array, and copy the data into it
        let children = Object.seal(Array(capacity).fill(null));
        for (let i = 0; i < this.childCount; i++) children[i] = this.children[i];
        this.children = children;
    }
    isLeaf() {
        return !(this.children[0] instanceof Node);
    }
    index() {
        return this.parent.children.indexOf(this);
    }
    updateTreeSize(start, end, sign=1) {        
        let sum = 0;
        if (this.isLeaf()) {
            sum = end - start;
        } else {
            for (let i = start; i < end; i++) sum += this.children[i].treeSize;
        }
        if (!sum) return;
        sum *= sign;
        // Apply the sum change to this node and all its ancestors
        for (let node = this; node; node = node.parent) {
            node.treeSize += sum;
        }
    }
    wipe(start, end) {
        this.updateTreeSize(start, end, -1);
        this.children.copyWithin(start, end, this.childCount);
        for (let i = this.childCount - end + start; i < this.childCount; i++) {
            this.children[i] = null;
        }
        this.childCount -= end - start;
        // Reduce allocated size if possible
        if (this.childCount * 2 <= this.children.length) this.setCapacity(this.children.length / 2);
    }
    moveFrom(neighbor, target, start, count=1) {
        // Note: `start` can have two meanings:
        //   if neighbor is null, it is the value/Node to move to the target
        //   if neighbor is a Node, it is the index from where value(s) have to be moved to the target
        // Make room in target node
        if (this.childCount + count > this.children.length) this.setCapacity(this.children.length * 2);
        this.children.copyWithin(target + count, target, Math.max(target + count, this.childCount));
        this.childCount += count;
        if (neighbor !== null) {
            // Copy the children
            for (let i = 0; i < count; i++) {
                this.children[target + i] = neighbor.children[start + i];
            }
            // Remove the original references
            neighbor.wipe(start, start + count);
        } else {
            this.children[target] = start; // start is value to insert
        }
        this.updateTreeSize(target, target + count, 1);
        // Set parent link(s)
        if (!this.isLeaf()) {
            for (let i = 0; i < count; i++) {
                this.children[target + i].parent = this;
            }
        }
    }
    moveToNext(count) {
        this.next.moveFrom(this, 0, this.childCount - count, count);
    }
    moveFromNext(count) {
        this.moveFrom(this.next, this.childCount, 0, count);
    }
    basicRemove(index) {
        if (!this.isLeaf()) {
            // Take node out of the level's linked list
            let prev = this.children[index].prev;
            let next = this.children[index].next;
            if (prev) prev.next = next;
            if (next) next.prev = prev;
        }
        this.wipe(index, index + 1);
    }
    basicInsert(index, value) {
        this.moveFrom(null, index, value);
        if (value instanceof Node) {
            // Insert node in the level's linked list
            if (index > 0) {
                value.prev = this.children[index-1];
                value.next = value.prev.next;
            } else if (this.childCount > 1) {
                value.next = this.children[1];
                value.prev = value.next.prev;
            }
            if (value.prev) value.prev.next = value;
            if (value.next) value.next.prev = value;
        }
    }
    pairWithSmallest() {            
        return this.prev && (!this.next || this.next.childCount > this.prev.childCount)
            ? [this.prev, this] : [this, this.next];
    }
    toString() {
        return "[" + this.children.map(v => v??"-").join() + "]";
    }
}

class Tree {
    constructor(nodeCapacity=32) {
        this.nodeCapacity = nodeCapacity;
        this.root = new Node(1);
        this.first = this.root; // Head of doubly linked list at bottom level
    }
    locate(offset) {
        let node = this.root;
        // Normalise argument
        offset = offset < 0 ? Math.max(0, node.treeSize + offset) : Math.min(offset, node.treeSize);

        while (!node.isLeaf()) {
            let index = 0;
            let child = node.children[index];
            while (offset > child.treeSize || offset === child.treeSize && child.next) {
                offset -= child.treeSize;
                child = node.children[++index];
            }
            node = child;
        }
        return [node, offset];
    }
    getItemAt(offset) {
        let [node, index] = this.locate(offset);
        if (index < node.childCount) return node.children[index];
    }
    setItemAt(offset, value) {
        let [node, index] = this.locate(offset);
        if (index < node.childCount) node.children[index] = value;
    }
    removeItemAt(offset) {
        let [node, index] = this.locate(offset);
        if (index >= node.childCount) return;

        while (true) {
            console.assert(node.isLeaf() || node.children[index].treeSize === 0);
            node.basicRemove(index);

            // Exit when node's fill ratio is fine
            if (!node.parent || node.childCount * 2 > this.nodeCapacity) return;
            // Node has potentially too few children, we should either merge or redistribute
            
            let [left, right] = node.pairWithSmallest();
            
            if (!left || !right) { // A node with no siblings? Must become the root!
                this.root = node;
                node.parent = null;
                return;
            }
            let sumCount = left.childCount + right.childCount;
            let childCount = sumCount >> 1;
            
            // Check whether to merge or to redistribute
            if (sumCount > this.nodeCapacity) { // redistribute
                // Move some data from the bigger to the smaller node
                let shift = childCount - node.childCount;
                if (!shift) { // Boundary case: when a redistribution would bring no improvement
                    console.assert(node.childCount * 2 === this.nodeCapacity && sumCount === this.nodeCapacity + 1);
                    return;
                }
                if (node === left) { // move some children from right to left
                    left.moveFromNext(shift);
                } else { // move some children from left to right
                    left.moveToNext(shift);
                }
                return;
            }
            
            // Merge:
            // Move all data from the right to the left
            left.moveFromNext(right.childCount);
            // Prepare to delete right node
            node = right.parent;
            index = right.index();
        }
    }
    insertItemAt(offset, value) {
        let [node, index] = this.locate(offset);
        while (node.childCount === this.nodeCapacity) { // No room here
            if (index === 0 && node.prev && node.prev.childCount < this.nodeCapacity) {
                return node.prev.basicInsert(node.prev.childCount, value);
            }
            // Check whether we can redistribute (to avoid a split)
            if (node !== this.root) {
                let [left, right] = node.pairWithSmallest();
                let joinedIndex = left === node ? index : left.childCount + index;
                let sumCount = left.childCount + right.childCount + 1;
                if (sumCount <= 2 * this.nodeCapacity) { // redistribute
                    let childCount = sumCount >> 1;
                    if (node === right) { // redistribute to the left
                        let insertInLeft = joinedIndex < childCount;
                        left.moveFromNext(childCount - left.childCount - +insertInLeft);
                    } else { // redistribute to the right
                        let insertInRight = index >= sumCount - childCount;
                        left.moveToNext(childCount - right.childCount - +insertInRight);
                    }
                    if (joinedIndex > left.childCount || 
                            joinedIndex === left.childCount && left.childCount > right.childCount) {
                        right.basicInsert(joinedIndex - left.childCount, value);
                    } else {
                        left.basicInsert(joinedIndex, value);
                    }
                    return;
                }
            }
            // Cannot redistribute: split node
            let childCount = node.childCount >> 1;
            // Create a new node that will later become the right sibling of this node
            let sibling = new Node(childCount);
            // Move half of node node's data to it
            sibling.moveFrom(node, 0, childCount, childCount);
            // Insert the value in either the current node or the new one
            if (index > node.childCount) {
                sibling.basicInsert(index - node.childCount, value);
            } else {
                node.basicInsert(index, value);
            }
            // Is this the root? 
            if (!node.parent) {
                // ...then first create a parent, which is the new root
                this.root = new Node(2);
                this.root.basicInsert(0, node);
            }
            // Prepare for inserting the sibling node into the tree
            index = node.index() + 1;
            node = node.parent;
            value = sibling;
        }
        node.basicInsert(index, value);
    }
    /* Below this point: these methods are optional */
    * [Symbol.iterator]() { // Make tree iterable
        let i = 0;
        for (let node = this.first; node; node = node.next) {
            for (let i = 0; i < node.childCount; i++) yield node.children[i];
        }
    }
    print() {
        console.log(this.root && this.root.toString());
    }
    verify() {
        // Raise an error when the tree violates one of the required properties
        if (!this.root) return; // An empty tree is fine.
        if (this.root.parent) throw "root should not have a parent";
        // Perform a breadth first traversal
        let q = [this.root];
        while (q.length) {
            if (q[0].isLeaf() && this.first !== q[0]) throw "this.first is not pointing to first leaf";
            let level = [];
            let last = null;
            for (let parent of q) {
                if (!(parent instanceof Node)) throw "parent is not instance of Node";
                if (parent.children.length > this.nodeCapacity) throw "node's children array is too large";
                if (parent.childCount > 0 && parent.childCount * 2 <= parent.children.length) throw "node's fill ratio is too low";
                for (let i = parent.childCount; i < parent.children.length; i++) {
                    if (parent.children[i] !== null) throw "child beyond childCount should be null but is not";
                }
                let treeSize = parent.treeSize;
                if (parent.isLeaf()) {
                    for (let value of parent.children.slice(0, parent.childCount)) {
                        if (value === null) throw "leaf has a null as value";
                        if (value instanceof Node) throw "leaf has a Node as value";
                    }
                    if (parent.treeSize !== parent.childCount) throw "leaf has mismatch in treeSize and childCount";
                } else {
                    for (let node of parent.children.slice(0, parent.childCount)) {
                        if (node === null) throw "internal node has a null as value";
                        if (!(node instanceof Node)) throw "internal node has a non-Node as value";
                        if (node.parent !== parent) throw "wrong parent";
                        if (node.prev !== last) throw "prev link incorrect";
                        if (last && last.next !== node) throw "next link incorrect";
                        if (last && last.children.length + node.children.length <= this.nodeCapacity) {
                            throw "two consecutive siblings have a total number of children that is too small";
                        }
                        if (node.childCount * 2 < this.nodeCapacity) {
                            throw "internal node is too small: " + node;
                        }
                        level.push(node);
                        last = node;
                        treeSize -= node.treeSize;
                    }
                    if (treeSize) throw "internal node treeSize sum mismatches";
                }
            }
            if (last && last.next) throw "last node in level has a next reference";
            q = level;
        }
    }
    test(count=100, option=3) {
        // option:
        //     0 = always insert & delete at left side (offset 0)
        //     1 = always insert & delete at right side
        //     2 = always insert & delete at middle
        //     3 = insert & delete at random offsets
        // Create array to perform the same operations on it as on the tree
        let arr = [];
        // Perform a series of insertions
        for (let i = 0; i < count; i++) {
            // Choose random insertion index
            let index = Array.isArray(option) ? option[i] : [0, i, i >> 1, Math.floor(Math.random() * (i+1))][option];
            // Perform same insertion in array and tree
            arr.splice(index, 0, i);
            this.insertItemAt(index, i);
            // Verify tree consistency and properties
            this.verify();
            // Verify the order of values in the array is the same as in the tree
            if (arr+"" !== [...this]+"") throw i + ": tree not same as array";
        }
        // Perform a series of updates
        for (let i = 0; i < count; i++) {
            // Choose random update index
            let index = Math.floor(Math.random() * count);
            // Perform same insertion in array and tree
            arr[index] += count;
            this.setItemAt(index, this.getItemAt(index) + count);
            // Verify tree consistency and properties
            this.verify();
            // Verify the order of values in the array is the same as in the tree
            if (arr+"" !== [...this]+"") throw "tree not same as array";
        }
        // Perform a series of deletions
        for (let i = arr.length - 1; i >= 0; i--) {
            // Choose random deletion index
            let index = [0, i, i >> 1, Math.floor(Math.random() * (i+1))][option];
            // Perform same deletion in array and tree
            arr.splice(index, 1);
            this.removeItemAt(index);
            // Verify tree consistency and properties
            this.verify();
            // Verify the order of values in the array is the same as in the tree
            if (arr+"" !== [...this]+"") throw "tree not same as array";
        }
    }
}

// Perform 1000 insertions, 1000 updates, and 1000 deletions on a tree with node capacity of 8
new Tree(8).test(1000);
console.log("all tests completed");

堆栈是后进先出的 LIFO 数据结构,而队列是先进先出的 FIFO 数据结构。堆栈具有push 和pop 方法(除了getItemAt(index) 和其他基本数组方法,此B+tree 已经实现)。队列有一个push 和shift 方法,其中shift 从数组的“前面”删除。所以它们已经很相似了,只需将项目添加到数组的末尾,或者从数组的开头或结尾删除。

我对现有 B+tree 执行 push 操作的方式是简单地跟踪数组的长度(您可以使用 tree.root.treeSize 进行操作),并在该位置进行 insertItemAt(tree.root.treeSize, val) 它。但也许有更优化的方式来做到这一点?

Tree.prototype.push = function(val) { this.insertItemAt(this.root.treeSize, val) }

对于流行音乐,我会这样做:

Tree.prototype.pop = function() {
  let val = this.getItemAt(this.root.treeSize - 1)
  this.removeItemAt(this.root.treeSize - 1)
  return val
}

最后,对于shift,我会这样做:

Tree.prototype.shift = function() {
  let val = this.getItemAt(0)
  this.removeItemAt(0)
  return val
}

但我的问题是,有没有更好更优化的方法来做到这一点?与其遍历整个树来查找第一个或最后一个项目,也许我们可以缓存它们?不确定这里的最佳方法。考虑到 B+tree 的结构(因为具有两个约束的强大功能,仅此而已),有什么方法可以使其达到最佳状态?例如,在“pluck”操作(pop 和 shift)中,有 两个遍历,也许这可能是一个(或者甚至没有)?如何修改此 B+树以使这些操作优化?

【问题讨论】:

    标签: javascript arrays algorithm data-structures tree


    【解决方案1】:

    但我的问题是,有没有更好更优化的方法来做到这一点?与其遍历整个树来查找第一个或最后一个项目,也许我们可以缓存它们?

    你可以做一些缓存。但是要意识到“遍历整棵树”并不像听起来那么糟糕。它是关于从根到叶子的遍历,因此节点访问的次数等于层数。树的层数是O(logn)。要得到一棵有 10 层的树,您必须插入数千亿个值。

    不过,您可以通过使用底层维护的链表来避免向下遍历。代码已经引用了该列表中最左边的节点 (this.first),我们可以添加对最后一个节点的引用并保持同步。

    此外,我们可以更改removeItemAt,使其也返回已删除的值。这样您就不必单独调用getItemAt。

    要做的改变

    要使removeItemAt 返回删除的值,请在函数开头附近添加一行:

    removeItemAt(offset) {
        let [node, index] = this.locate(offset);
        if (index >= node.childCount) return;
        let value = node.children[index]; // <-- get deleted item (to return it)
    

    ...并将 4 个 return 语句中的 每个 更改为:

    return value;
    

    在构造函数中,定义this.last:

    this.first = this.last = this.root; // Head & tail of doubly linked list at bottom level
    

    ...在insertItemAt 中插入节点时可能会分配给它:

    let sibling = new Node(childCount);
    if (node === this.last) this.last = sibling; // <----
    

    ...并在删除removeItemAt 中的最后一个节点时分配给它(接近尾声):

    left.moveFromNext(right.childCount);
    if (right === this.last) this.last = left; // <----
    

    那么locate方法可以检测到返回节点应该是树底层的第一个或最后一个节点的情况:

        // Normalise argument
        offset = offset < 0 ? Math.max(0, node.treeSize + offset) : Math.min(offset, node.treeSize);
        // Add these Shortcuts:
        if (offset < this.first.childCount) return [this.first, offset];
        if (offset >= node.treeSize - this.last.childCount) {
            return [this.last, offset - node.treeSize + this.last.childCount];
        }
    

    最后,我们要添加这些方法:

    push(value) {
        this.insertItemAt(this.root.treeSize, value);
    }
    pop() {
        return this.removeItemAt(-1);
    }
    unshift(value) {
        this.insertItemAt(0, value);
    }
    shift() {
        return this.removeItemAt(0);
    }
    

    实现-sn-p

    这是包含这些更改的代码,以及用于测试这些新方法的 test 方法:

    class Node {
        constructor(capacity) {
            // Mimic fixed-size array (avoid accidentally growing it)
            this.children = Object.seal(Array(capacity).fill(null));
            this.childCount = 0; // Number of used slots in children array
            this.treeSize = 0; // Total number of values in this subtree
            // Maintain back-link to parent.
            this.parent = null;
            // Per level in the tree, maintain a doubly linked list
            this.prev = this.next = null;
        }
        setCapacity(capacity) {
            if (capacity < 1) return;
            // Here we make a new array, and copy the data into it
            let children = Object.seal(Array(capacity).fill(null));
            for (let i = 0; i < this.childCount; i++) children[i] = this.children[i];
            this.children = children;
        }
        isLeaf() {
            return !(this.children[0] instanceof Node);
        }
        index() {
            return this.parent.children.indexOf(this);
        }
        updateTreeSize(start, end, sign=1) {        
            let sum = 0;
            if (this.isLeaf()) {
                sum = end - start;
            } else {
                for (let i = start; i < end; i++) sum += this.children[i].treeSize;
            }
            if (!sum) return;
            sum *= sign;
            // Apply the sum change to this node and all its ancestors
            for (let node = this; node; node = node.parent) {
                node.treeSize += sum;
            }
        }
        wipe(start, end) {
            this.updateTreeSize(start, end, -1);
            this.children.copyWithin(start, end, this.childCount);
            for (let i = this.childCount - end + start; i < this.childCount; i++) {
                this.children[i] = null;
            }
            this.childCount -= end - start;
            // Reduce allocated size if possible
            if (this.childCount * 2 <= this.children.length) this.setCapacity(this.children.length / 2);
        }
        moveFrom(neighbor, target, start, count=1) {
            // Note: `start` can have two meanings:
            //   if neighbor is null, it is the value/Node to move to the target
            //   if neighbor is a Node, it is the index from where value(s) have to be moved to the target
            // Make room in target node
            if (this.childCount + count > this.children.length) this.setCapacity(this.children.length * 2);
            this.children.copyWithin(target + count, target, Math.max(target + count, this.childCount));
            this.childCount += count;
            if (neighbor !== null) {
                // Copy the children
                for (let i = 0; i < count; i++) {
                    this.children[target + i] = neighbor.children[start + i];
                }
                // Remove the original references
                neighbor.wipe(start, start + count);
            } else {
                this.children[target] = start; // start is value to insert
            }
            this.updateTreeSize(target, target + count, 1);
            // Set parent link(s)
            if (!this.isLeaf()) {
                for (let i = 0; i < count; i++) {
                    this.children[target + i].parent = this;
                }
            }
        }
        moveToNext(count) {
            this.next.moveFrom(this, 0, this.childCount - count, count);
        }
        moveFromNext(count) {
            this.moveFrom(this.next, this.childCount, 0, count);
        }
        basicRemove(index) {
            if (!this.isLeaf()) {
                // Take node out of the level's linked list
                let prev = this.children[index].prev;
                let next = this.children[index].next;
                if (prev) prev.next = next;
                if (next) next.prev = prev;
            }
            this.wipe(index, index + 1);
        }
        basicInsert(index, value) {
            this.moveFrom(null, index, value);
            if (value instanceof Node) {
                // Insert node in the level's linked list
                if (index > 0) {
                    value.prev = this.children[index-1];
                    value.next = value.prev.next;
                } else if (this.childCount > 1) {
                    value.next = this.children[1];
                    value.prev = value.next.prev;
                }
                if (value.prev) value.prev.next = value;
                if (value.next) value.next.prev = value;
            }
        }
        pairWithSmallest() {            
            return this.prev && (!this.next || this.next.childCount > this.prev.childCount)
                ? [this.prev, this] : [this, this.next];
        }
        toString() {
            return "[" + this.children.map(v => v??"-").join() + "]";
        }
    }
    
    class Tree {
        constructor(nodeCapacity=32) {
            this.nodeCapacity = nodeCapacity;
            this.root = new Node(1);
            this.first = this.last = this.root; // Head of doubly linked list at bottom level
        }
        locate(offset) {
            let node = this.root;
            // Normalise argument
            offset = offset < 0 ? Math.max(0, node.treeSize + offset) : Math.min(offset, node.treeSize);
            // Shortcuts
            
            if (offset < this.first.childCount) return [this.first, offset]; // *
            if (offset >= node.treeSize - this.last.childCount) {
                return [this.last, offset - node.treeSize + this.last.childCount]; // *
            }
            while (!node.isLeaf()) {
                let index = 0;
                let child = node.children[index];
                while (offset > child.treeSize || offset === child.treeSize && child.next) {
                    offset -= child.treeSize;
                    child = node.children[++index];
                }
                node = child;
            }
            return [node, offset];
        }
        getItemAt(offset) {
            let [node, index] = this.locate(offset);
            if (index < node.childCount) return node.children[index];
        }
        setItemAt(offset, value) {
            let [node, index] = this.locate(offset);
            if (index < node.childCount) node.children[index] = value;
        }
        removeItemAt(offset) {
            let [node, index] = this.locate(offset);
            if (index >= node.childCount) return;
            let value = node.children[index]; // * get deleted item (to return it)
            
            while (true) {
                console.assert(node.isLeaf() || node.children[index].treeSize === 0);
                node.basicRemove(index);
    
                // Exit when node's fill ratio is fine
                if (!node.parent || node.childCount * 2 > this.nodeCapacity) return value; // *
                // Node has potentially too few children, we should either merge or redistribute
                
                let [left, right] = node.pairWithSmallest();
                
                if (!left || !right) { // A node with no siblings? Must become the root!
                    this.root = node;
                    node.parent = null;
                    return value; // *
                }
                let sumCount = left.childCount + right.childCount;
                let childCount = sumCount >> 1;
                
                // Check whether to merge or to redistribute
                if (sumCount > this.nodeCapacity) { // redistribute
                    // Move some data from the bigger to the smaller node
                    let shift = childCount - node.childCount;
                    if (!shift) { // Boundary case: when a redistribution would bring no improvement
                        console.assert(node.childCount * 2 === this.nodeCapacity && sumCount === this.nodeCapacity + 1);
                        return value; // *
                    }
                    if (node === left) { // move some children from right to left
                        left.moveFromNext(shift);
                    } else { // move some children from left to right
                        left.moveToNext(shift);
                    }
                    return value; // *
                }
                
                // Merge:
                // Move all data from the right to the left
                left.moveFromNext(right.childCount);
                if (right === this.last) this.last = left;
                // Prepare to delete right node
                node = right.parent;
                index = right.index();
            }
        }
        insertItemAt(offset, value) {
            let [node, index] = this.locate(offset);
            while (node.childCount === this.nodeCapacity) { // No room here
                if (index === 0 && node.prev && node.prev.childCount < this.nodeCapacity) {
                    return node.prev.basicInsert(node.prev.childCount, value);
                }
                // Check whether we can redistribute (to avoid a split)
                if (node !== this.root) {
                    let [left, right] = node.pairWithSmallest();
                    let joinedIndex = left === node ? index : left.childCount + index;
                    let sumCount = left.childCount + right.childCount + 1;
                    if (sumCount <= 2 * this.nodeCapacity) { // redistribute
                        let childCount = sumCount >> 1;
                        if (node === right) { // redistribute to the left
                            let insertInLeft = joinedIndex < childCount;
                            left.moveFromNext(childCount - left.childCount - +insertInLeft);
                        } else { // redistribute to the right
                            let insertInRight = index >= sumCount - childCount;
                            left.moveToNext(childCount - right.childCount - +insertInRight);
                        }
                        if (joinedIndex > left.childCount || 
                                joinedIndex === left.childCount && left.childCount > right.childCount) {
                            right.basicInsert(joinedIndex - left.childCount, value);
                        } else {
                            left.basicInsert(joinedIndex, value);
                        }
                        return;
                    }
                }
                // Cannot redistribute: split node
                let childCount = node.childCount >> 1;
                // Create a new node that will later become the right sibling of this node
                let sibling = new Node(childCount);
                if (node === this.last) this.last = sibling;
                // Move half of node node's data to it
                sibling.moveFrom(node, 0, childCount, childCount);
                // Insert the value in either the current node or the new one
                if (index > node.childCount) {
                    sibling.basicInsert(index - node.childCount, value);
                } else {
                    node.basicInsert(index, value);
                }
                // Is this the root? 
                if (!node.parent) {
                    // ...then first create a parent, which is the new root
                    this.root = new Node(2);
                    this.root.basicInsert(0, node);
                }
                // Prepare for inserting the sibling node into the tree
                index = node.index() + 1;
                node = node.parent;
                value = sibling;
            }
            node.basicInsert(index, value);
        }
        // * added 4 methods
        push(value) {
            this.insertItemAt(this.root.treeSize, value);
        }
        pop() {
            return this.removeItemAt(-1);
        }
        unshift(value) {
            this.insertItemAt(0, value);
        }
        shift() {
            return this.removeItemAt(0);
        }
        /* Below this point: these methods are optional */
        * [Symbol.iterator]() { // Make tree iterable
            let i = 0;
            for (let node = this.first; node; node = node.next) {
                for (let i = 0; i < node.childCount; i++) yield node.children[i];
            }
        }
        print() {
            console.log(this.root && this.root.toString());
        }
        verify() {
            // Raise an error when the tree violates one of the required properties
            if (!this.root) return; // An empty tree is fine.
            if (this.root.parent) throw "root should not have a parent";
            // Perform a breadth first traversal
            let q = [this.root];
            while (q.length) {
                if (q[0].isLeaf() && this.first !== q[0]) throw "this.first is not pointing to first leaf";
                let level = [];
                let last = null;
                for (let parent of q) {
                    if (!(parent instanceof Node)) throw "parent is not instance of Node";
                    if (parent.children.length > this.nodeCapacity) throw "node's children array is too large";
                    if (parent.childCount > 0 && parent.childCount * 2 <= parent.children.length) throw "node's fill ratio is too low";
                    for (let i = parent.childCount; i < parent.children.length; i++) {
                        if (parent.children[i] !== null) throw "child beyond childCount should be null but is not";
                    }
                    let treeSize = parent.treeSize;
                    if (parent.isLeaf()) {
                        for (let value of parent.children.slice(0, parent.childCount)) {
                            if (value === null) throw "leaf has a null as value";
                            if (value instanceof Node) throw "leaf has a Node as value";
                        }
                        if (parent.treeSize !== parent.childCount) throw "leaf has mismatch in treeSize and childCount";
                    } else {
                        for (let node of parent.children.slice(0, parent.childCount)) {
                            if (node === null) throw "internal node has a null as value";
                            if (!(node instanceof Node)) throw "internal node has a non-Node as value";
                            if (node.parent !== parent) throw "wrong parent";
                            if (node.prev !== last) throw "prev link incorrect";
                            if (last && last.next !== node) throw "next link incorrect";
                            if (last && last.children.length + node.children.length <= this.nodeCapacity) {
                                throw "two consecutive siblings have a total number of children that is too small";
                            }
                            if (node.childCount * 2 < this.nodeCapacity) {
                                throw "internal node is too small: " + node;
                            }
                            level.push(node);
                            last = node;
                            treeSize -= node.treeSize;
                        }
                        if (treeSize) throw "internal node treeSize sum mismatches";
                    }
                }
                if (last && last.next) throw "last node in level has a next reference";
                q = level;
            }
        }
        test(count=100, option=3) {
            // option:
            //     0 = always insert & delete at left side (offset 0)
            //     1 = always insert & delete at right side
            //     2 = always insert & delete at middle
            //     3 = insert & delete at random offsets
            // Create array to perform the same operations on it as on the tree
            let arr = [];
            // Perform a series of insertions
            for (let i = 0; i < count; i++) {
                // Choose random insertion index
                let index = Math.floor(Math.random() * (i+1));
                // Perform same insertion in array and tree
                if (Math.random() < 0.5) {
                    arr.push(i);
                    this.push(i);
                } else {
                    arr.unshift(i);
                    this.unshift(i);
                }
                // Verify tree consistency and properties
                this.verify();
                // Verify the order of values in the array is the same as in the tree
                if (arr+"" !== [...this]+"") throw i + ": tree not same as array";
            }
            // Perform a series of deletions and insertions
            for (let i = arr.length - 1; i >= 0; i--) {
                // Choose random deletion index
                let index = [0, i, i >> 1, Math.floor(Math.random() * (i+1))][option];
                // Perform same deletion in array and tree
                if (Math.random() < 0.6) {
                    if (Math.random() < 0.5) {
                        if (arr.pop(i) !== this.pop(i)) throw "pop returns different value";
                    } else {
                        if (arr.shift(i) !== this.shift(i)) throw "shift returns different value";
                    }
                } else {
                    if (Math.random() < 0.5) {
                        arr.push(i);
                        this.push(i);
                    } else {
                        arr.unshift(i);
                        this.unshift(i);
                    }
                }
                // Verify tree consistency and properties
                this.verify();
                // Verify the order of values in the array is the same as in the tree
                if (arr+"" !== [...this]+"") throw "tree not same as array";
            }
            return this;
        }
    }
    
    // Perform 1000 insertions, with either push or unshift, 
    // then a mix of 1000 insertions/removals, the latter with either pop or shift.
    new Tree(8).test(1000);
    console.log("all tests completed");

    【讨论】:

      猜你喜欢
      • 2017-05-10
      • 2021-05-24
      • 2020-10-07
      • 2011-05-25
      • 2019-01-23
      • 2016-01-06
      • 1970-01-01
      • 2011-07-27
      • 2012-09-05
      相关资源
      最近更新 更多