用Js實現紅黑樹

YDSS發表於2018-06-29

學習紅黑樹,用js擼了一個

紅黑樹是一個效率很高且穩定的資料結構,插入和刪除操作的時間複雜度都是logn。

紅黑樹的性質:

  1. 每一個節點或者著紅色,或者著黑色
  2. 根是黑色
  3. 如果一個節點是紅色的,那麼它的子節點必須是黑色
  4. 從一個節點到一個Null節點(樹葉)的每一條簡單路徑必須包含相同數目的黑色節點

插入操作以後再補~

刪除操作(自頂向下的實現方式)

刪除操作是紅黑樹最難的部分,通常有兩種實現方式:自頂向下自底向上。《演算法導論》裡使用的是自底向上的實現方式,對我而言相當晦澀,又看了幾篇類似的實現方式,需要羅列出相當多的情形,實現不易。《資料結構與演算法 -- C語言實現》裡使用的是自頂向下的實現方式,但只討論了大致邏輯,並未給出具體實現,最後在這篇文章裡找到了完整的實現。自頂向下的方式確實簡單易懂,且非常巧妙,我也是用這種方式來實現紅黑樹的刪除操作

在此之前,先複習一下前面列出的紅黑樹的性質。刪除操作之所以複雜,是因為如果需要刪除的節點是黑色的,那麼直接刪除它會破壞性質4。因此,我們需要保證刪除該節點之後,能有一種方式修復刪除後被破壞的部分。自底向上實現的思路是:先刪除節點,然後通過旋轉、變色等手段恢復破壞了紅黑樹性質的部分,而自頂向下實現的思路是:在查詢需要刪除的節點的路徑上,保證每個節點都是紅色的(如果不是就要通過變換讓它變成紅色,且不破壞樹的性質),如果它是要刪除的節點,就可以安心地刪除它。就思路而言,顯然自底向上的方式更易理解,好像也更容易實現,但當你去處理修復過程時會發現有相當多的情況需要考慮。而自頂向下的方式看似笨拙,卻能夠通過巧妙的變換簡化變換的過程

總的來說,就是我們要讓當前訪問的節點X變紅,然後再去考慮它是不是需要刪除的節點

  1. 啟動條件

刪除是一個遞迴函式,在進入遞迴之前需要確保當前當前結構符合啟動條件。這裡的結構指以X為中心的部分樹結構,可能包含P, S,GP, XL,XR,SL,SR。啟動條件如下:

用Js實現紅黑樹

即:X和它的兄弟節點S為黑色,父親節點P為紅色。實現insert時我們做了一個特殊處理,構造了一個假的根節點,值為負無窮,顏色為黑色,因此所有的真實節點都在它的右子樹上。它的左節點是一個null節點(黑色),右節點是真正的根節點(必然是黑色)。而自頂向下刪除的第一步,就是把根節點塗成紅色,這樣就天然滿足了啟動條件

  1. 若X有兩個黑色的節點。注意,當X為葉子節點時也適用,因為葉子節點的子節點為Null節點,是黑色

    用Js實現紅黑樹

    這時還需要考察S的子節點的情況才能決定如何變換,因此2還需要分幾種子情形

    2.1 符合2的條件,同時S也有兩個黑色的子節點

    用Js實現紅黑樹

    這種情況是最簡單的,讓X變紅只需要變換P,X,S的顏色即可。變換方法如下圖

    用Js實現紅黑樹

    2.2 符合2的條件,且S有一個紅色的左子節點,一個黑色的右子節點

    用Js實現紅黑樹

    這種情況下,X變紅後,左邊就少了一個黑色的節點,只能從P的右子樹借一個過來。SL這個紅色的節點可以利用起來,讓它移到P現在的位置。顯然這需要一個雙旋轉(SL位於整個旋轉路徑的內側),變換如下:

    用Js實現紅黑樹

    2.3 符合2的條件,且S有一個黑色的左子節點,一個紅色的右子節點

    用Js實現紅黑樹

    2.2類似,我們要利用SR這個紅色節點,因為SR在整個旋轉路徑的外側,所以使用一個單旋轉

    用Js實現紅黑樹

    2的這三種情況完成變換之後,X已經是紅色且保證了紅黑樹的性質,接下來就可以判斷X是否為需要刪除的節點了。這一步我們也標記一下,叫它D-1好了

    D-1

    如果X不是要刪除的節點,那麼下降一層,即讓P = X, X = XL或者XR, S = X的另一個子節點,然後進入下一個刪除迴圈(即1)。因為X是紅色,它的子節點必然為黑色,所以符合啟動條件

    如果X正式需要刪除的節點,需要分兩種情況

    1. X恰好是葉子節點,這種情況直接刪除X即可,不會對紅黑樹的性質有任何影響
    2. X為非葉子節點,如果直接刪除X,它的子節點應該如何與它的父節點對接是個很複雜的操作,所以我們採用二叉查詢樹的節點刪除方法:找到該節點的後繼或前驅(如果後繼不存在,再使用它的前驅節點),用它的值代替X的值,然後再去刪除它的後繼或前驅,反覆這個過程直到我們需要刪除的節點是葉子節點

    ok,2這類大的情況就處理好了,下面需要考慮X有至少一個紅色節點的情況

  2. X至少有一個紅色節點。這種情況需要改變一下思路,因為X有至少一個紅色節點,如果X不是要刪除的節點,就沒必要再把X變紅了,如果直接下降一層,X有很大概率直接落到紅色的節點上,這能節省很多時間

    所以對於3這種情況,先判斷X是否為要刪除的節點,這就分成兩種情況

    3.1 X不是要刪除的節點,那麼下降一層。這時X可能落到紅色節點上,也可能落到黑色節點上,兩種情況都需要考慮

    3.1.1 X是紅色節點,那麼X已經符合D-1的刪除條件,可以直接進入D-1

    3.1.2 X是黑色節點,這時需要作一次節點變換。為了更直觀,這裡分成兩個步驟,第一步下降一層,並讓X落到黑色節點上,第二步才是變換

    用Js實現紅黑樹

    用Js實現紅黑樹

    此時X還是黑色,並不滿足進入D-1的條件,但是仔細看X節點的上下結構,P、SR、X構成的子樹正好滿足1的啟動條件。所以下一步是進入1的刪除迴圈

    自此,3.1的所有情況已經處理好了,下面我們來看X是需要刪除的節點這種情況

    3.2 X是需要刪除的節點。因為X有紅色的子節點,所以它不可能是葉子節點,也就是說即使把它變紅也不能直接刪除。在這種情況下,我們在D-1的基礎上稍作修改:找到X的後繼或前驅,用它的值代替X的值,之後下降一層,再一次進入到3的邏輯

這就是自頂向下刪除需要考慮的所有情形了。我畫了一個流程圖,梳理了刪除的邏輯

用Js實現紅黑樹

按照這個流程圖來寫程式碼,會非常清晰~

原始碼

/**
 *  RedBlackTreeNode.js
 *  紅黑樹的節點實現,比普通的二叉樹節點多了color屬性
 */
class RedBlackTreeNode {
    constructor(data, color, lchild, rchild) {
        // validate color
        if (!Color[color]) {
            throw new Error(`color can only be RED or BLACK`);
        } 

        this.color = color;
        this.data = data;
        this.lchild = lchild;
        this.rchild = rchild; 
    } 
}

const Color = {
    "RED": "RED",
    "BLACK": "BLACK"
};

module.exports = {
    RedBlackTreeNode,
    Color,
};
複製程式碼
/**
 * @file 紅黑樹實現
 * @author YDSS
 *
 * Created on Sun May 27 2018
 */

const { RedBlackTreeNode, Color } = require("./RedBlackTreeNode");

class RedBlackTree {
    constructor(arr) {
        this._initialize();
        this.create(arr);
    }

    _initialize() {
        // init NullNode
        this.NullNode = new RedBlackTreeNode(
            Number.NEGATIVE_INFINITY,
            Color.BLACK,
            null,
            null
        );
        this.NullNode.lchild = this.NullNode;
        this.NullNode.rchild = this.NullNode;
        // extra attr for recognizing the NullNode
        this.NullNode.type = "null";
        // init header
        this.header = new RedBlackTreeNode(
            Number.NEGATIVE_INFINITY,
            Color.BLACK,
            this.NullNode,
            this.NullNode
        );
        // init nodes to store parent, grandparent and grandgrandparent
        this.X = null;
        this.P = null;
        this.GP = null;
        this.GGP = null;
        // X's sister
        this.S = null;
    }

    create(arr) {
        arr.forEach(item => {
            this.header = this.insert(item);
        });
    }

    find(val) {
        return this._find(val, this.header);
    }

    _find(val, T) {
        if (!T) {
            return null;
        }

        if (val === T.data) {
            return T;
        }
        if (val > T.data) {
            return this._find(val, T.rchild);
        }
        if (val < T.data) {
            return this._find(val, T.lchild);
        }
    }

    insert(data) {
        this.X = this.P = this.GP = this.GGP = this.header;

        this.NullNode.data = data;
        while (data !== this.X.data) {
            this.GGP = this.GP;
            this.GP = this.P;
            this.P = this.X;

            if (data < this.X.data) {
                this.X = this.X.lchild;
            } else {
                this.X = this.X.rchild;
            }
            if (
                this.X.lchild.color === Color.RED &&
                this.X.rchild.color === Color.RED
            )
                this._handleReorient(data);
        }

        // duplicate
        if (this.X !== this.NullNode) {
            return this.NullNode;
        }

        this.X = new RedBlackTreeNode(
            data,
            Color.RED,
            this.NullNode,
            this.NullNode
        );
        if (data < this.P.data) {
            this.P.lchild = this.X;
        } else {
            this.P.rchild = this.X;
        }
        this._handleReorient(data);

        return this.header;
    }

    _handleReorient(data) {
        this.X.color = Color.RED;
        this.X.lchild.color = Color.BLACK;
        this.X.rchild.color = Color.BLACK;

        if (this.P.color === Color.RED) {
            this.GP.color = Color.RED;

            if (data < this.GP.data !== data < this.P.data)
                this.P = this._rotate(data, this.GP);
            this.X = this._rotate(data, this.GGP);
            this.X.color = Color.BLACK;
        }
        this.header.rchild.color = Color.BLACK;
    }

    /**
     * single rotate
     *
     * @param {*} data
     * @param {RedBlackTreeNode} Parent Parent Node of the subtree will rotate
     */
    _rotate(data, Parent) {
        if (data < Parent.data) {
            return (Parent.lchild =
                data < Parent.lchild.data
                    ? this._singleRotateWithLeft(Parent.lchild)
                    : this._singleRotateWithRight(Parent.lchild));
        } else {
            return (Parent.rchild =
                data > Parent.rchild.data
                    ? this._singleRotateWithRight(Parent.rchild)
                    : this._singleRotateWithLeft(Parent.rchild));
        }
    }

    _singleRotateWithLeft(T) {
        let root = T.lchild;

        T.lchild = root.rchild;
        root.rchild = T;

        return root;
    }

    _singleRotateWithRight(T) {
        let root = T.rchild;

        T.rchild = root.lchild;
        root.lchild = T;

        return root;
    }

    /**
     * find precursor node of this node
     *  if this node doesn't have the left subtree, return null
     *
     * @param {*} data data of current node
     * @return {BinaryTreeNode|Null}
     */
    findPrecursor(node) {
        // let node = this.find(data);

        // if (!node) {
        //     throw new Error(`node with data(${data}) is not in the tree`);
        // }

        if (!node.lchild) {
            return null;
        }

        let pre = node.lchild;
        let tmp;
        while (!this._isNilNode(tmp = pre.lchild)) {
            pre = tmp;
        }

        return pre;
    }

    /**
     * find successor node of this node
     *  if this node doesn't have the right subtree, return null
     *
     * @param {BinaryTreeNode} current node
     * @return {BinaryTreeNode|Null}
     */
    findSuccessor(node) {
        // let node = this.find(data);

        // if (!node) {
        //     throw new Error(`node with data(${data}) is not in the tree`);
        // }

        if (!node.rchild) {
            return null;
        }

        let suc = node.rchild;
        let tmp;
        while (!this._isNilNode(tmp = suc.lchild)) {
            suc = tmp;
        }

        return suc;
    }

    /**
     * delete node by means of top to down
     * 
     * @param {*} val 
     */
    delete(val) {
        // prepare for deleting
        this.header.color = Color.RED;
        this.GP = null;
        this.P = this.header;
        this.X = this.header.rchild;
        this.S = this.header.lchild;

        this._delete(val);
    }

    _delete(val) {
        if (
            this.X.lchild.color === Color.BLACK &&
            this.X.rchild.color === Color.BLACK
        ) {
            // S has two black children
            if (
                this.S.lchild.color === Color.BLACK &&
                this.S.rchild.color === Color.BLACK
            ) {
                this._handleRotateSisterWithTwoBlackChildren();
                // judge if X.data is what we are looking for
                this._handleDeleteXWhenXhasTwoBlackChildren(val);
            }
            // S has at last one red children
            else {
                // single rotate when S with it's red child in a line,
                // reference to avl rotate
                // 2.3
                if (
                    this.S.data > this.P.data ===
                    (this.S.rchild.color === Color.RED)
                ) {
                    this._rotate(this.S.data, this.GP);
                    // change color
                    this.P.color = Color.BLACK;
                    this.X.color = Color.RED;
                    this.S.color = Color.RED;
                    this.S.lchild.color = Color.BLACK;
                    this.S.rchild.color = Color.BLACK;
                    // judge if X.data is what we are looking for
                    this._handleDeleteXWhenXhasTwoBlackChildren(val);
                    // double rotate when S with it's red child in a z-shape line
                    // 2.2
                } else {
                    let firstData =
                        this.S.data < this.P.data
                            ? this.S.rchild.data
                            : this.S.lchild.data;
                    this._rotate(firstData, this.P);
                    this._rotate(this.S.data, this.GP);
                    // change color
                    this.P.color = Color.BLACK;
                    this.X.color = Color.RED;
                    // judge if X.data is what we are looking for
                    this._handleDeleteXWhenXhasTwoBlackChildren(val);
                }
            }
        } else {
            this._handleDeleteXWhenXhasAtLastOneRedChild(val);
        }
    }

    // 2.1
    _handleRotateSisterWithTwoBlackChildren() {
        this.P.color = Color.BLACK;
        this.X.color = Color.RED;
        this.S.color = Color.RED;
    }

    _handleDeleteXWhenXhasTwoBlackChildren(val) {
        if (this.X.data === val) {
            if (this._hasChild(this.X)) {
                val = this._replaceWithSuccessorOrPrecursor(val);
                this._descend(val);
                this._delete(val);
            } else {
                // delete X when it's a leaf
                this._deleteLeafNode(val, this.P);
            }
        } else {
            this._descend(val);
            this._delete(val);
        }
    }

    _handleDeleteXWhenXhasAtLastOneRedChild(val) {
        if (this.X.data === val) {
            val = this._replaceWithSuccessorOrPrecursor(val);
            this._descend(val);
        } else {
            this._descend(val);
        }
        // X is red, enter the phase of judging X's data
        if (this.X.color === Color.RED) {
            this._handleDeleteXWhenXhasTwoBlackChildren(val);
        } else {
            this._handleRotateWhenXIsBlackAndSisterIsRed();
            this._delete(val);
        }
    }

    // 3.1.2
    _handleRotateWhenXIsBlackAndSisterIsRed() {
        let curGP = this._rotate(this.S.data, this.GP);
        // change color
        this.S.color = Color.BLACK;
        this.P.color = Color.RED;
        // fix pointer of S and GP
        this.S = this.X.data > this.P.data ? this.P.lchild : this.P.rchild;
        this.GP = curGP;
    }

    _deleteLeafNode(val, parent) {
        if (parent.rchild.data === val) {
            parent.rchild = this.NullNode;
        } else {
            parent.lchild = this.NullNode;
        }
    }

    _hasChild(node) {
        return !this._isNilNode(node.lchild) || !this._isNilNode(node.rchild);
    }

    _isNilNode(node) {
        return node === this.NullNode;
    }

    /**
     * replace X with it's successor,
     *  if it has no successor, instead of it's precursor
     * @param {*} val the delete data
     *
     * @return {*} updated delete data
     */
    _replaceWithSuccessorOrPrecursor(val) {
        let child = this.findSuccessor(this.X);
        if (!child) {
            child = this.findPrecursor(this.X);
        }
        this.X.data = child.data;

        return child.data;
    }

    /**
     * descend one floor
     *
     * @param {*} val the val of node will be deleted
     */
    _descend(val) {
        this.GP = this.P;
        this.P = this.X;

        if (val < this.X.data) {
            this.S = this.X.rchild;
            this.X = this.X.lchild;
        } else if (val > this.X.data) {
            this.S = this.X.lchild;
            this.X = this.X.rchild;
        }
        // val === this.X.data when X's successor or precursor
        //  is it's child, in this situation it's wrong to choise
        //  where X to go down by comparing val, cause X.data is equal
        //  with new delete value
        else {
            if (val === this.X.lchild) {
                this.S = this.X.rchild;
                this.X = this.X.lchild;
            }
            else {
                this.S = this.X.lchild;
                this.X = this.X.rchild;
            }
        }
    }
}

module.exports = RedBlackTree;
複製程式碼

相關文章