0
点赞
收藏
分享

微信扫一扫

红黑树的原理及实现(TypeScript)

木匠0819 2022-04-04 阅读 86
数据结构

         前言:找博客学习红黑树,发现有些比较晦涩难懂,有些讲的不清不楚。幸运地在B站找到一部视频,按照算法导论讲的例子,听懂了。所以好的教材挺重要的。
        然后决定自己写一篇博客讲解一下我的理解和实现,希望能帮助到有需要的小伙伴。
        视频链接:1小时搞定红黑树_哔哩哔哩_bilibili,感兴趣的可以去看看

        抱歉,写完之后发现图里的树画得不严谨,不完整,看上去不是平衡的,但是我也懒得改了。请大家主要考虑情况和解决方案即可。可自行画一个完整的红黑树,再按照解决方案进行操作、思考。

二叉搜索树

        首先了解一下二叉搜索树,它的规则就是父节点的左子树上的点都要比它“小”,右子树上的右子树上的点都要比它“大”。左右顺序不重要,重要的是要有这个顺序,让它是一颗有序的二叉树。当它有序的时候且是一个完全平衡的时候(即除了最后一层,其它层都是满的,没有空节点),它的搜索效率是最高的。类似于二分搜索,时间复杂度是O(logN)。

        查找:类似于二分法,比如要找5,则7-->3-->5。找到返回。

        插入:找到合适的位置,再插入。比如插入10,则 7-->9-->13,比13小,插入左子节点。

        删除:
                如果目标节点没有子节点如1,则直接删除即可。
                如果目标节点有1个子节点如13,则删除13,将节点10顶替上去。 
                如果目标节点有2个子节点,则取其中序遍历的前驱节点或后继节点作为实际删除节点,先交换值,再进行删除操作。如节点9,其中序遍历的后继节点为10,则两节点交换值,再进行删除操作。

        

        修改:即先做删除,再做插入操作。

        至此,二叉搜索树的四种操作讲解完毕,它们的处理都是为了保证二叉搜索树的有序性。

 

左旋和右旋

        再了解一下二叉搜索树的左旋和右旋。

        左旋:
                以目标节点为支点进行向左的旋转。
                条件是目标节点不为空,且右子节点不为空。如下图是以节点a为支点进行左旋。

        右旋:
                以目标节点为支点进行向右的旋转。
                条件是目标节点不为空,且右子节点不为空。如下图是以节点b为支点进行旋转。

         

 

        左旋和右旋是一个对称的操作, 过程比较简单,我也就不做赘述了,相信大家看一看就能理解,这2步的操作,也保证了二叉搜索树的有序性。

红黑树

        基础讲完现在进入红黑树的内容,红黑树也是一颗二叉搜索树。
        二叉搜索树越接近完全平衡,则其查找效率越高。当你进行了删除或插入操作的时候,会破坏平衡,此时则需要进行调整平衡的操作。当然可以通过一系列操作重新调整为完全平衡,但是操作会很繁琐,在频繁插入和删除的时候代价过大。
        红黑树则利用颜色来限制自身的高度,来实现了一种近乎平衡。
        颜色条件:
                1、节点有颜色,红色或者黑色。
                2、根节点必须为黑色,空节点视为黑色。
                3、红色节点不能相连。
                4、以任意节点为起点,到它的叶子节点上的黑色数目要相等。

        从根节点出发,到叶子节点的黑色节点数目为N,则其高度不会超过2N。

        下面来讲红黑树是如果调整颜色的平衡,关注插入和删除即可。查找不改变节点,修改则是先删除后插入。
        颜色平衡的主要思想是要么向上推进,因为到根节点一定能解决,要么就是往可解决的状态进行调整。理解这句话,则可以理解后续的所有操作,后续的操作基本是递归处理。

插入操作

        插入操作首先按照二叉搜索树的规则,进行插入。然后再进行调整,使颜色平衡。
        
        插入的节点为红色,然后根据以下几种情况分别处理,下面的()里会说明一些确定的信息。
                1、根节点为空,则插入的节点作为根节点,设为黑色即可。
                2、父节点为黑色,直接平衡,无需处理。
                3、父节点为红色(此时祖父节点必为黑色)。
                        3.1 叔节点为红色。
                        3.2 叔节点为黑色,插入节点与父节点不同侧,不同侧指父节点是祖父节点的左(右)节点,而插入节点是父节点的右(左)节点。后文不再做赘述。
                        3.3 叔节点为黑色,插入节点与父节点同侧。

        情况1、2很简单,跳过。然后来讲情况3中的具体处理。

        3.1 父节点为红色,叔节点为红色。此时要做的操作是,将父节点和叔节点设为黑色,祖父节点设为红色,当祖父节点是根节点时,仍为黑色。此时,祖父节点的左右子树颜色已经平衡,然后将祖父节点视为新的目标节点,根据情况1、2、3进行处理。
        这一步可视为将祖父节点的黑色分给了父、叔两个子节点。目的就是把问题节点向上推进。

         3.2 父节点为红色,叔节点为黑色,目标节点与父节点不同侧。此时要做的操作是,以父节点为支点进行旋转操作。当目标节点在右侧,则左旋,当目标节点在左侧,则右旋。下图以前者为例。
        旋转完之后,两者同侧。再以父节点作为新的目标节点,此时则进入到了情况3.3,这也是我们的目的。
        

 

         3.3 父节点为红色,叔节点为黑色,目标节点与父节点同侧(该情况有可一步解决的方案)。此时的操作是,祖父节点与父节点颜色进行叫唤,然后以祖父节点为支点,进行旋转。父节点在左侧,则右旋,在右侧则左旋。下图以前者为例。


        

        上面2幅图的没有名字的黑色节点是为了提醒大家,旋转的时候,如果该子节点不为空,记得要做对应的处理。 
        至此,插入情况处理完毕。

删除操作

        首先进行二叉搜索树的删除操作,再调整颜色使其符合规则。
        当目标节点左右子节点均不会空时,我们取中序遍历的后继节点为实际删除节点。将目标节点和实际删除节点交换值,颜色不变。此时实际删除节点即可视为目标节点,再进行删除操作。后文的目标节点
        显然目标节点至多有1个子节点,至少有1个节点是空节点,视为黑色。然后我们做一个逻辑上的约定,删除目标节点之后,会遗留颜色在当前位置,它的子节点会顶替上去,然后当前子节点的颜色视为遗留颜色 和 自身颜色的相加得到的和。如果两个子节点为空,则视为空节点顶替上去。

        如上图所示,灰色表示未知节点,x表示黑或者红。比如当父节点为黑色,子节点为红色时,删除父节点,子节点顶替,则节点的颜色为红+黑。 此时这个有2种颜色的节点在该支路上暂时仍是平衡的,但我们要做的是如何把多的这个颜色处理掉,因为一个节点只能有一个颜色。

        

 

        根据这种设定,我们有两种情况需要处理。
        1、红+黑
        2、黑+黑
                2.1 目标节点为根节点
                2.2 兄弟节点是黑色,且其2个子节点也是黑色
                2.3 兄弟节点是黑色,且其同侧子节点是黑色
                2.4 兄弟节点是黑色,且其同侧子节点是红色

        下面,我们来根据情况一个一个处理。

        1、红+黑
                此时直接丢弃红色即可,显然不会破坏左右子树的平衡。

        2.1 目标节点为根节点
                此时丢弃1个黑色即可,相当于所有路径上都减少了1个黑色节点,仍保持平衡。
        

         理论上上图情况不会出现,黑+黑且为根节点,我的理解是当只有1个根节点,删除根节点的情况下才会出现。此图只是便于理解,无需较真。

        2.2 兄弟节点是黑色,且其2个子节点也是黑色
                2个节点可能都是黑节点,也可以都是空节点(空节点视为黑色)。此时要做的操作是将兄弟节点置为红色,目标节点置为黑色,父节点置为x+黑。此时将父节点视为问题节点,再根据情况1、2进行处理。这一步可视为将目标节点和兄弟节点的黑色往上推了1层,给了父节点。目的就是将问题节点上移。

        2.3 兄弟节点是黑色,且同侧子节点是黑色
                情况的顺序是按优先级来考虑的,所以在不满足2.2的情况下,此时必有1个子节点是红色。按照兄弟节点在右侧进行处理,若左侧则进行对应操作即可。
                此时要做的操作是,左子节点和兄弟节点颜色交换,以兄弟节点为支点进行右旋操作。此时得到了一个新的兄弟节点为黑色,且它的同侧子节点为红色。符合情况2.4,这也是我们的目的。

 

 

        2.4 兄弟节点是红色,且其同侧子节点是红色 
                这一步是有解决方案的。这也是为什么要从2.3调整至2.4。
                此时要做的操作是将兄弟节点与父节点交换颜色,然后以父节点为支点进行左旋。将目标节点上多的1个黑色,赋值给兄弟节点的同侧红色子节点。

        至此颜色平衡。
        图画得不严谨,不完整,看上去不是平衡的。比如父节点右侧,可以考虑调整前左侧是平衡的,那兄弟节点的左子树必然是平衡的,进行左旋之后,它就接在了父节点的右边,此时当然也是平衡的。
        主要理解情况和思想即可。

红黑树的实现(TypeScript)

/** 节点类 */
export class TreeNode {
    /** 键 */
    public key: number;
    /** 值 */
    public value: number;
    /** 是否是红色 */
    public is_red: boolean;
    /** 左子节点 */
    public left: TreeNode;
    /** 右子节点 */
    public right: TreeNode;
    /** 父节点 */
    public parent: TreeNode;

    /**
     * @param key: 键
     * @param value 节点值
     * @param is_red 是否是红色
     * @param left 左子节点
     * @param right 右子节点
     * @param parent 父节点(为空则为根)
     */
    constructor(key: number, value: number, is_red: boolean = true, left?: TreeNode, right?: TreeNode, parent?: TreeNode) {
        this.key = key;
        this.value = value;
        this.is_red = is_red;
        this.left = left;
        this.right = right;
        this.parent = parent;
    }
}

/** 红黑树 */
export class RBTree {
    /** 根节点 */
    public root: TreeNode;

    /**
     * @param root 根节点
     */
    constructor(root?: TreeNode) {
        this.root = root;
    }

    /**
     * 查找一个节点
     * @param key 键
     * @returns 返回目标节点,若为null,则未找到
     */
    public find(key: number): TreeNode {
        let node: TreeNode = this.root;

        let is_find: boolean = false;
        while (node != null) {
            if (node.key === key) {
                is_find = true;
                break;
            } else if (node.key > key) {
                node = node.left;
            } else if (node.key < key) {
                node = node.right;
            }
        }

        return is_find ? node : null;
    }

    /**
     * 插入节点
     * @param node 新增节点
     * @returns 是否插入成功
     */
    public insert(node: TreeNode): boolean {

        node.parent = null;
        node.left = null;
        node.right = null;
        node.is_red = true;

        // 如果根为空,则置为根节点
        if (this.root == null) {
            node.is_red = false;
            this.root = node;
            return true;
        }

        let parent_node: TreeNode = this.root;
        while (parent_node != null) {
            // 如果key重复,则插入失败
            if (parent_node.key === node.key) {
                return false;
            }
            // 向左或者右寻找合适的父节点插入
            if (parent_node.key > node.key) {
                if (parent_node.left == null) {
                    parent_node.left = node;
                    node.parent = parent_node;
                    break;
                } else {
                    parent_node = parent_node.left;
                }
            } else {
                if (parent_node.right == null) {
                    parent_node.right = node;
                    node.parent = parent_node;
                    break;
                } else {
                    parent_node = parent_node.right;
                }
            }
        }

        this.adjustColorAferInsert(node);

        return true;
    }

    /**
     * 插入之后调整颜色
     * @param node 插入的节点
     */
    private adjustColorAferInsert(node: TreeNode) {
        // 1.插入是根节点或其父节点是黑色
        if (node.parent == null || node.parent.is_red == false) {
            return;
        }

        let parent_node: TreeNode = node.parent;
        let grandparent_node: TreeNode = parent_node.parent;
        let is_left: boolean = grandparent_node.left == parent_node;
        let uncle_node: TreeNode = is_left ? grandparent_node.right : grandparent_node.left;
        
        // 2.父节点是红色且叔节点是红色,则将祖父节点的黑色分发下来,将父节点置为新的问题节点,继续递归,根节点除外
        if (uncle_node != null &&  uncle_node.is_red == true) {
            parent_node.is_red = false;
            uncle_node.is_red = false;
            if (grandparent_node == this.root) {
                return;
            } else {
                grandparent_node.is_red = true;
                return this.adjustColorAferInsert(grandparent_node);
            }
        }

        // 3.父节点是红色且叔节点是黑色(空节点视为黑色),子节点和父节点不同侧,以父节点为中心进行旋转,变成同侧,继续递归
        if ((uncle_node == null || uncle_node.is_red == false) && (node == parent_node.left) !== is_left) {
            is_left ? this.leftRotate(parent_node) : this.rightRotate(parent_node);
            return this.adjustColorAferInsert(parent_node);
        }

        // 4.父节点是红色且叔节点是黑色(空节点视为黑色),子节点和父节点同侧,父节点和祖父节点交换颜色,进行对应的旋转即可
        if ((uncle_node == null || uncle_node.is_red == false) && (node == parent_node.left) === is_left) {
            parent_node.is_red = !parent_node.is_red;
            grandparent_node.is_red = !grandparent_node.is_red;
            is_left ? this.rightRotate(grandparent_node) : this.leftRotate(grandparent_node);
        }

        return;
    }

    /**
     * 删除某个key值节点
     * @param key 键值
     * @returns 是否删除成功
     */
    public delete(key: number): boolean {
        let node: TreeNode = this.find(key);
        if (node == null) {
            return false;
        }

        // 先找到要删除的节点
        // 如果目标节点左右子节点不为空,则取中序遍历的后继节点为目标节点
        // 交换key与值,不交换颜色
        let temp_node: TreeNode = node;
        if (node.right != null) {
            node = node.right;
        }
        if (node.left != null && node.right != null) {
            while (node.left != null) {
                node = node.left;
            }
        }
        let temp_value = temp_node.value;
        temp_node.value = node.value;
        node.value = temp_value;
        let temp_key = temp_node.key;
        temp_node.key = node.key;
        node.key = temp_key;

        // 假定删除,进行颜色的调整
        // 此时删除节点最多只有1个子节点
        // 该删除节点与顶替节点颜色做相加处理(空节点视为黑色)
        let parent_node: TreeNode = node.parent;
        let is_left: boolean = parent_node != null && parent_node.left == node;
        let child_node: TreeNode = node.left != null ? node.left : node.right;
        let is_red: boolean = child_node != null ? child_node.is_red : false;
        this.adjustColorAfterDelete(node, is_red);

        // 颜色调整完毕,实际进行删除的操作
        // 删除节点,保留颜色
        if (parent_node != null) {
            is_left == true ? parent_node.left = child_node : parent_node.right = child_node;
        } else {
            this.root = child_node;
        }
        if (child_node != null) {
            child_node.is_red = false;  // 顶替节点最终必然是黑色
            child_node.parent = parent_node;
        }
        node.parent = null;
        node.left = null;
        node.right = null;

        return true;
    }

    /**
     * 删除节点之后调整颜色
     * @param node 替换节点
     * @param is_red 遗留颜色
     */
    private adjustColorAfterDelete(node: TreeNode, is_red: boolean) {
        // 1.红+黑,直接返回
        // 2.黑加黑,但为根节点,直接返回
        if (node == this.root || node.is_red !== is_red) {
            node.is_red = false;    // 这一步是为了处理在情况4之后,直接红+黑的情况
            return;
        }
        
        let parent_node: TreeNode = node.parent;
        let is_left: boolean = node == parent_node.left;
        let brother_node: TreeNode = is_left ? parent_node.right : parent_node.left;

        // 黑加黑的情况下,兄弟节点必不为空
        // 3.兄弟节点是红色, 父节点必为黑色,两者交换颜色,以父节点为支点进行旋转,新的兄弟节点必为黑色,递归
        if (brother_node.is_red === true) {
            brother_node.is_red = false;
            parent_node.is_red = true;
            is_left ? this.leftRotate(parent_node) : this.rightRotate(parent_node);
            return this.adjustColorAfterDelete(node, is_red);
        }

        // 4.兄弟节点是黑色,其2个子节点也是黑色,兄弟节点置为红色,将多的1个黑色上移至父节点,递归
        if ((brother_node.left == null || brother_node.left.is_red === false) && (brother_node.right == null || brother_node.right.is_red == false)) {
            brother_node.is_red = true;
            return this.adjustColorAfterDelete(parent_node, false);
        }

        // 5.兄弟节点是黑色,其同侧的子节点是黑色,不同侧子节点是红色,则交换颜色,进行相应的旋转,递归,目的是让同侧子节点是红色
        if (is_left === true && (brother_node.right == null || brother_node.right.is_red === false)) {
            brother_node.left.is_red = false;
            brother_node.is_red = true;
            this.rightRotate(brother_node);
            return this.adjustColorAfterDelete(node, is_red);
        }
        if (is_left === false && (brother_node.left == null || brother_node.left.is_red === false)) {
            brother_node.right.is_red = false;
            brother_node.is_red = true;
            this.leftRotate(brother_node);
            return this.adjustColorAfterDelete(node, is_red);
        }

        // 6.兄弟节点是黑色,其同侧子节点是红色,同侧子节点置为黑色,兄弟节点与父节点交换颜色,进行旋转,Over
        // 此时必然满足条件无需判断
        is_left === true ? brother_node.right.is_red = false : brother_node.left.is_red = false;
        brother_node.is_red = parent_node.is_red;
        parent_node.is_red = false;
        is_left === true ? this.leftRotate(parent_node) : this.rightRotate(parent_node);        
    }

    /**
     * 修改节点的值
     * @param key 节点的键
     * @param value 新的值
     * @returns 是否修改成功
     */
    public change(key: number, value: number): boolean {
        let node: TreeNode = this.find(key);
        if (node != null) {
            node.value = value;
            return true;
        }

        return false;
    }

    /**
     * 左旋
     * @param node 中心点
     * @returns 是否左旋成功
     */
    private leftRotate(node: TreeNode): boolean {
        // 当前节点为空或右子节点为空,无法左旋
        if (node == null || node.right == null) {
            return false;
        }

        let parent_node: TreeNode = node.parent;
        let right_child: TreeNode = node.right;

        // 将右子节点与父节点相连
        if (parent_node != null) {
            parent_node.left == node ? parent_node.left = right_child : parent_node.right = right_child;
        }
        right_child.parent = parent_node;

        // 调整当前节点与右子节点的关系
        node.parent = right_child;
        node.right = right_child.left;
        right_child.left = node;

        // 判断根节点是否改变
        if (right_child.parent == null) {
            this.root = right_child;
        }

        return true;
    }

    
    /**
     * 右旋
     * @param node 中心点
     * @returns 是否右旋成功
     */
    private rightRotate(node: TreeNode): boolean {
        // 当前节点为空或左子节点为空,无法右旋
        if (node == null || node.left == null) {
            return false;
        }

        let parent_node: TreeNode = node.parent;
        let left_child: TreeNode = node.left;

        // 将右子节点与父节点相连
        if (parent_node != null) {
            parent_node.left == node ? parent_node.left = left_child : parent_node.right = left_child;
        }
        left_child.parent = parent_node;

        // 调整当前节点与右子节点的关系
        node.parent = left_child;
        node.left = left_child.right;
        left_child.right = node;

        // 判断根节点是否改变
        if (left_child.parent == null) {
            this.root = left_child;
        }

        return true;
    }
}
举报

相关推荐

0 条评论