用Js实现红黑树

学习红黑树,用js撸了一个

红黑树是一个效率很高且稳定的数据结构,插入和删除操作的时间复杂度都是logn。

红黑树的性质:

  1. 每一个节点或者着红色,或者着黑色
  2. 根是黑色
  3. 如果一个节点是红色的,那么它的子节点必须是黑色
  4. 从一个节点到一个Null节点(树叶)的每一条简单路径必须包含相同数目的黑色节点

插入操作以后再补~

删除操作(自顶向下的实现方式)

删除操作是红黑树最难的部分,通常有两种实现方式:自顶向下自底向上。《算法导论》里使用的是自底向上的实现方式,对我而言相当晦涩,又看了几篇类似的实现方式,需要罗列出相当多的情形,实现不易。《数据结构与算法 -- C语言实现》里使用的是自顶向下的实现方式,但只讨论了大致逻辑,并未给出具体实现,最后在这篇文章里找到了完整的实现。自顶向下的方式确实简单易懂,且非常巧妙,我也是用这种方式来实现红黑树的删除操作

在此之前,先复习一下前面列出的红黑树的性质。删除操作之所以复杂,是因为如果需要删除的节点是黑色的,那么直接删除它会破坏性质4。因此,我们需要保证删除该节点之后,能有一种方式修复删除后被破坏的部分。自底向上实现的思路是:先删除节点,然后通过旋转、变色等手段恢复破坏了红黑树性质的部分,而自顶向下实现的思路是:在查找需要删除的节点的路径上,保证每个节点都是红色的(如果不是就要通过变换让它变成红色,且不破坏树的性质),如果它是要删除的节点,就可以安心地删除它。就思路而言,显然自底向上的方式更易理解,好像也更容易实现,但当你去处理修复过程时会发现有相当多的情况需要考虑。而自顶向下的方式看似笨拙,却能够通过巧妙的变换简化变换的过程

总的来说,就是我们要让当前访问的节点X变红,然后再去考虑它是不是需要删除的节点

  1. 启动条件

删除是一个递归函数,在进入递归之前需要确保当前当前结构符合启动条件。这里的结构指以X为中心的部分树结构,可能包含P, S,GP, XL,XR,SL,SR。启动条件如下:

即:X和它的兄弟节点S为黑色,父亲节点P为红色。实现insert时我们做了一个特殊处理,构造了一个假的根节点,值为负无穷,颜色为黑色,因此所有的真实节点都在它的右子树上。它的左节点是一个null节点(黑色),右节点是真正的根节点(必然是黑色)。而自顶向下删除的第一步,就是把根节点涂成红色,这样就天然满足了启动条件

  1. 若X有两个黑色的节点。注意,当X为叶子节点时也适用,因为叶子节点的子节点为Null节点,是黑色

    这时还需要考察S的子节点的情况才能决定如何变换,因此2还需要分几种子情形

    2.1 符合2的条件,同时S也有两个黑色的子节点

    这种情况是最简单的,让X变红只需要变换P,X,S的颜色即可。变换方法如下图

    2.2 符合2的条件,且S有一个红色的左子节点,一个黑色的右子节点

    这种情况下,X变红后,左边就少了一个黑色的节点,只能从P的右子树借一个过来。SL这个红色的节点可以利用起来,让它移到P现在的位置。显然这需要一个双旋转(SL位于整个旋转路径的内侧),变换如下:

    2.3 符合2的条件,且S有一个黑色的左子节点,一个红色的右子节点

    2.2类似,我们要利用SR这个红色节点,因为SR在整个旋转路径的外侧,所以使用一个单旋转

    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落到黑色节点上,第二步才是变换

    此时X还是黑色,并不满足进入D-1的条件,但是仔细看X节点的上下结构,P、SR、X构成的子树正好满足1的启动条件。所以下一步是进入1的删除循环

    自此,3.1的所有情况已经处理好了,下面我们来看X是需要删除的节点这种情况

    3.2 X是需要删除的节点。因为X有红色的子节点,所以它不可能是叶子节点,也就是说即使把它变红也不能直接删除。在这种情况下,我们在D-1的基础上稍作修改:找到X的后继或前驱,用它的值代替X的值,之后下降一层,再一次进入到3的逻辑

这就是自顶向下删除需要考虑的所有情形了。我画了一个流程图,梳理了删除的逻辑

按照这个流程图来写代码,会非常清晰~

源码

/**
 *  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;
复制代码
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值