数据结构----红黑树

news2024/11/23 8:02:51

       小编会一直更新数据结构相关方面的知识,使用的语言是Java,但是其中的逻辑和思路并不影响,如果感兴趣可以关注合集。

       希望大家看完之后可以自己去手敲实现一遍,同时在最后我也列出一些基本和经典的题目,可以尝试做一下。大家也可以自己去力扣或者洛谷牛客这些网站自己去练习,数据结构光看不敲是学不好的,加油,祝你早日学会数据结构这门课程。

       知不足而奋进,望远山而前行。

概述

        在开始之前我想说一下,红黑树其实是算比较难的数据结构了,但是要讲清楚红黑树的特性并没有那么难,所以红黑树难就难在它的实现上,所以手撕红黑树这件事,不能只有理论,更多应该去实践,正所谓纸上得来终觉浅 绝知此事要躬行。

        本次我们学习的红黑树也是一种自平衡的二叉搜索树,和上一次我们学习的AVL树相比,红黑树的插入和删除时旋转次数更少。红黑树这一块还是有一定难度的,需要深刻理解上一节所介绍的旋转机制。

        红黑树有以下这些特性:

        1. 所有节点都有两种颜色:红与黑

        2. 所有null都视为黑色

        3. 红色节点不能相邻

        4. 根节点是黑色

        5. 从根到任意一个叶子节点,路径中的黑色节点数一样(黑色完美平衡)

        下图所示就是一个红黑树,对于上面五种特性,其实都是满足的。

        

        上面这五条特性多看几遍,因为下面操作时都要遵循特性才能实现出平衡的红黑树。

实现

    结构

        红黑树的结构在节点上其实也就是多了一个颜色的属性。但是红黑树的删除操作完美需要用到父节点,所以完美额外还需要一个变量去记录当前节点的父节点,另外颜色属性初始化为红色,也就是每个节点一开始默认就是红色。

public class RedBlackTree {

    enum Color {
        Red, Black;
    }

    private Node root;
    private static  class  Node{
        int key;
        int value;
        Node left;
        Node right;
        Node parent;  // 父节点
        Color color = Color.Red; // 颜色
    }
}

工具方法 

        熟悉完红黑树的结构后,我们也同样来实现几个好用的工具方法,目的是后续实现crud时可以调用工具方法来解决一些代码的复用以及提升代码的健壮性。

        首先我们来实现一个判断当前节点是否是左孩子的方法,这个因为我们记录了当前节点的父节点所以很好判断。

        // 是不是左孩子
        boolean isLeftChild() {
            return parent != null && parent.left == this;
        }

         接着我们来实现一个找叔叔节点的方法,同样我们知道父节点所以这个方法实现起来也是非常简单的。

        // 找到叔叔节点
        Node uncle() {
            if (parent == null || parent.parent == null) {
                return null;
            }
            if (parent.isLeftChild()) {
                return parent.parent.right;
            } else {
                return parent.parent.left;
            }
        }

        第三个工具方法,我们来实现找到当前节点的兄弟节点,这个对于大家来说相信也是轻轻又松松,舒舒又服服。

        // 找到兄弟节点
        Node sibling() {
            if (parent == null) {
                return null;
            }
            if (this.isLeftChild()) {
                return parent.right;
            } else {
                return parent.left;
            }
        }

         第四个工具方法我们来判断当前节点是否是红色还是黑色,这个方法主要是增强代码健壮性,不要让外界直接可以访问颜色属性。

    // 判断节点是否是红色
        boolean isRed(Node node) {
        return node != null && node.color == Color.Red;
    }

    // 判断节点是否是黑色
    boolean isBlack(Node node) {
        //    return !isRed(node);
        return node == null || node.color == Color.Black;
    }

        既然红黑树也是自平衡的树形结构,那么红黑树底层也是要通过旋转机制来实现自平衡的,所以接下来我们来实现一下红黑树的左旋和右旋。其实大部分逻辑和前面的还是一样的,不同的就是我们这次节点多了一个parent属性,所以在旋转时候要处理好parent指向,第二个不同的就是之前我们是通过返回值的形式返回新的根节点,这一次我们没有返回值,我们是要在旋转内就把新根的父子关系建立好。

    //  右旋  1. parent 的处理  2. 没有返回值
    private void rightRotate(Node node) {
        Node parent = node.parent;
        Node leftNode = node.left;
        Node leftNodeRight = leftNode.right;
        if (leftNodeRight != null) {
            leftNodeRight.parent = node;
        }
        leftNode.right = node;
        leftNode.parent = parent;
        node.left = leftNodeRight;
        node.parent = leftNode;
        if (parent == null) {
            root = leftNode;
        }
        if (parent.left == node) {
            parent.left = leftNode;
        } else {
            parent.right = leftNode;
        }
    }

    //  左旋
    private void leftRotate(Node node) {
        Node parent = node.parent;
        Node rightNode = node.right;
        Node rightNodeLeft = rightNode.left;
        if (rightNodeLeft != null) {
            rightNodeLeft.parent = node;
        }
        rightNode.left = node;
        rightNode.parent = parent;
        node.right = rightNodeLeft;
        node.parent = rightNode;
        if (parent == null) {
            root = rightNode;
        }
        if (parent.left == node) {
            parent.left = rightNode;
        } else {
            parent.right = rightNode;
        }
    }

         

        对照上面这颗红黑树来看左旋和右旋代码,相信这对有了旋转基础的你来说没有问题。

        如果有对左旋后右旋代码一点也不熟悉的可以看我上一篇AVL树,那里对于旋转机制做了详细的图文解释。

数据结构----AVL树-CSDN博客

新增

        其实网上有许多不同的红黑树讲解方式,比如什么左倾红黑树,2-3树等等,这里我就是给出经典的红黑树的架子,了解了基础才可以更好去学习别的实现。

        红黑树的新增操作其实和前面讲解的树形结构的新增基本上都是一样的,正常删除,不同的就是当遇到红红不平衡时进行调整。在代码层面不同的就是我们在插入节点时我们需要将新节点的parent属性也设置一下,另外当新增节点与它的父节点都是红色的时候,这就是红红不平衡,这时就需要进行调整。调整策略其实前人也已经给出策略了,我们只需要根据策略实现代码就好了。   

 

         以下就是红黑树的插入代码,实现起来还是有些复杂的,处理好逻辑,情况一个一个处理相信你也可以实现出来的。

    // 新增或更新
    // 正常增,遇到红红不平衡进行调整。
    public void put(int key, int value) {
        Node p = root;
        Node parent = null;
        while (p != null) {
            parent = p;
            if (key < p.key) {
                p = p.left;
            } else if (key > p.key) {
                p = p.right;
            } else {
                p.value = value; // 更新
                return;
            }
        }
        Node inserted = new Node(key, value);
        if (parent == null) {
            root = inserted;
        } else if (key < parent.key) {
            parent.left = inserted;
            inserted = parent;
        } else {
            parent.right = inserted;
            inserted = parent;
        }
        fixRedRed(inserted);
    }

    void fixRedRed(Node x) {
        // case 1 插入节点时根节点,变黑即可
        if (x == root) {
            x.color = Color.Black;
            return;
        }
        // case 2 插入节点父亲是黑色,无需调整
        if (isBlack(x.parent)) {
            return;
        }
        Node parent = x.parent;
        Node uncle = x.uncle();
        Node grandparent = parent.parent;
        /* case 3 叔叔为红色
            需要将父亲,叔叔变黑,祖父变红,然后对祖父做递归处理
        */
        if (isRed(uncle)) {
            parent.color = Color.Black;
            uncle.color = Color.Black;
            grandparent.color = Color.Red;
            fixRedRed(grandparent);
            return;
        }
        // case 4 叔叔为黑色
        if (parent.isLeftChild() && x.isLeftChild()) { // LL
            parent.color = Color.Black;
            grandparent.color = Color.Red;
            rightRotate(grandparent);
        } else if (parent.isLeftChild() && !x.isLeftChild()) { // LR
            leftRotate(parent);
            x.color = Color.Black;
            grandparent.color = Color.Red;
            rightRotate(grandparent);
        } else if (!parent.isLeftChild() && !x.isLeftChild()) { // RR
            parent.color = Color.Black;
            grandparent.color = Color.Red;
            leftRotate(grandparent);
        } else { // RL
            rightRotate(parent);
            x.color = Color.Black;
            grandparent.color = Color.Red;
            leftRotate(grandparent);
        }
    }

        红黑树的遍历其实和之前的逻辑是一模一样的,这里就不做实现了,相信大家都会。其实树形结构的遍历都是一样的。 

删除

        删除节点其实也是和前面的大差不差,但是这里我们介绍一个新的删除方法叫做李代桃僵。

        例如下面这颗红黑树假如我们要删除节点5,我们可以将节点五的key和value与它的后继节点(节点6)交换,接着我们只要删除节点6也就实现了删除节点五的目的。

        另外在删除时如果遇到黑黑不平衡,我们需要进行调整,同样调整的策略前人也已经给我们总结好了,我们只需要根据策略实现代码就好了。

 

    

 

        以下就是红黑树的删除代码,实现起来同样也是比较复杂的,处理好逻辑,情况一个一个处理相信你也是可以实现出来的。  

    // 删除
    // 正常删除,会用到李代桃僵,遇到黑黑不平衡进行调整。
    public void remove(int key) {
        Node deleted = find(key);
        if (deleted == null) {
            return;
        }
        doRemove(deleted);
    }

    private void doRemove(Node deleted) {
        Node replaced = findReplaced(deleted);
        Node parent = deleted.parent;
        // 没有孩子
        if (replaced == null) {
            // case 1 删除的是根节点
            if (deleted == root) {
                root = null;
            } else {
                if (isBlack(deleted)) {
                    // 复杂调整
                    fixDoubleBlack(deleted);
                } else {
                    // 红色叶子,无需任何处理
                }
                if (deleted.isLeftChild()) {
                    parent.left = null;
                } else {
                    parent.right = null;
                }
                deleted.parent = null;
            }
            return;
        }
        // 有一个孩子
        if (deleted.left == null || deleted.right == null) {
            // case 1 删除的是根节点
            if (deleted == root) {
                root.key = replaced.key;
                root.value = replaced.value;
                root.left = root.right = null;
            } else {
                if (deleted.isLeftChild()) {
                    parent.left = replaced;
                } else {
                    parent.right = replaced;
                }
                replaced.parent = parent;
                deleted.left = deleted.right = deleted.parent = null;
                if (isBlack(deleted) && isBlack(replaced)) {
                    // 复杂调整
                    fixDoubleBlack(replaced);
                } else {
                    // case 2
                    replaced.color = Color.Black;
                }
            }
            return;
        }
        // case 0 有两个孩子 用李代桃僵 将其转换为有一个孩子或者没有孩子的情况
        int tmpKey = deleted.key;
        deleted.key = replaced.key;
        replaced.key = tmpKey;
        int tmpValue = deleted.value;
        deleted.value = replaced.value;
        replaced.value = tmpValue;
        doRemove(replaced);
    }

    // 处理双黑(case3,case4,case5)
    private void fixDoubleBlack(Node x) {
        if (x == root) {
            return;
        }
        Node parent = x.parent;
        Node sibling = x.sibling();
        // case 3 兄弟节点是红色
        if (isRed(sibling)) {
            if (x.isLeftChild()) {
                leftRotate(parent);
            } else {
                rightRotate(parent);
            }
            parent.color = Color.Red;
            sibling.color = Color.Black;
            fixDoubleBlack(x);
            return;
        }
        if (sibling != null) {
            // case 4 兄弟是黑色,两个侄子也是黑色
            if (isBlack(sibling.left) && isBlack(sibling.right)) {
                sibling.color = Color.Red;
                if (isRed(parent)) {
                    parent.color = Color.Black;
                } else {
                    fixDoubleBlack(parent);
                }
            } else {  // case 5 兄弟是黑色,侄子有红色
                // LL
                if (sibling.isLeftChild() && isRed(sibling.left)) {
                    rightRotate(parent);
                    sibling.left.color = Color.Black;
                    sibling.color = parent.color;
                } else if (sibling.isLeftChild() && isRed(sibling.right)) {
                    // LR
                    sibling.right.color = parent.color;
                    leftRotate(sibling);
                    rightRotate(parent);
                } else if (!sibling.isLeftChild() && isRed(sibling.left)) {
                    // RL
                    sibling.left.color = parent.color;
                    rightRotate(sibling);
                    leftRotate(parent);
                } else {
                    // RR
                    leftRotate(parent);
                    sibling.right.color = Color.Black;
                    sibling.color = parent.color;
                }
                parent.color = Color.Black;
            }
        } else {
            fixDoubleBlack(parent);
        }
    }


    // 查找删除节点
    private Node find(int key) {
        Node p = root;
        while (p != null) {
            if (key < p.key) {
                p = p.left;
            } else if (key > p.key) {
                p = p.right;
            } else {
                return p;
            }
        }
        return null;
    }

    // 查找剩余节点
    private Node findReplaced(Node deleted) {
        if (deleted.left == null && deleted.right == null) {
            return null;
        }
        if (deleted.left == null) {
            return deleted.right;
        }
        if (deleted.right == null) {
            return deleted.left;
        }
        Node s = deleted.right;
        while (s.left != null) {
            s = s.left;
        }
        return s;
    }

         其实至此我们已经完成了手撕红黑树这件事,相信看到这里的你真得好好奖励一下自己,非常不错,红黑树真的是比较男的数据结构了。

相关题目

        红黑树这一块没有什么题目推荐,只要可以把红黑树给实现出来就很不错了,另外在后面的哈希表中我们还会再一次见到它。

        如果不是天才,就请一步一步来。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2070548.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

DRF——Filter条件搜索模块

文章目录 条件搜索自定义Filter第三方Filter内置Filter 条件搜索 如果某个API需要传递一些条件进行搜索&#xff0c;其实就在是URL后面通过GET传参即可&#xff0c;例如&#xff1a; /api/users?age19&category12在drf中也有相应组件可以支持条件搜索。 自定义Filter …

学习2d直线拟合-2

参考文章 直线拟合算法&#xff08;续&#xff1a;加权最小二乘&#xff09;_加权拟合直线法-CSDN博客 对比了参考文中和opencv中的直线拟合权重&#xff0c;不知道理解的对不对&#xff0c;前者是权重平方&#xff0c;后者没有平方

迷雾大陆辅助:VMOS云手机助力升级装备系统秘籍!

在《迷雾大陆》的广阔世界中&#xff0c;装备的选择和获取对于每一位冒险者来说都是至关重要的。为了让玩家能够更轻松地管理装备并在冒险中获得更高的效率&#xff0c;VMOS云手机提供了专属定制版云手机&#xff0c;内置游戏安装包&#xff0c;不需要重新下载安装游戏。VMOS云…

【VectorNet】vectornet网络学习笔记

文章目录 前言(vectornet算法流程)(向量表示)(图构建)(子图构建)(全局图构建)(解码器: 轨迹预测)(辅助研究)(损失函数)(实验)(问题厘清) VectorNet Overview 前言 论文: https://arxiv.org/pdf/2005.04259代码: https://github.com/xk-huang/yet-another-vectornet年份: 2020.…

Hadoop联邦模式搭建

在Hadoop架构中提供了三类搭建方式&#xff0c;第一类是给测试或开发人员使用的伪分布式或单NN节点搭建方式&#xff0c;第二类是用来商用化并解决NN单点故障的HA搭建方式&#xff0c;第三类就是这里要说的联邦模式&#xff0c;它本身也是一种供给商用的模式&#xff0c;但是它…

【Apache Doris】周FAQ集锦:第 19 期

【Apache Doris】周FAQ集锦&#xff1a;第 19 期 SQL问题数据操作问题运维常见问题其它问题关于社区 欢迎查阅本周的 Apache Doris 社区 FAQ 栏目&#xff01; 在这个栏目中&#xff0c;每周将筛选社区反馈的热门问题和话题&#xff0c;重点回答并进行深入探讨。旨在为广大用户…

openmediavault 存储安装

1、简介 openmediavault NAS存储&#xff0c;支持linux和windows文件共享&#xff08;文件系统共享&#xff09;&#xff0c;有中文web界面&#xff0c;有filebrowser插件可以web界面管理、下载文件&#xff0c;有FTP插件支持ftp操作&#xff0c;有用户管理&#xff1b;插件丰富…

“七人团裂变风暴:重塑社交电商格局

在当今商业浪潮中&#xff0c;七人共创团购模式以其独特的魅力&#xff0c;正引领着中小型企业走向市场的新高地。这一模式巧妙融合了社交电商的精髓与拼购的乐趣&#xff0c;不仅加速了用户群体的指数级增长&#xff0c;还极大地提升了产品的市场渗透率与品牌影响力。同时&…

TQSDRPI开发板教程:单音回环测试

将我提供的启动文件复制到SD卡中&#xff0c;并插入开发板&#xff0c;插入串口线&#xff0c;启动模式设置为SD卡启动&#xff0c;开启开关。提供的文件在文章末尾。 ​ 查看串口输出内容 ​ 在串口输出的最后有写命令可以使用 ​ 在串口输入如下内容可以对输出的信号进…

计算机毕业设计选题推荐-游戏比赛网上售票系统-Java/Python项目实战

✨作者主页&#xff1a;IT研究室✨ 个人简介&#xff1a;曾从事计算机专业培训教学&#xff0c;擅长Java、Python、微信小程序、Golang、安卓Android等项目实战。接项目定制开发、代码讲解、答辩教学、文档编写、降重等。 ☑文末获取源码☑ 精彩专栏推荐⬇⬇⬇ Java项目 Python…

二叉树的分层遍历、栈的压入弹出序列

本章主要来讲解两个OJ题&#xff0c;针对每个OJ题我分三部分来解决&#xff0c;分别是题目解析&#xff08;主要弄清楚题目要求我们解决什么问题&#xff09;&#xff0c;算法原理&#xff0c;代码编写&#xff0c;接下来让我们进入正题。 一、二叉树的分层遍历 1.题目解析 题…

VSCODE 使用正则表达式匹配替换有规律的行

需求描述 我有类似的文本 count count_l24 count_l32 count count count我需要逐行替换l24,l32所在行&#xff0c;其他行保留。 步骤 替换的时候找到正则表达式的选项 输入: ^._l.$ 替换为空行就行.

攻防世界-web题型-7星难度汇总-个人wp

Web_php_wrong_nginx_config 这个题目nginx配置错误 随便输入显示网站还没建设好。。。 先信息收集一下 换了个无敌好看的终端 没想到7星了还玩这些。。。 看了admin的页面需要先登录 现在的问题是如果读取到这个文件 这个hack的文件也没有东西 到此就不知道了&#xff0…

【二叉树---堆的C语言实现】

1.树的概念与结构 树是一种非线性的数据结构&#xff0c;它n&#xff08;N>0&#xff09;个有限节点组成一个具有层次关系的集合。把它叫做树是因为它看起来像一棵倒挂着的树&#xff0c;也就是说它是根朝上&#xff0c;而叶朝下的。 有一个特殊的节点&#xff0c;称为根节…

【基础算法】位运算

位运算 概念位运算模板模板题 概念 异或&#xff08;x⊕y或x ^ y&#xff09; 高低位交换:https://www.luogu.com.cn/problem/P1100 题意&#xff1a;给定一个32 3232位整数x xx&#xff0c;在二进制下交换其前16 1616位与后16 1616位&#xff0c;输出最终的数。 答案为ans (…

JVM系列--垃圾回收

在C/C这类没有自动垃圾回收机制的语言中&#xff0c;一个对象如果不再使用&#xff0c;需要手动释放&#xff0c;否则就会出现内存泄漏。内存泄漏指的是不再使用的对象在系统中未被回收&#xff0c;内存泄漏的积累可能会导致内存溢出。 在这段代码中&#xff0c;通过死循环不停…

besier打断和升阶,高阶性质

欢迎关注更多精彩 关注我&#xff0c;学习常用算法与数据结构&#xff0c;一题多解&#xff0c;降维打击。 问题描述 对besier曲线在u处打断&#xff0c;生成两条besier曲线对besier曲线升阶处理 bezier高阶性质 求导推导 P ( t ) ∑ i 0 n B i n ( t ) b i \boldsymbol …

uniapp中H5网页怎么实现自动点击事件

<template><view><button ref"myButton" click"handleClick">点击我</button></view> </template><script> export default {mounted() {this.$nextTick(() > {const button this.$refs.myButton;console.l…

【系统分析师】-综合知识-计算机系统基础

1、流水线的吞吐率是指流水线在单位时间里所完成的任务数或输出的结果数。设某流水线有 5 段&#xff0c;有 1 段的时间为 2ns &#xff0c;另外 4 段的每段时间为 1ns&#xff0c;利用此流水线完成 100 个任务的吞吐率约为&#xff08;16&#xff09;个/s 。 2、矢量图像通过使…

Python+PyCharm安装和配置(详细步骤)

Python的安装步骤可以根据用户选择的安装方式&#xff08;如使用安装包安装或源码安装&#xff09;而有所不同。以下将详细讲解两种安装方式的步骤&#xff0c;并附上源码安装的相关说明。 一、使用安装包安装Python 1. 访问Python官网 打开浏览器&#xff0c;输入Python官…