手把手教你实现红黑树

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(使用前将#替换为@)

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

  • 五大常用经典算法

    五大常用算法之一 分治算法 一 基本概念 在计算机科学中 分治法是一种很重要的算法 字面上的解释是 分而治之 就是把一个复杂的问题分成两个或更多的相同或相似的子问题 再把子问题分成更小的子问题 直到最后子问题可以简单的直接求解 原问题的解即
  • 数据结构中常见的树(BST二叉搜索树、AVL平衡二叉树、RBT红黑树、B-树、B+树、B*树)

    原文 http blog csdn net sup heaven article details 39313731 数据结构中常见的树 BST二叉搜索树 AVL平衡二叉树 RBT红黑树 B 树 B 树 B 树 转载 2014年09月16日
  • 树06--二叉树中和为某一值的路径

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

    一 题目要求 输入一棵二叉排序树 现在要将该二叉排序树转换成一个有序的双向链表 而且在转换的过程中 不能创建任何新的结点 只能调整树中的结点指针的指向来实现 include
  • 4399 C++笔试题

    1 写出一个函数 取到链表中倒数第二个节点 双链表 node getSec List mylist return mylist m tail gt m prev m prev为链表前指针 单链表 node getSec List mylis
  • findBug 错误修改指南

    FindBugs错误修改指南 1 EC UNRELATED TYPES Bug Call to equals comparing different types Pattern id EC UNRELATED TYPES type EC c
  • JavaScript实现数据结构 -- 链表

    文章目录 链表 链表的特点 链表和数组的区别 JS模拟链表 遍历链表 插入节点 删除节点 链表应用 删除链表中的节点 leetcode 237 思路 代码 反转链表 leetcode 206 思路 代码 链表 链表和数组一样是有多个元素组成
  • 逆波兰表达式求值(C语言实现)

    实验项目 从文本文件输入任意一个语法正确的 中缀 表达式 显示并保存该表达式 利用栈结构 把上述 中缀 表达式转换成后缀表达式 并显示栈的状态变化过程和所得到的后缀表达式 利用栈结构 对上述后缀表达式进行求值 并显示栈的状态变化过程和最终结
  • 手把手教你实现一个向量

    文章目录 什么是向量 向量提供哪些接口 实现 宏定义 定义类 成员变量 构造函数与析构函数 构造函数 析构函数 成员函数 size get r put r e expand insert r e remove lo hi remove r
  • 如何根据链表节点数据大小对链表节点进行排序

    对链表排序有两种方法 1 比较了两个节点的大小后 对指针进行改变 从而交换节点的顺序 2 比较了两个节点的大小后 只交换数据域 而不改变指针 从而交换节点的顺序 第二种办法比较简单 本文主要对第二种方法进行讲解 链表节点排序算法 采用 冒泡
  • 数据结构之图的两种遍历实现(C语言版)

    上一期文章分享完了图的两种遍历方式 也是两种很重要的算法 DFS和BFS 这两种算法的应用和重要性我就不多说了 内行的人懂的都懂 今天这文章重要就是来上机实现这两种算法 又由于这两种算法都可以由邻接矩阵和邻接表来表示 博主分享的代码都是上机
  • 字符串09--表示数值的字符串

    字符串09 表示数值的字符串 jz53 题目概述 解析 参考答案 注意事项 说明 题目概述 算法说明 请实现一个函数用来判断字符串是否表示数值 包括整数和小数 例如 字符串 100 5e2 123 3 1416 和 1E 16 都表示数值
  • Unique Binary Search Trees -- LeetCode

    原题链接 http oj leetcode com problems unique binary search trees 这道题要求可行的二叉查找树的数量 其实二叉查找树可以任意取根 只要满足中序遍历有序的要求就可以 从处理子问题的角度来
  • 堆栈01--用两个栈实现队列

    堆栈01 用两个栈实现队列 jz05 题目概述 解析 参考答案 注意事项 说明 题目概述 算法说明 用两个栈来实现一个队列 完成队列的Push和Pop操作 队列中的元素为int类型 测试用例 队列先进先出 输入 1 2 输出 1 2 解析
  • 时间复杂度+常见复杂度解释

    前言 算法的效率 虽然计算机能快速的完成运算处理 但实际上 它也需要根据输入数据的大小和算法效率来消耗一定的处理器资源 要想编写出能高效运行的程序 我们就需要考虑到算法的效率 算法的效率主要由以下两个复杂度来评估 时间复杂度 评估执行程序所
  • 机器学习算法GBDT的面试要点总结-上篇

    1 简介 gbdt全称梯度提升决策树 在传统机器学习算法里面是对真实分布拟合的最好的几种算法之一 在前几年深度学习还没有大行其道之前 gbdt在各种竞赛是大放异彩 原因大概有几个 一是效果确实挺不错 二是即可以用于分类也可以用于回归 三是可
  • 用两个栈实现队列

    目录 一 栈的基本结构及其接口 二 我的队列结构定义 三 我的队列创建及其初始化 四 我的队列入队 五 我的队列出队 六 我的队列取队头元素 七 我的队列判空 八 我的队列销毁 一 栈的基本结构及其接口 栈的结构定义 typedef int
  • 数组实现循环队列(增设队列大小size)

    目录 一 前言 1 如何实现循环 2 如何判断队列为空 3 如何判断队列为满 二 循环队列的结构定义 三 循环队列的创建及其初始化 四 入队 五 出队 六 取队头元素 七 取队尾元素 八 循环队列判空 九 循环队列判满 十 循环队列销毁 一
  • 【数据结构入门精讲 | 第二篇】一文讲清算法复杂度

    上篇文章中我们引入了算法 数据结构 数据类型等概念 而要想衡量一个算法与数据结构是否为优质的 就需要一个衡量标准 这个衡量标准也是在我们实现一个好的算法时要遵循的原则 目录 基本概念 渐进性态 渐进性态数学表征 算法复杂度的运算 顺序搜索算
  • 按照层次遍历结果打印完全二叉树

    按照层次遍历结果打印完全二叉树 按照推论结果 l 层首个节点位置 2 h l 1 l 层节点间距 2 h l 1 1 编码实现 public static

随机推荐

  • 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单级正查和反查 多级正查和反查 以及综合查
  • 手把手教你实现红黑树

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