26 红黑树

目录

1.概念
2.性质
3.节点定义
4.结构
5.插入
6.验证
7.删除
8.红黑树和avl树比较
9.应用

概念

是一种二叉搜索树,但在每个节点上增加一个存储位表示节点的颜色,可以是red或black。通过对任何一条从根到叶子的路径上各个节点着色方式的限制,红黑树确保没有一条路径会比其他路径长处两倍,因此是接近平衡的

在这里插入图片描述

性质

  1. 每个节点不是红色就是黑色
  2. 根节点是黑色的
  3. 如果一个节点时红色的,则它的两个孩子结点是黑色的(不能有连续红色)
  4. 对于每个结点,从该结点到其所有后代叶节点的简单路径上,均包含相同数目的黑色节点
  5. 每个叶子节点都是黑色的(此处的叶子结点指的是空结点)

第5条指的是空节点,方便数出二叉树的路径,上面的红黑树共有11条路径,每条路径都有2个黑色节点

最短路径:全黑
最长路径:一黑一红间隔
假设共有N个黑色节点,最短路径logN,最长路径2logN
假设每条路径都有N个黑色节点,每条路径的结点数量[N,2*N]之间

avl树是绝对平衡,红黑树是相对平横,最长路径不超过最短路径的2倍

节点定义

枚举一个颜色,红和黑,节点中加入颜色

enum color
{
	RED,
	BLACK
};

template <class K, class V>
struct TreeNode
{
	struct TreeNode<K, V>* _parent;
	struct TreeNode<K, V>* _left;
	struct TreeNode<K, V>* _right;
	std::pair<K, V> _kv;
	color _col;

	TreeNode(std::pair<K, V> kv)
		:_parent(nullptr), _left(nullptr), _right(nullptr)
		, _kv(kv), _col(RED)
	{}
};

为什么节点默认是红色?
因为每条路径的黑色节点数量都必须相等,如果插入结点默认为黑色,就会对所有路径造成影响。如果是红色,最多只会影响自己所在的路径

红黑树结构

为了后续实现关联式容器简单,红黑树的实现中可以增加一个头结点,因为根节点必须为黑色,为了与根节点区分,将头结点给成黑色,并且让头结点的pParent域指向红黑树的根节点,pLeft域指向红黑树中最小的结点,_pRight域指向红黑树最大的结点,如下,本节的实现不采用头结点
在这里插入图片描述

插入

红黑树是在二叉搜索树的基础上加入其平衡限制条件,因此红黑树的插入可分为两步:

插入结点

#pragma once
#include <iostream>
#include <assert.h>
#include <queue>

enum color
{
	RED,
	BLACK
};

template <class K, class V>
struct TreeNode
{
	struct TreeNode<K, V>* _parent;
	struct TreeNode<K, V>* _left;
	struct TreeNode<K, V>* _right;
	std::pair<K, V> _kv;
	color _col;

	TreeNode(std::pair<K, V> kv)
		:_parent(nullptr), _left(nullptr), _right(nullptr)
		, _kv(kv), _col(RED)
	{}
};

template <class K, class V>
class RBTree
{
	typedef TreeNode<K, V> node;
public:

	bool insert(const std::pair<K, V>& kv)
	{
		if (_root == nullptr)
		{
			_root = new node(kv);
			_root->_col = BLACK;
			return true;
		}

		node* parent = nullptr;
		node* cur = _root;
		while (cur)
		{
			parent = cur;
			if (kv.first < cur->_kv.first)
			{
				cur = cur->_left;
			}
			else if (kv.first > cur->_kv.first)
			{
				cur = cur->_right;
			}
			else
			{
				return false;
			}
		}
		//插入
		cur = new node(kv);
		cur->_parent = parent;
		if (kv.first < parent->_kv.first)
		{
			parent->_left = cur;
		}
		else
		{
			parent->_right = cur;
		}	
	}

private:
	node* _root = nullptr;
};

检测新节点插入后,红黑树的性质是否遭到破坏

因为新节点的默认颜色是红色,因此:如果其双亲节点的颜色是黑色,没有违反红黑树任何性质,则不需要调整;但当新插入节点的双亲颜色是红色时,就违反了性质三不能有连在一起的红色节点,此时分情况讨论:

约定:cur为当前节点,p为父节点,g为祖父节点,u为叔叔节点

1.当a/b/c/d/e都是空,cur就是新增
在这里插入图片描述

2.c/d/e是每条路径1个黑色节点的红黑树,那么就是x/y/z/r中任意一种
在这里插入图片描述
cur更改,变为黑色节点,左右各连a和b节点,cur就是在a和b插入,就会失衡
在这里插入图片描述

cde共有444种可能,插入位置有4个,就是64*4=256种可能

3.当每条路径变为2个黑色节点的子树
在这里插入图片描述
在这里插入图片描述

第一个是C88,C87…C80,再加上下面两个,第一个2个红色节点是C42,可以换一边,就是C42*2,第二个是C44,如果这些是100种可能,c,d,e就有1003种可能,其他更不用算了

从上面所有肯能找出一种规律,解决插入的相关问题
如果插入结点的双亲是黑色,不需要处理,是红色时才需要下面内容

  • 情况一:cur为红,p为红,g为黑,u存在且为红
  • 注意:此处看到的树,可能是一颗完整的树,也可能是一颗子树
    在这里插入图片描述

上面的右边是左边变色而来,p和u变黑,g变红,就平衡了
如果g是根节点,调整完成后,需要改为黑色

如果g是子树的根,调整完成后,g的双亲如果是黑色,就要继续往上调整
在这里插入图片描述
解决方式,将p和u改为黑,g改为红,然后把g当做cur,继续向上调整

  • 情况二,cur为红,p为红,g为黑,u不存在/存在且为黑

在这里插入图片描述
u的情况有两种:
1.如果u节点不存在,则cur一定是新插入节点,因为如果cur不是新插入节点,则cur和p一定有一个节点的颜色是黑色,就不满足性质4
2.如果u节点存在,则一定是黑色的,那么cur原来的颜色也是黑色,现在看到的红色是因为cur的子树在调整的过程中将cur节点的颜色由黑色变为红色
在这里插入图片描述

在这里插入图片描述
p为g的左孩子,cur为p的左孩子,则进行右单旋转,相反
p为g的右孩子,cur为p的右孩子,则进行左单旋转

  • 情况三,cur为红,p为红,g为黑,u不存在/存在且为黑

和情况二类似
p为g的左孩子,cur为p的右孩子,则针对p做左单旋转,旋转后变成了情况二,再右单旋转
p为g的右孩子,cur为p的左孩子,则针对p做右单旋转,旋转后变成了情况二,再左单旋转

在这里插入图片描述

调整

//父节点是红色调整
while (parent && parent->_col == RED)
{
	node* gdparent = parent->_parent;
	node* uncle;

	if (gdparent->_left == parent)
	{
		uncle = gdparent->_right;
		//第一种情况 叔叔节点是红色,变色
		if (uncle && uncle->_col == RED)
		{
			parent->_col = uncle->_col = BLACK;
			gdparent->_col = RED;
		}
		//第二种情况 分左右
		//     g
		//  par  un
		// cur
		else
		{
			if (cur == parent->_left)
			{
				RotateRight(gdparent);
				parent->_col = BLACK;
				gdparent->_col = RED;
			}
			//     g
			//  par  un
			//    cur
			else
			{
				RotateLeft(parent);
				RotateRight(gdparent);
				cur->_col = BLACK;
				parent->_col = RED;
				gdparent->_col = RED;
			}

			break;
		}
	}
	else
	{
		uncle = gdparent->_left;
		if (uncle && uncle->_col == RED)
		{
			parent->_col = uncle->_col = BLACK;
			gdparent->_col = RED;
		}
		else
		{
			if (cur == parent->_right)
			{
				RotateLeft(gdparent);
				parent->_col = BLACK;
				gdparent->_col = RED;
			}
			//     g
			//  par  un
			//    cur
			else
			{
				RotateRight(parent);
				RotateLeft(gdparent);
				cur->_col = BLACK;
				//parent->_col = RED;
				gdparent->_col = RED;
			}

			break;
		}
	}

	cur = gdparent;
	parent = cur->_parent;
}

//根必须是黑色
_root->_col = BLACK;
return true;

根节点在最后必须变为黑色,在函数最后部分统一改变
首先判断par在g的左边的三种情况,叔叔节点存在且是红色直接变色,更新节点向上。叔叔节点不存在或为黑色,判断cur在par的哪边,采取不同的旋转,然后改变节点颜色
par在g右边是相反

插入过程:
在这里插入图片描述

红黑树的验证

红黑树的验证分为两步:
1.检测是否满足二叉搜索树的(中序遍历是否有序序列)
2.检测是否满足红黑树的性质
先判断根节点是否为黑色,再记录一条路径黑色节点数量用来参考,递归红黑树,遇见黑色节点,黑色节点数量++。如果是红色节点,判断它的父节点是否为红色。遇到空,判断这条路径黑色节点数量和参考值是否一样

bool IsBalance()
{
	if (_root->_col == RED)
	{
		std::cout << "根节点是红色" << std::endl;
		return false;
	}

	int refVal = 0;  //记录最左路径黑色节点的数量,用来参考
	node* cur = _root;
	while (cur)
	{
		if (cur->_col == BLACK)
		{
			refVal++;
		}

		cur = cur->_left;
	}

	return _IsBalance(_root, 0, refVal);
}

bool _IsBalance(node* cur, int blackNum, int refVal)
{
	if (cur == nullptr)
	{
		//判断每条路径黑色节点数量正常
		if (blackNum != refVal)
		{
			std::cout << "黑色节点数量不相等" << std::endl;
			return false;
		}
		return true;
	}
	
	if (cur->_col == BLACK)
	{
		blackNum++;
	}

	if (cur->_col == RED && cur->_parent->_col == RED)
	{
		std::cout << cur->_kv.first << " 连续红色节点" << std::endl;
	}

	return _IsBalance(cur->_left, blackNum, refVal) 
		&& _IsBalance(cur->_right, blackNum, refVal);
}

删除

参考《算法导论》或者《STL源码剖析》
https://www.cnblogs.com/fornever/archive/2011/12/02/2270692.html

这里只提供思路
删除前部分内容和avl树都一样,如果删除的节点有两个孩子,就找前驱节点替换,这样所有情况都变成了删除节点只有一个孩子的情况,如果没有孩子也可以当做只有一个孩子处理

父亲节点称为par,删除节点del,孩子节点为child

  • 第一种情况,删除的结点是红色,它如果有孩子一定是黑色,只需要将par链接到child
  • 第二种情况,删除节点是黑色,child为红色,还是将par连接到child,将child改为黑色
  • 第三种情况,删除节点是黑色,child也为黑色,这时考察del的兄弟节点,v有两种情况

情况1,v是黑色节点,v的左子女是w,根据w的颜色讨论:
下图g为祖父节点,u为del的child节点,v是del的兄弟节点,del节点在g的右孩子,删除后g连接到u
当删除了del时,g的右边ul和ur少了一个黑色节点,g的左边的每条路径wl,wr,vr都有相等的黑色节点

根据节点w的状态,有以下情况:
设平衡路径有2个黑色节点
1.w是红色节点,设wl,wr,vr都有2个黑色节点,wl,wr,vr中各有一个黑色节点,ul和ur无黑色节点。以g为中心右旋,vr接到g的左边,v变为红色,w和g改为黑色。此时,wl和wr里面各有1个黑色,w也是黑色,还是2个。vr中有1个黑色,加上g也是2个,u是黑色,g是黑色,ul和ur路径也是两个黑色节点

2.w是黑色节点,这时看w的有兄弟r节点,根据r分情况
① r是红色节点,通过一次先左后右的双旋,将g染成黑色
wl和wr无黑色节点,rl和rr各有1个黑色。旋转完后,右边增加一个黑色节点
② r是黑色节点,r是黑色节点,这时就要看g的颜色,如果g是红色,只要交换g和子女v的颜色。如果g是黑色,可以做一次右单旋转,将节点v上升染成双重黑色,消除节点u的双重黑色,双重黑色向根的方向移动
在这里插入图片描述
情况2,v是红色节点,考察有子女r,r一定是黑色节点,在看r的左子女s,根绝s的颜色分两种情况:
(1)s是红色,先左后右双旋,s染黑色,r上升,使包含u的路径的黑高度加1
在这里插入图片描述

(2) s是黑色,再看s的右兄弟节点t,根绝t的颜色分情况:
① 如果t为红色,先以t左旋,t替补r的位置,然后再以t先左后右的双旋,消除u的双重黑色
在这里插入图片描述

② 若节点t是黑色,以v为轴右单旋转,并改变v和r的颜色
在这里插入图片描述

节点u是g的左子女的情况是镜像的

红黑树和avl树的比较

都是高效的平衡二叉树,增删改查的时间复杂度都是O( l o g 2 N log_2N log2N),红黑树不追求绝对平衡,只要保证最长路径不超过最短路径的2倍,相对而言,降低了插入和旋转的次数,所以再经常增删的结构中更优,实际中红黑树用的更多

红黑树的应用

1.C++STL库,map/set、mutil_map/mutil_set
2.java库
3.linux内核
4.其他一些库

相关推荐

最近更新

  1. TCP协议是安全的吗?

    2024-06-19 03:22:03       18 阅读
  2. 阿里云服务器执行yum,一直下载docker-ce-stable失败

    2024-06-19 03:22:03       19 阅读
  3. 【Python教程】压缩PDF文件大小

    2024-06-19 03:22:03       18 阅读
  4. 通过文章id递归查询所有评论(xml)

    2024-06-19 03:22:03       20 阅读

热门阅读

  1. Python 3.13 有什么新变化

    2024-06-19 03:22:03       6 阅读
  2. 062、Python 解决命名冲突的两种方式

    2024-06-19 03:22:03       5 阅读
  3. Ribbon与Nginx的区别

    2024-06-19 03:22:03       7 阅读
  4. QT day04

    QT day04

    2024-06-19 03:22:03      6 阅读
  5. Blender下使用python设置骨骼旋转

    2024-06-19 03:22:03       7 阅读
  6. ArcGIS Pro SDK (五)内容 1 地图工程

    2024-06-19 03:22:03       5 阅读
  7. 微信小程序,分享和反馈功能

    2024-06-19 03:22:03       9 阅读
  8. 代码随想录刷题经历

    2024-06-19 03:22:03       5 阅读
  9. 基于估计的无约束预测控制

    2024-06-19 03:22:03       6 阅读