强查找数据结构(1)-- 红黑树

一、红黑树有什么用?

红黑树是一个二叉排序树,是一种很常用的查找组件,作为key - value数据结构,通过key查找value,典型的有C++ STL中的map和set;
查找过程运用了红黑树根据key值构成二叉排序树的特点,这种特点可以使按key查找结点具有很高的效率(近似于二分查找)。
(a)epoll中查找数据源访问的io,查找过程使用的数据结构也是红黑树;
(b) Linux 进程调度cfs
© Nginx timer定时器
强查找的数据结构有:红黑树、哈希表、B/B+树、跳表。

二、怎么样的树才是红黑树?

满足如下5个条件的是红黑树:
(1)树的每个结点是红的或者黑的;
(2)根结点是黑的;
(3)每个叶子结点是黑的(根叶黑);
(4)每个红结点的两个儿子节点都是黑的(红子黑);
(5)任一结点到其叶子结点的所有路径包含相同数量的黑结点(黑平衡);
注意:红黑树本身一定是二叉搜索树;红黑树的叶子结点通常是隐藏的,将它们指向同一个“空节点”nil。

三、红黑树的代码实现

(1)结点定义实现

typedef struct _rbtree_node {
	int key;
	void *value;         // value类型为void是为了便于接受各种类型的值
    //如果看到包含以下四个成员的结构体就可以判定为红黑树的结点结构
	rbtree_node *left;
	rbtree_node *right;
	rbtree_node *parent; //指向父节点
	
	unsigned char color; //颜色,红或黑,出于内存布局的考虑放在结构体最后一个成员

} rbtree_node;

(2)红黑树定义实现

typedef struct _rbtree {
	rbtree_node *root;
	rbtree_node *nil;  // 所有的叶子结点都指向该结点
}rbtree;

(3)提高红黑树结构体的复用性
如果需要用到多个红黑树,重复定义多个红黑树结构体会造成代码重复和冗余,冗余的原因在于红黑树结点成员中的功能代码(key、value成员)和特性代码(父子结点指针和颜色)没有分离,即使不同的红黑树使用的是相同的功能(都是根据key-value查找),也必须重复定义该结构体。因此可以采用宏定义方式增强红黑树结构体的复用性:

#define RBTREE_ENTRY(name, type) \
	struct name{                 \
		type *left;              \
		type *right;             \
		type *parent;            \
		unsigned char color;     \
	}
	
typedef struct _rb_node{
	int key;
	void *value;         // value类型为void是为了便于接受各种类型的值

	//示例: 复用特性代码,共用功能代码
	RBTREE_ENTRY(, rb_node) ready; 
	
	RBTREE_ENTRY(, rb_node) wait;
	
	RBTREE_ENTRY(, rb_node) sleep;

} rb_node;

四、红黑树的旋转

为什么需要对红黑树进行旋转?因为当红黑树的性质被破坏时,需要通过旋转将调整,使其恢复红黑树的特性。
旋转分为左旋和右旋。左旋和右旋作为红黑树的原语操作,当红黑树的性质被破坏时触发调整(疑问:如何检查红黑树的性质是否被破坏了呢?),左旋和右旋操作是可逆的。如下图所示:
在这里插入图片描述 红黑树的左旋和右旋,图中的a、b、c代表的是子树(而不是叶子结点),x和y结点可以是根节点。
对于任意一个红黑树,旋转时要以x结点为核心,找出其对应的y结点,然后找出对应的a、b、c子树,最后再依照图中规律进行旋转。
(1)左旋的代码实现
左旋过程需要改变6个指针的指向,注意改变指向的先后顺序,避免改变指针的指向后无法找到原结点。
左旋过程示意图:
在这里插入图片描述图1. 左旋改变的6根指针,绿色指针。
步骤1:先确定y结点,y = x->right;
步骤2:改变x与y的左节点的指针关系。将x的右指针指向y的左节点,x->right = y->left; 同时将y的左节点的父节点指针x,y->left->parent = x;
在这里插入图片描述
图2. 将x的右指针指向y的左节点
在这里插入图片描述
图3. 将y的左节点的父节点指向x

步骤3:改变y与父节点的指针。将x的父节点指向y,此处需要考虑x是否为根节点,
如果是根节点,T->root = y;
如果不是根节点,x可能是父节点的左子节点,x->parent->left = y; x可能是父节点的右子节点,x->parent->right = y;
同时,将y的父节点指向x的父节点, y->parent = x->parent;
在这里插入图片描述
图4. 将x的父节点指向y
在这里插入图片描述
图5. 将y的父节点指向x的父节点

步骤4:改变y与x直接的指针关系。将y的左指针指向x, y->left = x;将x的父指针指向y,x->parent = y。
完成左旋操作。
在这里插入图片描述
图6. 将y的左指针指向x
在这里插入图片描述
图7. 将x的父指针指向y

由此可得,左旋代码如下:

/* 参数1:T表示所指向的红黑树;
   参数2:x表示以x结点为核心进行旋转 */ 
void rbtree_left_rotate(rbtree *T, rbtree_node *x) {
	rbtree_node *y = x->right;
	
	x->right = y->left;
	if (y->left != T->nil) { 
		y->left->parent = x;
	}
	
	if(x == T->root) {
		T->root = y;
	} else if (x == x->parent->left) {
		x->parent->left = y;
	} else if ((x == x->parent->right) {
		x->parent->right = y;
	}
	y->parent = x->parent;
	
	y->left = x;
	x->parent = y;
}

(2)右旋的代码实现
将左旋代码的x和y互换,左右指针互换。

void rbtree_left_rotate(rbtree *T, rbtree_node *y) {
	rbtree_node *x = y->left;
	
	y->left= x->right;
	if (x->right != T->nil) { 
		x->right->parent = y;
	}
	
	if(y == T->root) {
		T->root = x;
	} else if (y == y->parent->right) {
		y->parent->right= x;
	} else if ((y == y->parent->left) {
		y->parent->left= x;
	}
	x->parent = y->parent;
	
	x->right= y;
	y->parent = x;
}

五、红黑树插入结点

红黑树插入结点需要考虑这些问题:(1)待插入的结点插在哪?(2)结点插入后需设置成什么颜色?(3)结点插入后如何判断是否依旧符合红黑树的性质,如果不符合如何进行调整?
关于第一个问题,由二叉搜索树的性质可以知道,新插入的结点总是在叶子结点位置;因此对插入点的搜索最后一定是指向叶子结点,找到该叶子结点后再判定是插在该叶子的左边还是右边;
关于第二个问题,需要利用红黑树的第5条性质:任一结点到其叶子结点的所有路径包含相同数量的黑结点。这意味插入新结点后红黑树的性质不能变,黑高不能变,因此将新插入的结点设置为红色可以不改变黑高。
关于第三个问题,后文分析。

#define RED 0;
#define BLACK 1;
void rbtree_insert(rbtree *T, rbtree_node *newNode) {
	rbtree_node *pSearch = T->root; // 搜索指针
	rbtree_node *pPre = T->nil;     // 保存搜索指针的前一个所指位置
	
	while(pSearch != T->nil) {
		pPre = pSearch;    // pSearch更新前存入pre
		if (newNode->key < pSearch->key) {
			pSearch = pSearch->left;
		} else if (newNode->key > pSearch->key) {
			pSearch = pSearch->right;
		} else { // 存在与待插入结点的键值相同的结点
			break;
			// 找到相同key的结点退出
		}
	}
	
	// 如果红黑树原本就是空树,不会进入while,pPre是T-nil;反之,也可以推导出 如果pPre是T-nil时,红黑树一定为空
	// 对于空树,直接将新节点作为根节点
	if (pPre == T->nil) {
		T->root = newNode;
	}
	
	// 找到相同key的结点退出,pSearch不为空
	if(pSearch != T->nil) {
		    //  找到相同key的结点处理方法视业务场景而定:
			//    可能会丢弃;
			//    可能会修改key对应的value
			//    ... ...
	}
	
	// 遍历红黑树没有找到key相同的结点,退出循环后,pSearch 会指向T->nil,需要pre保存pSearch所指的前一个位置
	if(pSearch == T->nil) {
		if(newNode->key < pPre->key) {
			pPre->left = newNode;
		}
		if(newNode->key < pPre->key) {
			pPre->right = newNode;
		}
	}
	
	// 处理插入结点的左、右、父指针和颜色
	newNode->parent = pPre;
	newNode->left = T->nil;
	newNode->right = T->nil;
	newNode->color = RED;
	
	// 插入结点后进行调整,后续补充
	// rb_tree_fixup()
}

新结点插入后可能会改变红黑树的性质,需要进行调整。调整时必须保持新插入结点的颜色始终是红色。在此前提下,有如下场景:
(1)当新插入结点的父结点是红色时,此时需要调整(不满足红黑树的第4条性质,红结点的子结点必须是黑结点)。
父结点是红色;
父节点的父节点(祖父结点)是黑色(如果祖父结点是红色,则父节点一定是黑色,不符合,所以祖父结点一定是黑色);
父节点的兄弟结点(叔父节点)颜色不确定,可红可黑;
(a)叔父结点是红色
(b)叔父结点是黑色
(2)当新插入结点的父结点是黑色

文章参考与<零声教育>的C/C++linux服务期高级架构线上课学习

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值