Home Reference Source

src/internal/tree.js

'use strict';

/** @ignore */
const {TreeNode, RED, BLACK} = require('./tree-node');
/** @ignore */
const {JsIterator, JsReverseIterator} = require('../public/js-iterators');
/** @ignore */
const {Iterator, ReverseIterator} = require('../public/iterators');
/** @ignore */
const {KeyOnlyPolicy, ValueOnlyPolicy, KeyValuePolicy} = require('./policies');
/** @ignore */
const {InsertionResult} = require('../public/insertion-result');

/** insertion mode of a multimap, nodes with the same keys can be added */
const INSERT_MULTI = 1;
/** if a node with the same key already exists then the subsequent attempts are ignored */
const INSERT_UNIQUE = 2;
/** if a node with the same key already exists then it's value is replaced on subsequent attempts */
const INSERT_REPLACE = 3;

/**
 * @private
 * Special node in a tree is created for performance reasons
 */
class Head {
    /** default constructor */
    constructor() {
        /** node with the smallest key */
        this.leftmost = this;
        /** node with the largest key */
        this.rightmost = this;
        /** root node of the tree */
        this.root = this;
        /** number of nodes in the tree */
        this.size = 0;
        /** extra tag used in debuggin of unit tests */
        this.id = 'HEAD';
    }
}

/**
 * @private
 * 3-way comparison, similar to strcmp and memcp in C programming language
 * @returns +1 if the value of rhs is greater than lhs
 *          -1 if the value of rhs is less than lhs
 *           0 if values are the same
 */
function compare(lhs, rhs) {
    if (lhs < rhs) {
        return -1;
    }
    else if (lhs === rhs) {
        return 0;
    }
    else {
        return 1;
    }
}

/**
 * Red-black tree
 * @access private
 */
class Tree {
    /** default constructor of an empty tree */
    constructor() {
        /** head */
        this.head = new Head();
        /** 3-way comparison function */
        this.compare = compare;
        /** must be an instance of KeyOnlyPolicy for sets, or KeyValuePolicy for maps */
        this.valuePolicy = new KeyOnlyPolicy();
    }

    /**
     * Deletes all nodes in the tree
     */
    clear() {
        this.head = new Head();
    }

    /**
     * @returns number of nodes in the tree
     */
    size() {
        return this.head.size;
    }

    /**
     * @private
     * A wrapper that calls 3-way comparison of node keys
     * @param {*} lhs
     * @param {*} rhs
     */
    compareNodes(lhs, rhs) {
        return this.compare(lhs.key, rhs.key);
    }

    /**
     * @private
     * used by rotation operations
     */
    replaceNode(oldNode, newNode) {
        if (oldNode === newNode) {
            return;
        }
        if (oldNode.parent === null) {
            this.head.root = newNode;
        }
        else {
            if (oldNode === oldNode.parent.left) {
                oldNode.parent.left = newNode;
            }
            else {
                oldNode.parent.right = newNode;
            }
        }

        if (!this.isLeaf(newNode)) {
            newNode.parent = oldNode.parent;
        }
    }

    /**
     * Rebalances tree as described below

              X                                           Y
             / \                                         / \
            Y   c         right rotate -->              a   X
           / \            <--  left rotate                 / \
          a   b                                           b   c
     * @private
     */
    rotateLeft(node) {
        let right = node.right;
        if (this.isLeaf(right)) {
            throw new Error('rotateLeft can\'t be performed. The tree is corrupted');
        }
        this.replaceNode(node, right);

        node.right = right.left;
        if (right.left !== null) {
            right.left.parent = node;
        }

        right.left = node;
        node.parent = right;
    }

    /**
     * Rebalances tree as described in rotateLeft
     * @param {*} node - parent node
     */
    rotateRight(node) {
        let left = node.left;
        if (this.isLeaf(left)) {
            throw new Error('rotateRight can\'t be performed. The tree is corrupted');
        }
        this.replaceNode(node, left);

        node.left = left.right;
        if (left.right !== null) {
            left.right.parent = node;
        }

        left.right = node;
        node.parent = left;
    }

    /**
     * @returns true - for null pointers and head node; false - for all other nodes
     * @param {*} node
     */
    isLeaf(node) {
        if (node === null || node === this.head) {
            return true;
        }
        return false;
    }

    /**
     * Leaf nodes are considered 'black'. All real nodes contain 'color' data member
     * @param {*} node
     */
    fetchColor(node) {
        if (this.isLeaf(node)) {
            return BLACK;
        }
        else {
            return node.color;
        }
    }

    /**
     * Tests a node for 'blackness'.
     * @param {*} node
     */
    isBlack(node) {
        return (this.fetchColor(node) === BLACK);
    }

    /**
     * Tests node for 'redness'.
     * @param {*} node
     */
    isRed(node) {
        return (this.fetchColor(node) === RED);
    }

    /* ===========================
       INSERT
       =========================== */
    /**
     * A node will be inserted into the tree even if nodes with the same key already exist
     * @param {*} node
     * @returns {InsertionResult} - indicates whether a node was added and provides iterator to it.
     */
    insertMulti(node) {
        return this.insertNode(node, INSERT_MULTI);
    }

    /**
     * The node is inserted into the tree only if nodes with the same key do not exist there
     * @param {*} node
     * @returns {InsertionResult} - indicates whether a node was added and provides iterator to it.
     */
    insertUnique(node) {
        return this.insertNode(node, INSERT_UNIQUE);
    }

    /**
     * The node is inserted. If a node with the same key exists it's value will be replaced by the value of the new node
     * @param {*} node
     * @returns {InsertionResult} - indicates whether a node was added and provides iterator to it.
     */
    insertOrReplace(node) {
        return this.insertNode(node, INSERT_REPLACE);
    }

    /**
     * @private
     * Inserts node. Updates head node. Rebalances tree.
     * @param {*} n - node
     * @param {*} mode - one of INSERT_MULTI, INSERT_UNIQUE, INSERT_REPLACE
     * @returns {InsertionResult} - indicates whether a node was added and provides iterator to it.
     */
    insertNode(n, mode = INSERT_MULTI) {
        let res = this.insertNodeInternal(this.head.root, n, mode);
        if (res.wasAdded) {
            if (this.head.size === 0) {
                this.head.root = n;
                this.head.leftmost = n;
                this.head.rightmost = n;

                n.left = this.head;
                n.right = this.head;
            }
            else if (this.head.leftmost.left === n) {
                this.head.leftmost = n;
                n.left = this.head;
            }
            else if (this.head.rightmost.right === n) {
                this.head.rightmost = n;
                n.right = this.head;
            }
            this.insertRepairTree(n);
            this.head.size = this.head.size + 1;
        }
        return res;
    }

    /**
     * @private
     * Inserts node according to the mode
     * @param {*} root - root node of the tree
     * @param {*} n - node to be inserted
     * @param {*} mode - one of INSERT_MULTI, INSERT_UNIQUE, INSERT_REPLACE
     * @returns {InsertionResult} - indicates whether a node was added and provides iterator to it.
     */
    insertNodeInternal(root, n, mode) {
        // recursively descend the tree until a leaf is found
        let x = root;
        let y = null;
        let rc = -1;
        // find matching node
        while (!this.isLeaf(x)) {
            y = x;
            rc = this.compareNodes(n, y);
            if (rc < 0) {
                x = y.left;
            }
            else if (rc > 0) {
                x = y.right;
            }
            else {
                // node with the same key value
                switch (mode) {
                    case INSERT_UNIQUE:
                        // it's a duplicate
                        return new InsertionResult(false, false, undefined);
                    case INSERT_REPLACE:
                        this.valuePolicy.copy(y, n);
                        return new InsertionResult(false, true, new Iterator(y, this));
                    default:
                        // INSERT_MULTI
                        x = y.right;
                }
            }
        }
        if (this.isLeaf(y)) {
            n.parent = null;
            n.left = this.head;
            n.right = this.head;
        }
        else {
            n.parent = y;
            if (rc < 0) {
                y.left = n;
            }
            else {
                y.right = n;
            }
        }
        return new InsertionResult(true, false, new Iterator(n, this));
    }

    /**
     * @private
     * The method is decribed at: https://en.wikipedia.org/wiki/Red%E2%80%93black_tree#Insertion
     * @param {*} n - node
     */
    insertRepairTree(n) {
        if (n.parent === null) {
            this.repairCase1(n);
        }
        else if (this.isBlack(n.parent)) {
        /* insert_case2(n);
           // do nothing */
        }
        else if (this.isRed(n.uncle())) {
            this.repairCase3(n);
        }
        else {
            this.repairCase4(n);
        }
    }

    /**
     * @private
     * The method is decribed at: https://en.wikipedia.org/wiki/Red%E2%80%93black_tree#Insertion
     * @param {*} n - node
     */
    repairCase1(n) {
        n.color = BLACK;
    }

    /**
     * @private
     * The method is decribed at: https://en.wikipedia.org/wiki/Red%E2%80%93black_tree#Insertion
     * @param {*} n - node
     */
    repairCase3(n) {
        n.parent.color = BLACK;
        n.uncle().color = BLACK;
        n.grandparent().color = RED;
        this.insertRepairTree(n.grandparent());
    }

    /**
     * @private
     * The method is decribed at: https://en.wikipedia.org/wiki/Red%E2%80%93black_tree#Insertion
     * @param {*} n - node
     */
    repairCase4(n) {
        let p = n.parent;
        let g = n.grandparent();

        let nr = null;
        if ((g.left !== null)
            && (n === g.left.right)) {
            this.rotateLeft(p);
            n = n.left;
        }
        else if ((g.right !== null)
            && (n === g.right.left)) {
            this.rotateRight(p);
            n = n.right;
        }

        p = n.parent;
        g = n.grandparent();
        if (n === p.left) {
            this.rotateRight(g);
        }
        else {
            this.rotateLeft(g);
        }

        p.color = BLACK;
        g.color = RED;
    }

    /**
     * @returns the node with the highest key for the subtree of the specified root node
     * @param {*} node - root node of the subtree to be evaluated
     */
    fetchMaximum(node) {
        while (!this.isLeaf(node.right)) {
            node = node.right;
        }

        return node;
    }

    /**
     * @returns the node with the lowest key for the subtree of the specified root node
     * @param {*} node - root node of the subtree to be evaluated
     */
    fetchMinimum(node) {
        while (!this.isLeaf(node.left)) {
            node = node.left;
        }

        return node;
    }

    /* ===========================
       ERASE
       =========================== */
    /**
     * Removes node from the tree
     * @param {*} node
     */
    erase(node) {
        if (this.isLeaf(node)) {
            return;
        }

        this.eraseInternal(node);
        let h = this.head;
        h.size = h.size - 1;
    }

    /**
     * @private
     * The method is decribed at: https://en.wikipedia.org/wiki/Red%E2%80%93black_tree#Removal
     * @param {*} node - node
     */
    eraseInternal(node) {
        if (!this.isLeaf(node.left)
            && !this.isLeaf(node.right)) {
            let pred = this.fetchMaximum(node.left);

            this.valuePolicy.copy(node, pred);
            node = pred;
        }

        let child = (this.isLeaf(node.right)) ? node.left : node.right;

        if (this.isBlack(node)) {
            this.eraseCase1(node);
        }
        this.replaceNode(node, child);
        if (this.head.size === 2) {
            if (!this.isLeaf(child)) {
                // Root node must be BLACK
                child.color = BLACK;
            }
        }

        let h = this.head;
        if (this.isLeaf(child)) {
            /* The node didn't have children and it was removed
               the head needs to update leftmost, rightmost pointers */
            if (h.leftmost === node) {
                let p = node.parent;
                if (p !== null) {
                    h.leftmost = p;
                    p.left = h;
                }
                else {
                    h.leftmost = h;
                }
            }
            if (h.rightmost === node) {
                let p = node.parent;
                if (p !== null) {
                    h.rightmost = p;
                    p.right = h;
                }
                else {
                    h.rightmost = h;
                }
            }
        }
        else {
            // the node had a child. Now node is removed. Any references should point to the child now
            if (h.leftmost === node) {
                h.leftmost = child;
                child.left = h;
            }
            if (h.rightmost === node) {
                h.rightmost = child;
                child.right = h;
            }
        }
    }

    /**
     * @private
     * The method is decribed at: https://en.wikipedia.org/wiki/Red%E2%80%93black_tree#Removal
     * @param {*} node
     */
    eraseCase1(node) {
        if (node.parent === null) {
            return;
        }
        else {
            this.eraseCase2(node);
        }
    }

    /**
     * @private
     * The method is decribed at: https://en.wikipedia.org/wiki/Red%E2%80%93black_tree#Removal
     * @param {*} node
     */
    eraseCase2(node) {
        let s = node.sibling();

        if (this.isRed(s)) {
            node.parent.color = RED;
            s.color = BLACK;

            if (node === node.parent.left) {
                this.rotateLeft(node.parent);
            }
            else {
                this.rotateRight(node.parent);
            }
        }
        this.eraseCase3(node);
    }

    /**
     * @private
     * The method is decribed at: https://en.wikipedia.org/wiki/Red%E2%80%93black_tree#Removal
     * @param {*} node
     */
    eraseCase3(node) {
        let s = node.sibling();
        let p = node.parent;
        if (this.isBlack(p)
            && this.isBlack(s)
            && this.isBlack(s.left)
            && this.isBlack(s.right)) {

            s.color = RED;
            this.eraseCase1(p);
        }
        else {
            this.eraseCase4(node);
        }
    }

    /**
     * @private
     * The method is decribed at: https://en.wikipedia.org/wiki/Red%E2%80%93black_tree#Removal
     * @param {*} node
     */
    eraseCase4(node) {
        let s = node.sibling();
        let p = node.parent;
        if (this.isRed(p)
            && this.isBlack(s)
            && this.isBlack(s.left)
            && this.isBlack(s.right)) {

            s.color = RED;
            p.color = BLACK;
        }
        else {
            this.eraseCase5(node);
        }
    }

    /**
     * @private
     * The method is decribed at: https://en.wikipedia.org/wiki/Red%E2%80%93black_tree#Removal
     * @param {*} node
     */
    eraseCase5(node) {
        let s = node.sibling();
        let p = node.parent;
        /* The check below is unnecessary
           due to case 2 (even though case 2 changed the sibling to a sibling's child,
           the sibling's child can't be red, since no red parent can have a red child). */
        /* if ((!this.isLeaf(s))
               && this.isBlack(s)) { */

        /* the following statements just force the red to be on the left of the left of the parent,
           or right of the right, so case six will rotate correctly. */
        if (node === p.left
            && this.isRed(s.left)
			&& this.isBlack(s.right)) {

            s.color = RED;
            s.left.color = BLACK;
            this.rotateRight(s);
        }
        else if (node === p.right
            && this.isBlack(s.left)
            && this.isRed(s.right)) {

            s.color = RED;
            s.right.color = BLACK;
            this.rotateLeft(s);
        }
        //}
        this.eraseCase6(node);
    }

    /**
     * @private
     * The method is decribed at: https://en.wikipedia.org/wiki/Red%E2%80%93black_tree#Removal
     * @param {*} node
     */
    eraseCase6(node) {
        let s = node.sibling();
        let p = node.parent;
        s.color = this.fetchColor(p);
        p.color = BLACK;

        if (node === p.left) {
            s.right.color = BLACK;
            this.rotateLeft(p);
        }
        else {
            s.left.color = BLACK;
            this.rotateRight(p);
        }
    }

    /* ===========================
       SEARCH BY KEY
       =========================== */
    /**
    * @returns an iterator pointin to a node with matching key value. If node is not found then end() iterator is returned.
    * @param {*} k - key value
    */
    find(k) {
        let y = this.head;
        let x = y.root;
        while (!this.isLeaf(x)) {
            let rc = this.compare(x.key, k);
            if (rc > 0) {
                y = x;
                x = x.left;
            }
            else if (rc < 0) {
                y = x;
                x = x.right;
            }
            else {
                return new Iterator(x, this);
            }
        }
        return new Iterator(this.head, this);
    }

    /**
     * @returns an iterator pointing to the first node in the tree that is not less than
     * (i.e. greater or equal to) the specified key value, or end() if no such node is found.
     * @param {*} k - key value
     */
    lowerBound(k) {
        let y = this.head;
        let x = y.root;
        while (!this.isLeaf(x)) {
            let rc = this.compare(x.key, k);
            if (rc >= 0) {
                y = x;
                x = x.left;
            }
            else {
                x = x.right;
            }
        }
        return new Iterator(y, this);
    }

    /**
     * @returns an iterator pointing to the first node in the tree that is greater than
     * the specified key value, or end() if no such node is found.
     * @param {*} k - key value
     */
    upperBound(k) {
        let y = this.head;
        let x = y.root;
        while (!this.isLeaf(x)) {
            let rc = this.compare(x.key, k);
            if (rc > 0) {
                y = x;
                x = x.left;
            }
            else {
                x = x.right;
            }
        }
        return new Iterator(y, this);
    }

    /* ===========================
       ITERATORS
       =========================== */

    /**
     * @returns iterator pointing to the node with the lowest key
     */
    begin() {
        return new Iterator(this.head.leftmost, this);
    }

    /**
     * @returns iterator pointing to the node following the node with the highest key
     */
    end() {
        return new Iterator(this.head, this);
    }

    /**
     * @returns iterator pointing to the node with the highest key
     */
    rbegin() {
        return new ReverseIterator(this.head.rightmost, this);
    }

    /**
     * @returns iterator pointing to the node preceding the node with the lowest key
     */
    rend() {
        return new ReverseIterator(this.head, this);
    }

    /**
     * @private
     * provides support for ES6 forward iteration
     */
    jsBegin() {
        return this.head.leftmost;
    }

    /**
     * @private
     * provides support for ES6 forward iteration
     */
    jsEnd() {
        return this.head;
    }

    /**
     * @private
     * provides support for ES6 reverse iteration
     */
    jsRbegin() {
        return this.head.rightmost;
    }

    /**
     * @private
     * provides support for ES6 forward iteration
     */
    jsRend() {
        return this.head;
    }

    /**
     * @returns node following the specified node in ascending order of their keys
     * @param {*} n - node
     */
    next(n) {
        if (n === this.head) {
            return this.head.leftmost;
        }
        if (n.right === this.head) {
            return this.head;
        }
        if (n.right !== null) {
            let res = this.fetchMinimum(n.right);
            return res;
        }
        else {
            while (n.parent.left !== n) {
                n = n.parent;
            }
            return n.parent;
        }
    }

    /**
     * @returns node preceding the specified node in ascending order of their keys
     * @param {*} n - node
     */
    prev(n) {
        if (n === this.head) {
            return this.head.rightmost;
        }
        if (n.left === this.head) {
            return this.head;
        }
        if (n.left !== null) {
            let res = this.fetchMaximum(n.left);
            return res;
        }
        else {
            while (n.parent.right !== n) {
                n = n.parent;
            }
            return n.parent;
        }
    }

    /**
     * ES6 forward iteration
     */
    [Symbol.iterator]() {
        return new JsIterator(this);
    }

    /**
     * ES6 reverse iteration
     */
    backward() {
        return new JsReverseIterator(this);
    }

    /**
     * @returns a new JsIterator object that contains the [key, value] pairs for each element in the order of the keys.
     */
    entries() {
        return new JsIterator(this);
    }

    /**
     * @returns a new JsIterator object that contains the keys for each element in the order of the keys.
     */
    keys() {
        return new JsIterator(this, new KeyOnlyPolicy());
    }

    /**
     * @returns a new JsIterator object that contains the values for each element in the order of the keys.
     */
    values() {
        return new JsIterator(this, new ValueOnlyPolicy());
    }

    /**
     * @returns first element of the container, or undefined if container is empty
     */
    first() {
        if (this.size() === 0) {
            return undefined;
        }
        else {
            let it = this.begin();
            return this.valuePolicy.fetch(it.node);
        }
    }

    /**
     * @returns last element of the container, or undefined if container is empty
     */
    last() {
        if (this.size() === 0) {
            return undefined;
        }
        else {
            let it = this.rbegin();
            return this.valuePolicy.fetch(it.node);
        }
    }

    /**
     * @returns String representation of the container
     */
    toString() {
        let parts = [];
        for (let it = this.begin(); !it.equals(this.end()); it.next()) {
            // convert each key-value pair
            parts.push(this.valuePolicy.toString(it.node));
        }
        return '{' + parts.join(',') + '}';
    }

    /**
     * @returns String tag of this class
     */
    get [Symbol.toStringTag]() {
        return 'Tree';
    }

    /**
     * @returns constructor object for this class
     */
    static get [Symbol.species]() {
        return Tree;
    }

}

module.exports = {
    Tree: Tree,
    compare: compare
};