【C++进阶5-红黑树】噩梦般的存在?手提AVLTree暴揍红黑树!

news2024/11/25 4:28:36

今天,带来无数人的噩梦——红黑树的讲解。文中不足错漏之处望请斧正!

如果还没看过AVLTree讲解的一定要去看看,看完才能更好理解红黑树!


是什么

红黑树是自平衡的二叉搜索树。

红黑树的规则:

  1. 每个结点非黑即红
    1. 根结点为黑
    2. 叶子结点为黑(此处的叶子结点指空结点)
  2. 不能有连续的红结点,但可以有连续的黑结点
  3. 每条简单路径上的黑结点数量相同

满足以上规则,就能保证最长路径不超过最短路径的二倍,保持了一种相对宽松的平衡。


实现

*为了降低学习成本,部分细节先略过,等封装map和set再添上。

结构

enum Color
{
    RED, BLACK
};

template<class K, class V>
struct RBTreeNode
{
    pair<K, V> _kv;
    RBTreeNode<K, V>* _left;
    RBTreeNode<K, V>* _right;
    RBTreeNode<K, V>* _parent;
    Color _clr;
    
    RBTreeNode(const pair<K, V>& kv)
    :_left(nullptr),
    _right(nullptr),
    _parent(nullptr),
    _kv(kv),
    _clr(RED)
    {}
};

template<class K, class V>
class RBTree
{
    typedef RBTreeNode<K, V> Node;
public:
		...
private:
		Node* _root = nullptr;
};

结点默认红色?往后看。

Insert

实现思路

BST插入总是找位置+插入,插入后只要保持红黑树的规则不被打破就可以。

新插入的结点设为什么颜色?

设为黑色:必然打破“黑结点数量相同”的规则。

设为红色:可能打破“不能有连续的红节点”的规则。(如果父亲是黑,就不会打破规则)

那自然设为红色。

我们要维持的规则主要就是上面两条。

注:

  • 称新插入的结点为cur
  • 称cur的父结点为parent
  • 称parent除cur外的另一孩子为uncle
  • 称parent的父结点为grandParent

插入新结点cur后:

  1. parent为黑:不打破规则,插入结束
  2. parent为红:打破规则,调整
    1. uncle为红:直接着色
    2. uncle不存在/为黑:旋转+着色

为什么根据uncle就能判断?

若插入后需要调整:

  1. parent必然为红
  2. grandParent必然为黑(不能有连续的红结点)

既然cur、parent、grandParent都确定了,那我们只用根据u来分类即可。

  1. uncle为红
  2. uncle不存在
  3. uncle为黑

情况1:u为红(抽象图)

u为红,直接改色。

在这里插入图片描述

  1. cur为红,parent为红打破了“不能有连续红节点”,所以p必须变黑
  2. parent变黑,到parent的简单路径上都增加了一个黑节点,因为“每条简单路径上黑节点数量相同”,所以到uncle的简单路径上也需要多一个黑节点
  3. 我们还需要考虑g是子树还是根
    1. 若是子树,变红没毛病,但g变红可能向上影响(如果g的parent是红,就打破“不能有连续红结点的规则”),所以我们需要把g当做新一轮的cur继续往后看是否需要调整
    2. 若是根,需要重新变黑(根结点必须是黑色)

在这里插入图片描述

u为红直接变色:p变黑、u变黑、g变红

  1. 当g为根→g变黑
  2. 当g不为根→g作新cur往上走
  3. 当p为黑就停(最差走到默认为黑的根结点)

情况2:u不存在

u不存在,单旋+变色。

u不存在,则cur一定是新增。因为u不存在,就代表右边没有黑,那左边也没有黑:p为红,cur也为红,且没有其他黑,那么cur一定是新增。

在这里插入图片描述

  • p是g的左孩子 = 左高 = 右单旋+变色(g变红,p变黑)
  • p是g的右孩子 = 右高 = 左单旋+变色(g变红,p变黑)
  • 旋转后不向上影响,调整结束

情况3:u为黑,整体过高

*有了AVLTree的积累,我们这就只讲一种单旋,另一种同理,反过来而已

在这里插入图片描述

  • p是g的左孩子、cur是p的左孩子 = 整体左高 = 右单旋+变色(g变红,p变黑)
  • p是g的右孩子、cur是p的右孩子 = 整体右高 = 左单旋+变色(g变红,p变黑)
  • 旋转后不向上影响(不会导致和上面出现连续的红节点,被旋转的部分也没有徒增给节点),调整结束

情况4:u为黑,整体过高且局部过高

在这里插入图片描述

  • p是g的左,cur是p的右 = 高左中右高 = 先对p左单旋,再对g右单旋,最后变色(cur变黑、g变红)
  • p是g的右,cur是p的左 = 高右中左高 = 先对p右单旋,再对g左单旋,最后变色(cur变黑、g变红)
  • 旋转后不向上影响,调整结束

旋转总结

到这里我们发现情况2、3、4有些相似之处,所以我们可以进一步分类:

  1. 种类1:
    1. u为红——直接变色并向上调整
  2. 种类2:
    1. u不存在/u为黑且规则高——单旋+变色解决
    2. u为黑且不规则高——双旋+变色解决

最后,我们要推理一个结论来圆满RBTree调平衡的合理性:种类2一定是由种类1变化而来的。

在这里插入图片描述

依照这个结论:种类1通过变色后向上调整,要么直接解决问题,要么演变为种类2,而种类2旋转后必然能解决问题,那么红黑树的调整我们也就必然解决干净了。

调整参考代码

while (parent && parent->_clr == RED) {
    Node *grandParent = parent->_parent;

    if(parent == grandParent->_left) {
        Node *uncle = grandParent->_right;
        //种类1: 直接变色
        if (uncle && uncle->_clr == RED) {
            parent->_clr = uncle->_clr = BLACK;
            grandParent->_clr = RED;

            cur = grandParent;
            parent = cur->_parent;
        } else { //种类2: 旋转+变色
            if(cur == parent->_left) { //整体过高: 单旋
                rotateR(grandParent);
                parent->_clr = BLACK;
                grandParent->_clr = RED;
            } else { //整体过高+局部过高: 双旋
                rotateL(parent);
                rotateR(grandParent);
                cur->_clr = BLACK;
                grandParent->_clr = RED;
            }
            break; //旋转后不再向上影响,结束调整
        }
    } else { //相反而已
        Node *uncle = grandParent->_left;

        if (uncle && uncle->_clr == RED) {
            parent->_clr = uncle->_clr = BLACK;
            grandParent->_clr = RED;

            cur = grandParent;
            parent = cur->_parent;
        } else {
            if(cur == parent->_right) {
                rotateL(grandParent);
                parent->_clr = BLACK;
                grandParent->_clr = RED;
            } else {
                rotateR(parent);
                rotateL(grandParent);
                cur->_clr = BLACK;
                grandParent->_clr = RED;
            }
            break;
        }
    }
}

旋转参考代码

	void rotateL(Node *parent) {
        Node *subR = parent->_right;
        Node *subRL = subR->_left;
        Node *grandParent = parent->_parent;

        //1. subRL变成parent的右子树
        parent->_right = subRL;
        if (subRL) subRL->_parent = parent;

        //2. parent变成subR的左子树
        subR->_left = parent;
        parent->_parent = subR;

        //3. subR变成局部根或整体根
        if (grandParent == nullptr) { //整体根
            _root = subR;
            _root->_parent = nullptr;
        } else { //局部根
            subR->_parent = grandParent;
            if (parent == grandParent->_left) grandParent->_left = subR;
            if (parent == grandParent->_right) grandParent->_right = subR;
        }
    }

    void rotateR(Node *parent) {
        Node *subL = parent->_left;
        Node *subLR = subL->_right;
        Node *grandParent = parent->_parent;

        //1. subLR变成parent的左
        parent->_left = subLR;
        if (subLR) subLR->_parent = parent;

        //2. parent变成subL的右
        subL->_right = parent;
        parent->_parent = subL;

        //3. subL变成局部根或整体根
        if (grandParent == nullptr) { //整体根
            _root = subL;
            _root->_parent = nullptr;
        } else { //局部根
            subL->_parent = grandParent;
            if (parent == grandParent->_left) grandParent->_left = subL;
            if (parent == grandParent->_right) grandParent->_right = subL;
        }
    }

Insert参考代码

	bool insert(const pair <K, V> &kv) {
        if (_root == nullptr) {
            _root = new Node(kv, BLACK); //根节点必须是黑的
            return true;
        }

        //1. 找位置
        Node *cur = _root;
        Node *parent = nullptr;
        while (cur) {
            parent = cur;
            if (kv.first < cur->_kv.first) {
                cur = cur->_left;
            } else if (kv.first > cur->_kv.first) {
                cur = cur->_right;
            } else if (kv.first == cur->_kv.first) {
                return false;
            } else { assert(false);}
        }

        //2. 插入
        cur = new Node(kv); //默认红色
        cur->_parent = parent;
        if (kv.first < parent->_kv.first)
            parent->_left = cur;
        else
            parent->_right = cur;

        //3. parent的clr为红,向上影响了,需要调整
        while (parent && parent->_clr == RED) {
            Node *grandParent = parent->_parent;

            if(parent == grandParent->_left) {
                Node *uncle = grandParent->_right;
                //种类1: 直接变色
                if (uncle && uncle->_clr == RED) {
                    parent->_clr = uncle->_clr = BLACK;
                    grandParent->_clr = RED;

                    cur = grandParent;
                    parent = cur->_parent;
                } else { //种类2: 旋转+变色
                    if(cur == parent->_left) { //整体过高: 单旋
                        rotateR(grandParent);
                        parent->_clr = BLACK;
                        grandParent->_clr = RED;
                    } else { //整体过高+局部过高: 双旋
                        rotateL(parent);
                        rotateR(grandParent);
                        cur->_clr = BLACK;
                        grandParent->_clr = RED;
                    }
                    break; //旋转后不再向上影响,结束调整
                }
            } else { //相反而已
                Node *uncle = grandParent->_left;

                if (uncle && uncle->_clr == RED) {
                    parent->_clr = uncle->_clr = BLACK;
                    grandParent->_clr = RED;

                    cur = grandParent;
                    parent = cur->_parent;
                } else {
                    if(cur == parent->_right) {
                        rotateL(grandParent);
                        parent->_clr = BLACK;
                        grandParent->_clr = RED;
                    } else {
                        rotateR(parent);
                        rotateL(grandParent);
                        cur->_clr = BLACK;
                        grandParent->_clr = RED;
                    }
                    break;
                }
            }
        }

        _root->_clr = BLACK; //根节点始终为黑
        return true;
    }

其实红黑树还有multi版本,允许重复,有insert_unique和insert_equal,至于相同的插入在左边还是右边就无所谓了,因为高度过大后需要旋转,而不管是插入哪边旋转后的结果都一样。

测试

红黑树的测试不像以前那样打印点信息就能解决,而是要写个方法来确定我们的树100%没问题——颜色没问题、路径长度也没问题。


...
//测试
public:
    //测试RBTree:
    //颜色正确 == 路径长度正确
    //路径长度正确 != 颜色正确
    void inorder() { inorder(_root);}

    bool isBlance()
    {
        if(_root == nullptr) return true;

        if(_root->_clr == RED)
        {
            cout << "违反规则:根为红" << endl;
            return false;
        }

        int refVal = 0;
        Node* left = _root;
        while(left)
        {
            if(left->_clr == BLACK) ++refVal;
            left = left->_left;
        }

        return check(_root, 0, refVal);
    }

private:
    void inorder(Node* root)
    {
        if(root == nullptr) return;

        inorder(root->_left);
        cout << root->_kv.first << ":" << root->_kv.second << endl;
        inorder(root->_right);
    }

    bool check(Node* root, int blackCnt, const int& refVal)
    {
        if(root == nullptr)
        {
            if(blackCnt != refVal)
            {
                cout << "违反规则:黑色结点数量不同" << endl;
                return false;
            }
            return true;
        }

        if(root->_clr == RED && root->_parent->_clr == RED)
        {
            cout << "违反规则:出现了连续红色节点" << endl;
            return false;
        }

        if(root->_clr == BLACK) ++blackCnt;

        return check(root->_left, blackCnt, refVal)
               && check(root->_right, blackCnt, refVal);
    }
};

void testRBTree()
{
//    int a[] = {8, 3, 1, 10, 6, 4, 7, 14, 13};
    int a[] = {16, 3, 7, 11, 9, 26, 18, 14, 15};
//    int a[] = {4, 2, 6, 1, 3, 5, 15, 7, 16, 14};

    RBTree<int, int> t;
    for(auto e : a) {
        t.insert(make_pair(e, e));
    }
    t.inorder();
    cout << t.isBlance() << endl;
}
#include <iostream>
using namespace std;
#include "RBTree.h"

int main() {
    testRBTree();
    return 0;
}
3:3
7:7
9:9
11:11
14:14
15:15
16:16
18:18
26:26
1

为什么Inorder要弄成子函数或者函数重载?

因为类外调用Inorder时得传参,_root是私有成员,想传也没办法访问到私有成员。


对比AVLTree

对平衡的要求没那么严格,插入删除的时候就会少很多旋转。虽然单次查找效率效率是2logN,但对于CPU来说logN和2logN区别很小,红黑树总体来说效率是更高的。


性能分析

红黑树是近似平衡的树,没有什么最坏情况,插入的时间复杂度为O(log(N)),查找也是。


应用场景

  1. C++的STL
  2. Java的库
  3. Linux内核
  4. 其他库

今天的分享就到这里了,感谢您能看到这里。

这里是培根的blog,期待与你共同进步!

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

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

相关文章

程序设计综合实习(C语言):考勤管理系统

一、目的 1&#xff0e;调动创新能力的培养 二、实习环境 Visual Studio 2022 三、实习内容与步骤 问题描述&#xff1a; 每个员工信息包括工号、姓名、年龄、性别、部门等&#xff1b; 功能要求&#xff1a; &#xff08;1&#xff09;能够增加、删除、修改员工信息。 &…

[CTFTraining] ASIS CTF 2019 Quals Unicorn shop

​ 我们随便买一件商品&#xff0c;1~3都显示&#xff1a; ​ 只有第4个显示&#xff1a; ​ 只允许输入一个字符&#xff0c;题目叫Unicorn&#xff0c;猜测为Unicode。在Unicode - Compart搜索比千大的Unicode码&#xff1a; ​ 最后填进去买下商品得到flag。 另外&#…

CDGA 认证:第四章 数据架构(重点章节)习题集解析

1. 企业架构不包括哪项&#xff1f;&#xff08; &#xff09; A 业务架构 B 数据架构 C 系统架构 D 技术架构 【答案解析】DAMA-DMBOK2 P72 2. 关于架构设计生命周期描述错误的是&#xff1f;( ) A 可以是针对当前的 B 可以是面向未来的 C 可以是已实施完成的 D 可以是已经…

Flutter 对话框【代码实践】

Flutter 对话框【代码实践】 文章目录 Flutter 对话框【代码实践】一、普通对话框1、示例一&#xff1a;圆角、背景截图代码 一、普通对话框 1、示例一&#xff1a;圆角、背景 截图 代码 Center(child: ElevatedButton(onPressed: () {showDialog(context: context,builder:…

Spring Boot如何实现分布式系统中的服务发现和注册?

Spring Boot如何实现分布式系统中的服务发现和注册&#xff1f; 随着互联网的快速发展&#xff0c;越来越多的企业开始将自己的业务迁移到分布式系统中。在这种情况下&#xff0c;服务发现和注册变得尤为重要。对于分布式系统中的每个服务来说&#xff0c;它需要知道其他服务的…

数据可视化——使用echars图表展示

目录 1、前言 2、解决方案 2.1、echars&#xff08;前端等组件库&#xff09; 2.2、PPT等其他软件工具 2.3、使用flourish等在线数据可视化制作平台 2.4、自己用代码实现 1、前言 有一个小作业&#xff0c;需要自己收集一组数据&#xff0c;然后进行数据可视化&#xff0…

QListWidget和QListView的使用和item点击事件

QListWidget和QListView很常用&#xff0c;但是使用上功能类似&#xff0c;往往容易分不清区别&#xff0c;但是不知道如何选择。这里总结下二者之间的区别和使用&#xff0c;分享给有需要的人&#xff0c;有需要的可点击收藏。 QListView介绍 QListView是Qt中用于显示列表的一…

程序在内存中的分布

1. 具体分布细节由编译器决定 2. 分布图 3. 静态局部变量通常被存放在程序的.data段中。 一般地&#xff0c;静态局部变量定义在函数体内&#xff0c;在函数执行时&#xff0c;它会在静态存储区分配内存&#xff0c;并且只被初始化一次。因为静态局部变量是在编译阶段产生的&…

SpringCloud GateWay 学习

SpringCloud GateWay 文章目录 SpringCloud GateWay1 Gateway 介绍2 代码实现 1 Gateway 介绍 有一个前后端分离项目&#xff0c;分析如图 使用网关服务Gateway&#xff0c;重构项目架构 Gateway 是在 Spring 生态系统之上构建的 API 网关服务&#xff0c;基于 Spring &#x…

DES,RAS,HASH

是猫咪&#xff0c;我加入了一些猫咪 1.DES Data Encryption Standard&#xff0c;即数据加密标准&#xff0c;是一种使用密钥加密的块算法。设计中使用了分组密码设计的两个原则&#xff1a;混淆&#xff08;confusion&#xff09;和扩散(diffusion)。DES加密算法原理简析_51…

秋招笔试零基础怎么办?自顶向下真题学习法,这样准备就稳啦!

秋招笔试零基础怎么办&#xff1f;自顶向下真题学习法&#xff0c;这样准备就稳啦 秋招临近&#xff0c;是时候提前准备笔试了。想必各位都忙着刷穿leetcode的剑指Offer&#xff0c;或者牛客的往年真题等等 但你真的了解自己的算法知识板块哪里有纰漏吗&#xff1f; 你知道今…

【C++】初识STL

目录 &#x1f31e;专栏导读 &#x1f31b;什么是STL &#x1f31b;STL的版本 &#x1f31b;STL的六大组件 &#x1f31b;STL的重要性 &#x1f31b;STL的缺陷 &#x1f31e;专栏导读 &#x1f31f;作者简介&#xff1a;日出等日落&#xff0c;在读本科生一枚&#xff0…

nginx网站安装服务

nginx概述 一款高性能、轻量级web服务软件稳定性高系统资源消耗低对HTTP并发连接的处理能力高单台物理服务器可支持30000~50000个并发请求 正向代理&#xff1a;通过代理服务器来访问资源&#xff0c;这种代理服务成为正向代理 反向代理&#xff1a;客户端与代理是无感知的&…

【Go LeetDay】总目录(1~83)

Leetcode Golang Day1~10 Golang每日一练(leetDay0001) 1. 两数之和 Two Sum 2. 两数相加 Add Two Numbers 3. 无重复字符的最长子串 Longest-substring-without-repeating-characters Golang每日一练(leetDay0002) 4. 寻找两个正序数组的中位数 Median of two sorted arra…

如何通过帮助文档来减少你的客服咨询量,提高工作效率

相信你的公司网站或者产品中总会设置一个“联系我们”按钮&#xff0c;让客户能够遇到问题随时能够找到客服人员并且快速解决&#xff0c;在创业初期&#xff0c;可能这样的模式没有问题&#xff0c;但是随着客户越来越多&#xff0c;客服的需求也随之增加&#xff0c;客服人员…

【iOS】--对象的底层结构

源码 先转一下源码 //#import <Foundation/Foundation.h> #import <objc/runtime.h>interface LGPerson : NSObject property (nonatomic, strong) NSString *KCName; endimplementation LGPersonendint main(int argc, const char * argv[]) {autoreleasepool {…

DVWA-XSS (Stored) Low/Medium/High低中高级别

「作者简介」&#xff1a;CSDN top100、阿里云博客专家、华为云享专家、网络安全领域优质创作者 「推荐专栏」&#xff1a;对网络安全感兴趣的小伙伴可以关注专栏《网络安全入门到精通》 XSS Stroed 一、Low级别二、Medium级别三、Hign级别 这关是一个论坛功能&#xff0c;把用…

设计模式(六):结构型之代理模式

设计模式系列文章 设计模式(一)&#xff1a;创建型之单例模式 设计模式(二、三)&#xff1a;创建型之工厂方法和抽象工厂模式 设计模式(四)&#xff1a;创建型之原型模式 设计模式(五)&#xff1a;创建型之建造者模式 设计模式(六)&#xff1a;结构型之代理模式 目录 一、…

深入分析——Linux DMA Fence

目录 一 简介 二 基本原理 三 代码实现 3.1 Init 3.2 wait 3.3 signaling 3.4 callback 3.5 signaled 3.6 signal 3.7 refcount & release 四 Fence Status 一 简介 dma-fence是linux 内核中同步原语&#xff0c;它只有两种状态signaled和unsigned。因为其本身的…

华为OD机试之找出经过特定点的路径长度(Java源码)

找出经过特定点的路径长度 题目描述 无 输入描述 输入一个字符串&#xff0c;都是以大写字母组成&#xff0c;每个相邻的距离是 1&#xff0c; 第二行输入一个字符串&#xff0c;表示必过的点。 说明每个点可过多次。 输出描述 经过这些必过点的最小距离是多少 用例 输入 ANT…