手把手教你实现红黑树

2023-10-29

目录

一.红黑树介绍与优势

二.红黑树的特性

①所有节点不是黑色就是红色

②根节点为黑色

③红色节点的左右孩子节点必须为黑色

④每一条路径均含有相同的黑色节点数

⑤叶子节点为黑色

三.红黑树实现原理

(一).插入节点颜色选择

(二).插入后,父节点是黑色

(三).插入后,父节点是红色

(四).叔叔节点为红色

①父节点、叔叔节点变黑,祖父节点变红

②以祖父节点为“新插入节点”继续向上判断

(五).叔叔节点为黑色、没有叔叔节点

①当前节点为父节点左子树,且在根节点左侧

②当前节点为父节点右子树,且在根节点右侧

③当前节点为父节点右子树,且在根节点左侧

④当前节点为父节点左子树,且在根节点右侧

四.思维导图

五.实现代码


一.红黑树介绍与优势

有兴趣的小伙伴可以先看看这篇文章:手把手教你实现AVL树、平衡二叉树

红黑树是在1972年由Rudolf Bayer发明的。

其特点是从根到叶子节点的所有路径中,最长路径不大于最短路径的2倍。

可以说红黑树是AVL树的plus版,因此,红黑树查找时间与AVL树相同,都为O(log N)。

同时,红黑树没有做到AVL树那种绝对平衡,因此其旋转次数少于AVL树。当数据量大时,其效率要优于AVL树。

正因如此,在实际应用中往往采取红黑树而非AVL树。

二.红黑树的特性

可以说,红黑树的特性正是它的精华与结构依据。因此,想要学懂红黑树,熟练掌握特性是必不可少的一环。

红黑树特性:

①所有节点不是黑色就是红色

②根节点为黑色

③红色节点的左右孩子节点必须为黑色

意思就是不能出现两个红色节点相连的情况。

④每一条路径均含有相同的黑色节点数

从根节点到叶子节点的每一条路径中,黑色节点数量均相同。

⑤叶子节点为黑色

三.红黑树实现原理

实现红黑树,我们需要时刻谨记它的特性,尤其是特性3和4。

(一).插入节点颜色选择

当插入节点时,如果是插入根节点,那么节点颜色为黑色即可,然后插入完成。

如果是孩子节点,那么先将颜色变为红色,为什么?

如果插入节点颜色是黑色,那么势必打破特性4:所有路径黑色节点数相同。

而如果插入红色,可能会打破特性3:不能有相连的红色节点。

相比于特性4,打破特性3的代价更小:插入黑色势必打破特性4,插入红色则有可能打破特性3。

(二).插入后,父节点是黑色

当插入孩子节点后,如果父节点是黑色,那么此时没有打破任何一条特性,任务完成。

(三).插入后,父节点是红色

如果父节点是红色,那么问题就出来了,此时打破了特性3:不能有相连的红色节点

此时需要分类讨论,而依据就是叔叔节点

需要考虑的重点是达成每条路径下均含有相同数目的黑色节点(特性4)且没有相连的红色节点(特性3)。

由此,分成(四)、(五)两种情况

(四).叔叔节点为红色

 将父节点与叔叔节点变为黑色,祖父节点变为红色,然后以祖父节点为“新插入节点”继续向上判断。

①父节点、叔叔节点变黑,祖父节点变红

此时,以祖父为根的树已经满足红黑树的特性要求,因此需要以祖父为出发点继续向上判断,此时祖父的颜色是否依旧符合红黑树,相当于将祖父节点作为“新插入节点”,重新判断

②以祖父节点为“新插入节点”继续向上判断

(五).叔叔节点为黑色、没有叔叔节点

需要注意,此时节点并不是真正新插入的节点,因为新插入的节点父节点为红色,说明祖父节点为黑,如果此时叔叔节点为黑色,那么到父节点的这条路径的黑色节点数量就比到叔叔节点的黑色数量少一,说明插入前就已经不是红黑树,这是不成立的。因此,该情况只能出现在祖父节点向上判断时。

这种情况时,单纯的变色已经不能解决问题了,只能靠旋转+变色来解决。

这里不再具体讲解旋转的过程,与AVL树一致,可以参考这篇博客:手把手教你实现AVL树、平衡二叉树

同时,经过旋转+变色后,此时整颗树已经满足红黑树全部特性,即插入完成。 

这时情况可以分成四类,分别对应不同的旋转策略,

前两种均为单旋转,后两种均为双旋转:

①当前节点为父节点左子树,且在根节点左侧

父节点右旋转,父节点变黑,祖父变红。

        步骤一、父节点右旋转

         步骤二、变色

②当前节点为父节点右子树,且在根节点右侧

父节点左旋转,父节点变黑,祖父节点变红

此处类比情况① ,将父节点右旋即可,不再具体演示 

③当前节点为父节点右子树,且在根节点左侧

cur节点左旋转后右旋转,插入节点变黑,祖父节点变红

         步骤一、cur节点左旋转

        步骤二、cur节点右旋转

          步骤三、变色

④当前节点为父节点左子树,且在根节点右侧

cur节点右旋转后左旋转,插入节点变黑,祖父节点变红

此处类比情况③,cur节点右旋后左旋即可,不再具体演示 

四.思维导图

五.实现代码

#pragma once
using namespace std;
#include<iostream>
#include<stdlib.h>
#include<stdbool.h>
#include<assert.h>
#define BLACK false//用bool定义红黑颜色
#define RED true
template<class V>
class rb_tree_node//红黑树节点
{
public:
	rb_tree_node(V val)
		:_val(val)
	{}

	bool _col = RED;
	rb_tree_node* right = nullptr;
	rb_tree_node* left = nullptr;
	rb_tree_node* parent = nullptr;
	V _val;
};

template<class V>//红黑树
class rb_tree
{
	typedef rb_tree_node<V> Node;
public:
	void inorder()//中序遍历
	{
		_inorder(_root);//调用函数
	}

	bool Is_RB_Tree()//判断是否为红黑树
	{
		int blackNum = 0;
		Node* cur = _root;
		while (cur)
		{
			if (cur->_col == BLACK) blackNum++;
			cur = cur->left;
		}
		return _is_rb_tree(_root, 0, blackNum);//调用判断函数
	}

	bool insert(V val);
	
private:
	void _inorder(Node* node)//中序遍历函数
	{
		if (node == nullptr) return;
		_inorder(node->left);
		cout << node->_val << " ";
		_inorder(node->right);
	}
	bool _is_rb_tree(Node* node, int blackNow, int blackNum);//判断函数

	void Rotate_R(Node* cur);
	void Rotate_L(Node* cur);
	Node* _root = nullptr;
};

template<class V>
void rb_tree<V>::Rotate_R(Node* cur)//右旋转
{
	Node* parent = cur->parent, *node_r = cur->right, *grandfather = parent->parent;

	if (node_r)
	{
		node_r->parent = parent;
	}
	parent->left = node_r;

	cur->right = parent;
	cur->parent = grandfather;

	if (grandfather)
	{
		if (grandfather->left == parent) grandfather->left = cur;
		else grandfather->right = cur;
	}
	else _root = cur;
	parent->parent = cur;
}

template<class V>
void rb_tree<V>::Rotate_L(Node* cur)//左旋转
{
	Node* parent = cur->parent, *node_l = cur->left, *grandfather = parent->parent;

	if (node_l)
	{
		node_l->parent = parent;
	}
	parent->right = node_l;

	parent->parent = cur;
	cur->left = parent;

	cur->parent = grandfather;
	if (grandfather)
	{
		if (grandfather->left == parent) grandfather->left = cur;
		else grandfather->right = cur;
	}
	else _root = cur;
}

template<class V>
bool rb_tree<V>::_is_rb_tree(Node* node, int blackNow, int blackNum)
{
	if (node == nullptr) return true;
	if (node->_col == BLACK) blackNow++;
	if (node->left == nullptr && node->right == nullptr)
	{
		if (node->_col == RED && node->parent->_col == RED) return false;
		return blackNow == blackNum;
	}

	if (node != _root && node->_col == RED && node->parent->_col == RED) return false;

	return _is_rb_tree(node->left, blackNow, blackNum) &&
	_is_rb_tree(node->right, blackNow, blackNum);

}


template<class V>
bool rb_tree<V>::insert(V val)
{
	if (_root == nullptr)//插入为根节点
	{
		_root = new Node(val);
		_root->_col = BLACK;
		return true;
	}
	Node* parent = _root->parent, * cur = _root;
	while (cur)
	{
		if (cur->_val == val) return false;//已存在该节点
		if (cur->_val > val)
		{
			parent = cur;
			cur = cur->left;
		}
		else
		{
			parent = cur;
			cur = cur->right;
		}
	}
    //插入节点
	cur = new Node(val);
	cur->parent = parent;
	if (val < parent->_val)
	{
		parent->left = cur;
	}
	else
	{
		parent->right = cur;
	}
    //根据颜色判断是否红黑树
	while (parent && parent->_col == RED)//父节点为红
	{
		Node* grandfather = parent->parent;
		assert(grandfather);//进入循环,说明一定有祖父节点
		assert(grandfather->_col == BLACK);//进入循环,说明祖父节点一定是黑色
		Node* uncle = nullptr;
		if (parent == grandfather->left) uncle = grandfather->right;
		else uncle = grandfather->left;
		if (parent == grandfather->left)//左
		{
			if (uncle && uncle->_col == RED)//变色 + 向上调整
			{
				parent->_col = uncle->_col = BLACK;
				grandfather->_col = RED;
				cur = grandfather;
				parent = cur->parent;
			}
			else if (uncle == nullptr || uncle->_col == BLACK)//叔叔为空,或叔叔为黑
			{
				if (cur == parent->left)//cur为左子树,右单旋
				{
					Rotate_R(parent);
					grandfather->_col = RED;
					parent->_col = BLACK;
				}
				else//cur为右子树,左+右旋+变色
				{
					Rotate_L(cur);
					Rotate_R(cur);
					grandfather->_col = RED;
					cur->_col = BLACK;
				}
				break;
			}
			else
			{
				assert(false);
			}
		}
		else//右
		{
			if (uncle && uncle->_col == RED)//变色 + 向上调整
			{
				parent->_col = uncle->_col = BLACK;
				grandfather->_col = RED;
				cur = grandfather;
				parent = cur->parent;
			}
			else if (uncle == nullptr || uncle->_col == BLACK)//叔叔为空,或叔叔为黑
			{
				if (cur == parent->right)//cur为右子树,左单旋
				{
					Rotate_L(parent);
					grandfather->_col = RED;
					parent->_col = BLACK;
				}
				else//cur为左子树,右+左旋+变色
				{
					Rotate_R(cur);
					Rotate_L(cur);
					grandfather->_col = RED;
					cur->_col = BLACK;
				}
				break;
			}
			else
			{
				assert(false);
			}
		}
	}
	_root->_col = BLACK;//根节点一定为黑色
	return true;
}

人类精神必须置于技术之上——Albert Einstein


如有错误,敬请斧正

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

手把手教你实现红黑树 的相关文章

  • 五大常用经典算法

    五大常用算法之一 分治算法 一 基本概念 在计算机科学中 分治法是一种很重要的算法 字面上的解释是 分而治之 就是把一个复杂的问题分成两个或更多的相同或相似的子问题 再把子问题分成更小的子问题 直到最后子问题可以简单的直接求解 原问题的解即
  • C语言/C++实现栈操作

    一 栈的概念 栈是一种常用的数据结构 它遵循先入后出 Last In First Out LIFO 的原则 栈的操作只在栈的一端进行 该端被称为栈顶 而另一端称为栈底 栈的基本操作包括压栈 入栈 push 和弹栈 出栈 pop 分别用于将元
  • 50个c/c++源代码网站

    C C 是最主要的编程语言 这里列出了50名优秀网站和网页清单 这些网站提供c c 源代码 这份清单提供了源代码的链接以及它们的小说明 我已尽力包括最佳的C C 源代码的网站 这不是一个完整的清单 您有建议可以联系我 我将欢迎您的建 议 以
  • netty handler的执行顺序(3)

    2019独角兽企业重金招聘Python工程师标准 gt gt gt 今天解决2个问题 1 handler在pipeline当中究竟是如何存储的 2 在遍历handler的过程中 会根据event的不同 调用不同的handler 这一点是如何
  • 树06--二叉树中和为某一值的路径

    树06 二叉树中和为某一值的路径 jz24 题目概述 解析 参考答案 注意事项 说明 题目概述 算法说明 输入一颗二叉树的根节点和一个整数 按字典序打印出二叉树中结点值的和为输入整数的所有路径 路径定义为从树的根结点开始往下一直到叶结点所经
  • 将二叉树转为有序的双向链表

    一 题目要求 输入一棵二叉排序树 现在要将该二叉排序树转换成一个有序的双向链表 而且在转换的过程中 不能创建任何新的结点 只能调整树中的结点指针的指向来实现 include
  • 直线检测方法—LSD论文翻译

    附原文链接 LSD a Line Segment Detector 摘 要 LSD是一个线段检测器 能够在线性时间内得到亚像素级精度的检测结果 它无需调试参数就可以适用于任何数字图像上 并且能够自我控制错误数量的检测 平均来说 一个图像中允
  • 递归算法中的时间复杂度分析

    对于一种算法的时间复杂度分析还是特别重要的 在一些非递归算法中 我们仅仅看运算次数最多的那一行代码可能执行多少次就可以 实际就是看在循环中变量的变化 但是对于递归算法中该怎么分析呢 下面介绍几种递归函数中的算法时间复杂度分析的方法 0 递推
  • 手把手教你实现一个向量

    文章目录 什么是向量 向量提供哪些接口 实现 宏定义 定义类 成员变量 构造函数与析构函数 构造函数 析构函数 成员函数 size get r put r e expand insert r e remove lo hi remove r
  • Hash映射理解

    先说数组 数组优点之一 能通过索引很快定位到值 hashmap 就是利用了数组这个优点 对比 线性映射 定义一个数组 数组的元素是结构体 结构体包括 一对键 值 伪代码表示 a 0 struct Bill 5 a 1 struct KK 6
  • 亚利桑那州立大学周纵苇:研习 U-Net ——现有的分割网络创新

    雷锋网AI研习社按 经典的 Encoder Decoder 结构在目标分割问题中展现出了举足轻重的作用 然而这样一个相对固定的框架使得模型在感受野大小和边界分割精度两方面很难达到兼顾 本次公开课 讲者以 U Net 为案例分析 总结现有的分
  • 『Python基础-15』递归函数 Recursion Function

    什么是递归函数 一种计算过程 如果其中每一步都要用到前一步或前几步的结果 称为递归的 用递归过程定义的函数 称为递归函数 例如连加 连乘及阶乘等 凡是递归的函数 都是可计算的 即能行的 递归就是一个函数在它的函数体内调用它自身 编程语言中的
  • 以太坊系列之十五: 以太坊数据库

    以太坊数据库中都存了什么 以太坊使用的数据库是一个NOSQL数据库 是谷歌提供的开源数据leveldb 这里尝试通过分析以太坊数据库存储了什么来分析以太坊可能为我们提供哪些关于区块链的API 存储内容 NOSQL是一个key value数据
  • 4Sum

    Given an array S of n integers are there elements a b c and d in S such that a b c d target Find all unique quadruplets
  • Linux下进程退出的几种形式

    进程退出 Linux 下进程的退出分为正常退出和异常退出两种 1 正常退出 a 在main 函数中执行return b 调用exit 函数 c 调用 exit 函数 2 异常退出 a 调用about函数 b 进程收到某个信号 而该信号使程序
  • 二叉树结构的建立与遍历

    实验项目 1 编写建立二叉树的二叉链表存储结构 左右链表示 的程序 并以适当的形式显示和保存二叉树 2 完成二叉树的7种遍历操作 3 给定一个二叉树 编写算法完成下列应用 1 判断其是否为完全二叉树 2 求二叉树中任意两个结点的公共祖先 输
  • 插入排序超详解释,一看就懂

    目录 一 插入排序的相关概念 1 基本思想 2 基本操作 有序插入 二 插入排序的种类 三 直接插入排序 1 直接插入排序的过程 顺序查找法查找插入位置 2 使用 哨兵 直接插入排序 四 直接插入排序算法描述 五 折半插入排序 1 查找插入
  • 基数排序代码实现

    详情请看排序总结 传送门 https blog csdn net m0 52711790 article details 121914543 基数排序的知识点我就不贴出来 相信都能搜到对应概念解释 下面就直接上代码 代码解释其实也很清晰了
  • 【数据结构】单链表的定义和操作

    目录 1 单链表的定义 2 单链表的创建和初始化 3 单链表的插入节点操作 4 单链表的删除节点操作 5 单链表的查找节点操作 6 单链表的更新节点操作 7 完整代码 嗨 我是 Filotimo 很高兴与大家相识 希望我的博客能对你有所帮助
  • C++ AVL树(四种旋转,插入)

    C AVL树 四种旋转 插入 一 AVL树的概念及性质 二 我们要实现的大致框架 1 AVL树的节点定义 2 AVL树的大致框架 三 插入 1 插入逻辑跟BST相同的那一部分 2 修改平衡因子

随机推荐

  • C#面向对象编程

    面向对象 C 不是一种纯粹的面向对象编程语言 它提供了多种编程范式 但是 面向对象是C 的一个重要概念 也是 NET 提供的所有库的核心原则 面向对象的三个最重要的概念是继承 封装和多态性 本章将介绍如何使用继承增强基类型 如何创建类层次
  • kafka应用问题

    1 问题一 Connection to node 2 could not be established Broker may not be available 解决办法 1 检查防火墙是否开放相关端口 2 如果是部署在云服务器 检查云服务器
  • C++ 拷贝(复制)构造函数

    拷贝构造函数用以将一个类的对象拷贝给同一个类的另一个对象 比如之前学习过的string类 string s1 string s2 s1 一般情况下的拷贝构造函数 class A private int n double d char s p
  • 小梅哥Xilinx FPGA学习笔记6——参数化设计及模块重用设计流水灯(跑马灯)

    参数化设计及模块重用设计流水灯 功能介绍 1 功能描述 一 代码编写 1 设计文件 2 激励文件 3 仿真图 二 总结 功能介绍 1 功能描述 8个Led灯以0 5s的的速率循环闪烁 参数化设计并且调用三八译码器模块完成该设计 三八译码器模
  • TCP/IP详解 卷1:协议 学习笔记 第六章 ICMP:Internet控制报文协议

    ICMP是IP层的组成部分 用来传递差错报文和其他需要注意的信息 它通常被更高层的协议 TCP UDP 使用 一些ICMP报文把差错返回给用户进程 类型字段可以有15个不同值 用来描述ICMP报文的类型 某些ICMP还使用代码字段的值进一步
  • 【BZOJ 4069】 [Apio2015]巴厘岛的雕塑

    4069 apio2015 巴厘岛的雕塑 Time limit 1000 ms Memory limit 65536 KB Description The province of Bali has many sculptures locat
  • QSharedMemory

    来源 https www devbean net 2013 11 qt study road 2 ipc Qt 提供了四种进程间通信的方式 1 使用共享内存 shared memory 交互 这是 Qt 提供的一种各个平台均有支持的进程间交
  • 难解的AIoT焦虑,华为是否在准备一剂特效药存在?

    几个月前有朋友问我 AIoT到底是什么 跟说了好多年的IoT有什么不同 我是这么回答的 想一想有台空调 可以手机来操控它打开和关闭 你想买不 我家的空调现在就可以 可是从来没用过手机操作 遥控器就在茶几上触手可得 打开手机找到APP再操作太
  • redis详解(二)—— 数据类型详解

    Redis常用数据类型详解 1 Redis最为常用的数据类型主要有以下 String Hash List Set Sorted set pub sub Transactions 在具体描述这几种数据类型之前 我们先通过一张图了解下Redis
  • 信号量与共享内存实现进程间通信(生产者消费者问题为例)

    一 信号量 信号量是IPC的一种 可以看做是一个计数器 计数值为可用的共享资源的数量 信号量可用于多进程的同步 为多个进程提供对共享资源的访问 linux下的信号量的接口函数如下 1 获取信号量 int semget key t key i
  • 学习心得_我的算法学习心得

    关于 严格来说 本文题目应该叫作 我的数据结构和算法面试学习心得 但这个写法实在太绕口 所以干脆叫 我的算法学习心得 希望对大家有帮助 需要说明下 本文主要是应对面试的算法学习 这篇文章讲了什么 对于算法的认知 算法的方法总结 小结 算法的
  • 解决python3 pkl文件打印出的数组有省略号的问题(numpy, pytorch)

    问题描述 python3 load了pkl文件后 发现打印出来的数组有省略号 不能用于继续的计算和操作 import pickle with open filename pkl rb as f data pickle load f prin
  • 'chcp' 不是内部或外部命令,也不是可运行的程序 或批处理文件。 'cmd' 不是内部或外部命令,也不是可运行的...

    打开anaconda promp 提示 chcp 不是内部或外部命令 也不是可运行的程序 或批处理文件 cmd 不是内部或外部命令 也不是可运行的 解决办法 我在安装Anaconda是默认添加了环境变量 此时需要在环境变量的系统变量的pat
  • 经典网络VGGNet介绍

    经典网络VGGNet 其中VGG为Visual Geometry Group 由Karen Simonyan等于2014年提出 论文名为 Very Deep Convolutional Networks for Large Scale Im
  • oracle expdp导出时报 ora-39070:无法打开日志文件

    在通过expdp导出命令导出某个用户的对象时出现以下截图错误 ORA 39002 操作无效 ORA 39070 无法打开日志文件 ORA 39087 目录名
  • MVG学习笔记(1) --无处不在的射影几何

    文章目录 前言 无处不在的射影几何 坐标 齐次性 仿射和欧几里得几何 仿射几何 欧几里得几何 3D欧几里得几何 前言 关于计算机视觉圣经的学习笔记 本次此系列的博文除了本次博文 基本不会包含前言了 参考书 多视图几何 第二版 无处不在的射影
  • python基础一(print函数+变量赋值 )

    1print 函数 注意 敲代码必须是英文输入状态 1 1 无引号 print 123 1 2单引号 print 路飞 1 3 双引号 注意 是英文输入法下的双引号 不是两个单引号 与单引号效果没什么差别 print one piece 1
  • 如何分析和提高大型项目(C/C++)的编译速度?

    C 编译基本原理 对于C C 代码通常来说整个构建过程分为以下几个主要部分 预处理 在此阶段主要完成的工作是将头文件展开 替换宏指令 条件编译展开 消除注释 编译 在此阶段主要将预编译好的文件转换成汇编语言 高级语言 gt LLVM平台无关
  • 生产制造业ERP系统模块

    生产制造业ERP系统模块 1 计划管理系统 1 物料需求管理 支持如下功能 配置产品的管理 用户可以定义可选件 必选件 以及必选件中的可选件 BOM成批修改 BOM合法性 完整性和嵌套性检查 BOM单级正查和反查 多级正查和反查 以及综合查
  • 手把手教你实现红黑树

    目录 一 红黑树介绍与优势 二 红黑树的特性 所有节点不是黑色就是红色 根节点为黑色 红色节点的左右孩子节点必须为黑色 每一条路径均含有相同的黑色节点数 叶子节点为黑色 三 红黑树实现原理 一 插入节点颜色选择 二 插入后 父节点是黑色 三