学习红黑树

红黑树的简介

红黑树是一种自平衡的二叉查找树,是一种高效的查找树。它是由 Rudolf Bayer 于1972年发明,在当时被称为对称二叉 B 树(symmetric binary B-trees)。后来,在1978年被 Leo J. Guibas 和 Robert Sedgewick 修改为如今的红黑树。

红黑树具有良好的效率,它可在 O(logN) 时间内完成查找、增加、删除等操作。因此,红黑树在业界应用很广泛,比如 Java 中的 TreeMap,JDK 1.8 中的 HashMap、C++ STL 中的 map 均是基于红黑树结构实现的。

考虑到红黑树是一种被广泛应用的数据结构,所以我们很有必要去弄懂它。

红黑树的性质

  1. 每个节点是红的或者黑的
  2. 根节点是黑的
  3. 每个叶子节点是黑的
  4. 如果一个结点是红的,则它的两个儿子都是黑的。【这一点说明如果红黑树中的一个结点是红色,那么它的父节点一定是黑色,因为如果父节点是红色,则对于父节点来说,不满足它的两个儿子都是黑色的性质;也说明了从根节点到叶子结点的所有路径上不能有2个连续的红色结点。】
  5. 对每个结点,从该结点到其子孙结点的所有路径上的包含相同数目的黑结点。

红黑树的旋转

红黑树在性质被破坏时会旋转。旋转包括左旋和右旋。
在这里插入图片描述

红黑树的插入

红黑树在插入结点之前,它已经是一颗红黑树。若插入结点z为红色结点。违背了性质4。插入红黑树。z的父节点也是红色,z的祖父结点是黑色,z的叔父结点不确定。

  1. 叔父结点是红色的【把父节点和叔父结点变为黑色,把祖父结点变为红色。】

  2. 若叔父结点是黑色的。z在左子树。树的左边比较重,通过右旋变成右边的样子。
    为什么要旋转?插入结点的这一端比较重,结点数量比较多。
    如何判断哪边重?两颗红色结点相邻,并且当前结点z是左孩子。
    1 把z的父节点改为黑色,祖父结点改为红色,然后进行旋转。
    图1 调整出来的现象: z是黑色, 调整变为红色(89、145由红变黑,140由黑变红)
    图1 调整出来的现象: z是黑色, 调整变为红色(89、145由红变黑,140由黑变红)

    变色:1. 把z的父节点和叔父结点变为黑色;2. 把z的祖父结点变为红色。

  3. 如果z的叔父结点是黑色结点,当前结点z是右孩子。
    这种状态不能一步到位。需要转成中间状态,如图2的右边所示。

图2 转成中间状态
图2 转成中间状态
对应的代码是:

if (z == z->parent->right) //需要进行2次调整
{
	z = z->parent;
	rbtree_left_rotate(T, z);

}

图3 由中间状态转成最终状态
图3 由中间状态转成最终状态

z->parent->color = BLACK;
z->parent->parent->color = RED;

//以z的祖父结点进行旋转
rbtree_right_rotate(T, z->parent->parent);

插入总共以上三种状态。

红黑树的删除更加复杂,先放下。红黑树很经典,如果为了开发,了解性能、时间复杂度即可。自己实现一个红黑树在工作中的可能性极低。
总代码:

#include <iostream>
using namespace std;

#define RED 0
#define BLACK 1
typedef int KEY_TYPE; /*就是说KEY_TYPE现在是一种和int类型等价的类型。*/

typedef struct _rbtree_node
{
	KEY_TYPE key;
	void* value;
	struct _rbtree_node* right;
	struct _rbtree_node* left;
	struct _rbtree_node* parent;
	unsigned char color;
}rbtree_node;

typedef struct _rbtree {
	struct _rbtree_node* root;/*根节点*/
	struct _rbtree_node* nil;/*叶子节点*/
}rbtree;

//左旋、右旋应该带哪些参数? 红黑树【因为需要判断它的左子树、右子树是不是叶子节点,还需要判断父亲节点是不是根节点】
/*
* 左旋 三根指针方向修改 每个方向有两根
* 6根指针,3对
*/
void rbtree_left_rotate(rbtree* T, rbtree_node* x)
{
	rbtree_node* y = x->right; //x的右子树
	x->right = y->left;//两个都要变
	if (y->left != T->nil) //非叶子节点才有parent
	{
		y->left->parent = x;
	}
	y->parent = x->parent;
	//x可能是根节点,判断是左子树还是右子树
	if (x->parent == T->nil)
	{
		T->root = y;
	}//如果是左子树
	else if (x == x->parent->left)
	{
		x->parent->left = y;
	}
	else
	{
		x->parent->right = y;
	}

	y->left = x;
	x->parent = y;
}

void rbtree_right_rotate(rbtree* T, rbtree_node* y)
{
	rbtree_node* x = y->right; //x的右子树
	y->left = x->right;//两个都要变
	if (x->right != T->nil) //非叶子节点才有parent
	{
		x->right->parent = y;
	}
	x->parent = y->parent;
	//x可能是根节点,判断是左子树还是右子树
	if (y->parent == T->nil)
	{
		T->root = x;
	}//如果是左子树
	else if (y == y->parent->right)
	{
		y->parent->right = x;
	}
	else
	{
		x->parent->left = x;
	}

	x->left = y;
	y->parent = x;
}

/*
* 插入需要调整的函数
* 
*/
void rbtree_insert_fixup(rbtree* T, rbtree_node* z)
{
	//z = RED
	while (z->parent->color == RED)
	{
		//如果z的父亲是z的祖父的左子树
		if (z->parent == z->parent->parent->left)
		{
			rbtree_node* y = z->parent->parent->right; //叔父结点:祖父结点的右边

			if (y->color == RED)
			{
				z->parent->color = BLACK; //z的父节点变为黑色
				y->color = BLACK; //z的叔父结点变为黑色
				z->parent->parent->color = RED; //z的祖父结点变为红色

				//回溯【不太明白】
				z = z->parent->parent; //z==RED 才会保证z.parent==RED需要修改

			}
			else //如果叔叔结点是黑色的 y == BLACK
			{
				if (z == z->parent->right) //需要进行2次调整
				{
					z = z->parent;
					rbtree_left_rotate(T, z);

				}

				z->parent->color = BLACK;
				z->parent->parent->color = RED;

				//以z的祖父结点进行旋转
				rbtree_right_rotate(T, z->parent->parent);
			}

		}
		else
		{

		}
	}
}
/*
* 插入
* @param1: tree
* @param2 需要插入的节点: node
*/
void rbtree_insert(rbtree *T, rbtree_node *z)
{
	rbtree_node* x = T->root; //得到根节点
	rbtree_node* y = T->nil;//首先等于空节点
	//一个遍历的过程 循环遍历终止的条件是什么?
	//需要两个遍历的指针,当x指向下一个,y始终指向x之前的位置
	while (x!=T->nil) //等于叶子节点就跳出循环
	{
		y = x;
		if (z->key < x->key) //如果要插入节点比当前节点小,插入左子树,大,插入右子树
		{
			x = x->left;
		}
		else if (z->key > x->key)
		{
			x = x->right;
		}
		else {//等于就是已经存在了 暂时不退出
			return;
		}
	}
	//如果红黑树一个节点也没有。x、y都指向空节点
	if (y == T->nil)
	{
		T->root = z;
	}
	else
	{
		//插入节点z
		if (y->key > z->key)
		{
			y->left = z;
		}
		else
		{
			y->right = z;
		}
	}
	z->parent = y;
	z->left = T->nil;
	z->right = T->nil;
	z->color = RED;
	rbtree_insert_fixup(T, z);
}
int main()
{
	cout << "yes" << endl;


	return 0;
}

拓展:红黑树的应用

1. Linux进程调度CFS

CFS调度算法在Linux内核中使用了红黑树。CFS算法通过使用红黑树来维护进程优先级和时间片的分配,使得每个进程都能公平地获得CPU时间片。CFS算法将所有可运行的进程按照优先级放置在红黑树上,每个进程的优先级被转化为一个虚拟运行时间,运行时间越短的进程优先级越高,运行时间越长的进程优先级越低。当一个进程运行时,它的虚拟运行时间会不断增加,表示它已经使用了一定的CPU时间片。进程调度器会选择虚拟运行时间最小的进程运行,如果有多个虚拟运行时间相同的进程,进程调度器会选择其中最先插入红黑树的进程运行。这样就能保证每个进程都能公平地获得CPU时间片。

2. Nginx Timer事件管理

Nginx的Timer事件管理使用了红黑树。在Nginx中,Timer事件是通过红黑树这种数据结构进行管理的。每个红黑树的节点代表一个定时器事件,节点的值是定时器的超时时间戳。红黑树的最左边的节点代表距离超时时间最近的事件,最右边的节点代表距离超时时间最远的事件。添加、删除、查找定时器事件实际上就是对应于红黑树的插入、删除和查找节点的操作。

3. Epoll事件块的管理

epoll内部使用红黑树管理事件块。epoll是Linux中的一种I/O多路复用技术,用于高效地处理大量文件描述符。在内核实现中,epoll使用了红黑树这种高效数据结构来管理事件块(文件描述符)。具体来说,每个事件块可以对应多个文件描述符,这些文件描述符通过红黑树进行管理,使得查找、添加、删除事件块的操作更加高效。

4. map和unordered_map

map在内部使用红黑树作为底层实现。C++标准库中的map容器是基于红黑树实现的,红黑树是一种自平衡的二叉搜索树,能够保证查找、插入和删除操作的时间复杂度为O(log n)。而unordered_map则使用哈希表作为底层实现,是一种基于哈希表的数据结构,它能够以O(1)的时间复杂度进行查找、插入和删除操作,但是需要解决哈希冲突的问题。

总结:感觉红黑树是一个平衡的二叉树,不会出现某一端重的情况,如果出现了也会进行调整变成平衡的二叉树,它的查找、插入、删除操作的事件复杂度为O(logn)。

参考:https://zhuanlan.zhihu.com/p/91960960

  • 16
    点赞
  • 25
    收藏
    觉得还不错? 一键收藏
  • 3
    评论
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值