论坛首页 综合技术论坛

Linux2.6.36内核红黑树源码注释

浏览 5708 次
精华帖 (0) :: 良好帖 (0) :: 新手帖 (0) :: 隐藏帖 (0)
作者 正文
   发表时间:2011-12-27  

红黑树的原理理解起来并不困难,无非是左右旋转,涂色。然而要编码出一个高效简洁的实现代码却挺复杂。近日学习Linux源码,对其红黑树实现作了注释,与大家分享一下。我贴的代码是我边理解边“抄写”出来的,和源始代码可能会有少许出入,但不影响理解。本文着重注释插入删除操作,其它部分不作重点。若要了解红黑树的基础知识或是Linux红黑树的使用,请另行参考其它资料。

 

先看一下节点定义:

 

typedef struct st_rb_node {
	/**
	 * 本结构体四字节对齐,因而其地址中低位两个bit永远是0。
	 * Linux内核开发人员非常爱惜内存,他们用其中一个空闲的位来存储颜色信息。
	 * parent_color成员实际上包含了父节点指针和自己的颜色信息。
	 */
	unsigned long parent_color;
#define RB_RED   0
#define RB_BLACK 1
	struct st_rb_node *left, *right;
}__attribute__((aligned(sizeof(long)))) rb_node;

 

Linux的数据结构有个特点就是结构不包括数据,而是由数据来包括结构信息。由结构体获取数据信息是通过 CONTAINER_OF 这个宏来实现的,它利用了一些编译器的特性,有兴趣的可以参考Linux的链表源码。

 

头文件中其它主要内容如下(比较好理解,未作注释):

 

#define rb_parent(r)    ((rb_node *)((r)->parent_color & ~3))
#define rb_color(r)     ((r)->parent_color & 1)
#define rb_is_red(r)    (!rb_color(r))
#define rb_is_black(r)  rb_color(r)
#define rb_set_red(r)   do { (r)->parent_color &= ~1; } while (0)
#define rb_set_black(r) do { (r)->parent_color |= 1; } while (0)

static inline void rb_set_parent(rb_node *node, rb_node *p) {
	node->parent_color = (node->parent_color & 3) | (unsigned long) p;
}

static inline void rb_set_color(rb_node *node, int color) {
	node->parent_color = (node->parent_color & ~1) | color;
}

#define RB_ROOT (rb_root) { NULL, }
#define rb_entry(ptr, type, member) container_of(ptr, type, member)

#define RB_EMPTY_ROOT(root) ((root)->node == NULL)
#define RB_EMPTY_NODE(node) (rb_parent(node) == node)
#define RB_CLEAR_NODE(node) (rb_set_parent(node, node))

void rb_insert_color(rb_node *node, rb_root *root);
void rb_erase(rb_node *node, rb_root *root);

rb_node * rb_next(const rb_node *node);
rb_node * rb_prev(const rb_node *node);
rb_node * rb_first(const rb_root *root);
rb_node * rb_last(const rb_root *root);

void rb_replace_node(rb_node *victim, rb_node *new, rb_root *root);

static inline void rb_link_node(rb_node *node, rb_node *p, rb_node **link) {
	node->parent_color = (unsigned long) p;
	node->left = node->right = NULL;
	*link = node;
}
 

两个基本操作:左旋与右旋(未注释,仅为方便参考)

 

/**
 * 对node进行左旋。有关概念可参考WIKI等资源。
 */
static void _rb_rotate_left(rb_node *node, rb_root *root) {
	rb_node *right = node->right, *parent = rb_parent(node);

	if ((node->right = right->left))
		rb_set_parent(node->right, node);

	right->left = node;
	rb_set_parent(node, right);

	if (parent) {
		if (node == parent->left)
			parent->left = right;
		else
			parent->right = right;
	} else {
		root->node = right;
	}
	rb_set_parent(right, parent);
}

/**
 * 对node进行右旋。有关概念可参考WIKI等资源。
 */
static void _rb_rotate_right(rb_node *node, rb_root *root) {
	rb_node *left = node->left, *parent = rb_parent(node);

	if ((node->left = left->right))
		rb_set_parent(node->left, node);

	left->right = node;
	rb_set_parent(node, left);

	if (parent) {
		if (node == parent->left)
			parent->left = left;
		else
			parent->right = left;
	} else {
		root->node = left;
	}
	rb_set_parent(left, parent);
}

 

插入操作的实现(前提是节点已经插入在目标位置,下面的函数只负责修正树的性质):

 

/**
 * node是新插入的结点,有着默认的红色。
 * 本函数检查是否有违背红黑树性质的地方,并进行纠正。
 */
void rb_insert_color(rb_node *node, rb_root *root) {
	rb_node *parent, *gp;

	/**
	 * 如果有父节点且父节点是红色,进行树的调整以保证树的性质。
	 */
	while ((parent = rb_parent(node)) && rb_is_red(parent)) {
		gp = rb_parent(parent);

		if (parent == gp->left) {
			/**
			 * 父节点是祖父节点左子的情况。
			 * "else"中的情况左右相反,这里只注释"if"里的代码。
			 */
			register rb_node *uncle = gp->right;
			if (uncle && rb_is_red(uncle)) {
				/**
				 * 如果有红叔,则将红叔与红父均涂黑,并将祖父节点涂红。
				 */
				rb_set_black(parent);
				rb_set_black(uncle);
				rb_set_red(gp);
				/**
				 * 现在简单路径中黑节点个数仍然平衡,但祖父变成了红色,
				 * 我们不确定有没有造成父子均红的情况,所以需要对祖父节点进行下一轮修复。
				 */
				node = gp;
				continue;
			}

			/**
			 * 现在是叔节点为空或为黑的情况。
			 */
			if (node == parent->right) {
				/**
				 * 如果新节点是父节点的右子,对父节点进行左旋。
				 * 旋转后树仍然平衡,但新节点占了原父节点的位子。
				 * 这两个节点交换角色后,新的父节点是红的,其左子也是红的。
				 */
				_rb_rotate_left(parent, root);
				register rb_node *tmp = node;
				node = parent;
				parent = tmp;
			}
			/**
			 * 此时父红左子红,树平衡但有连续红节点。
			 *
			 * 父涂黑,祖父涂红,再对祖父右旋,树即调整到合法状态。
			 */
			rb_set_black(parent);
			rb_set_red(gp);
			_rb_rotate_right(gp, root);
			return;
		} else {
			register rb_node *uncle = gp->left;
			if (uncle && rb_is_red(uncle)) {
				rb_set_black(parent);
				rb_set_black(uncle);
				rb_set_red(gp);
				node = gp;
				continue;
			}

			if (node == parent->left) {
				_rb_rotate_right(parent, root);
				register rb_node *tmp = node;
				node = parent;
				parent = tmp;
			}
			rb_set_black(parent);
			rb_set_red(gp);
			_rb_rotate_left(gp, root);
			return;
		}
	}
	/**
	 * 若无父节点,只需将node涂黑;
	 * 若父节点为黑,插入红节点不影响树的性质。
	 * 循环体后直接将node涂黑,可以同时保证以上两点。
	 */
	 rb_set_black(root->node);
}

 

最复杂的是删除操作,分为两部分。

删除第一步,移出节点:

 

/**
 * 删除node节点(其实只是将其与树脱离),并修正树。
 * 红黑树的节点删除相对比插入更复杂,加之Linux的源码风格也不易懂,
 * 此段代码需认真体会,可以多画画图,对各种情况标记、对照一下。
 */
void rb_erase(rb_node *node, rb_root *root) {
	rb_node *child, *parent;
	int color;
	
	/**
	 * 这里其实分成了两个大分枝。
	 * 如果node有任一空子,则记录child并跳至条件语句后的语句,具体位置参考注释;
	 * 如果两子均不为空,则进入"else"分枝。
	 */
	if (!node->left) {
		child = node->right;
	} else if (!node->right) {
		child = node->left;
	} else {
		/**
		 * 乾坤大挪移,将要删除的节点转换成只有一个子节点或无子节点的节点。
		 * 具体方法是,找到它的下一个节点,也即先找其右节点,再循环找左节点,
		 * 直到没有左子节点。找到的节点一定是比node"大"且紧邻node的节点。
		 * 将此找到的节点放到node位置,并保持node的原有颜色,树的性质并不会受影响。
		 * 此时问题转化成了删除找到的没有左子的节点。
		 * (提示:也可以找左子的右节点,循环下去,得到的将是“紧邻”的小于node的节点)
		 */
		rb_node *old = node, *left;
		node = node->right;
		while ((left = node->left) != NULL)
			node = left;
		
		/**
		 * 找到节点后,将其“大挪移”过去。
		 * 首先用old的父亲或root指向新找到的节点node.
		 */
		if (rb_parent(old)) {
			if (rb_parent(old)->left == old)
				rb_parent(old)->left = node;
			else
				rb_parent(old)->right = node;
		} else {
			root->node = node;
		}
		
		/**
		 * 记录找到的node节点相关信息。对于这种删除操作,node节点移至old位置后会沿用old的颜色属性,
		 * 树的性质不受影响,但原node位置将失去一个节点,我们要记录节点的相关信息以便做节点的删除工作。
		 */
		child = node->right;
		parent = rb_parent(node);
		color = rb_color(node);
		
		if (parent == old) {
			/**
			 * 参考前面while循环,如果parent==old,则找到的单子节点只能是old节点的右子。
			 * 这里保存的parent可以理 解为将用作node原来子节点的亲父节点。对于old是node右子的情况,
			 * node取代old后,原node的父节点还是node.
			 */
			parent = node;
		} else {
			/**
			 * node节点的删除处理。主要是将相应的父、子联结起来。
			 * 注意我们的“乾坤大挪移”可能会比较绕,删的数据是old节点,但我们将node转移过去占了old的位置,
			 * 所以对于节点结构来说,我们删除的是node原来的位置。
			 */
			if (child)
				rb_set_parent(child, parent);
			parent->left = child;
			
			node->right = old->right;
			rb_set_parent(old->right, node);
		}
		
		/**
		 * node移过去后继续使用old节点的颜色和父节点属性。
		 * parent_color同时保存了父节点信息和自己的颜色信息。
		 * 关于parent_color请参考头文件中的宏。
		 */
		node->parent_color = old->parent_color;
		/**
		 * 之前已将old的parent的指向old子节点的指针指向了node,
		 * 并且node也已经连接了old的右子(如果node与old是子与父的关系,则node保留原右子即可)
		 * 上面一句完成了node指向新父亲即设置颜色的操作,现在只剩下左子了,把它搞定!
		 */
		node->left = old->left;
		rb_set_parent(old->left, node);
		
		/**
		 * 该删的该改的都已搞定,现在要检查颜色了。
		 */
		goto color;
	}
	
	/**
	 * 这里的条件语句和goto语句实在是让人晕头转向。现在对应的是本函数最上面的if语句中前两个case。
	 *
	 * 如果node有任一空子,也即node无子或有单子。这里跳过了前面的“乾坤大挪移”,同时相比挪移的情况,
	 * 这里的区别是node可能是右子,也可能是左子。但这里的删除操作要简单些。
	 *
	 * 思考:挪移与不挪移的情况,最终都是把问题转到了一个无子或只有单子的节点上,
	 * 为什么不能让两种情况执行一段共同的删除代码呢?
	 * 我想应该是挪移本身已经使node从原位置消失了,两者删除操作有差别,加之Linux内核对速度的狂热,
	 * 就分情况写成了现在这种很绕的代码。
	 */
	parent = rb_parent(node);
	color = rb_color(node);
	
	if (child)
		rb_set_parent(child, parent);
	if (parent) {
		if (parent->left = node)
			parent->left = child;
		else
			parent->right = child;
	} else {
		root->node = child;
	}

color:
	/**
	 * color是删除节点的颜色,删除的节点的层次正好在child参数与parent参数中间。
	 * 如果删除的是红节点,对树没有影响,所以只有删除的是黑节点时才修正颜色。
	 */
	if (color == RB_BLACK)
		_rb_erase_color(child, parent, root);
}

 

第二步,简单移出如果破坏了树的性质,则需要修复:

 

/**
 * 前置环境:在node与parent之前,刚刚删除了一个黑色节点。现在树很可能不平衡,
 * node与parent也可能红色冲突。
 * 本函数进行树的性质的修正,以使树恢复平衡。在一些情况下问题会转移到上一层节点,
 * 则须对上一层节点进行递归检查与修正。本函数中的while循环实际上实现了这种递归。
 *
 * 提示:这是红黑树里最绕的地方,看不明白可以多画画图。
 */
static void _rb_erase_color(rb_node *node, rb_node *parent, rb_root *root) {
	/**
	 * other用来保存兄弟节点,这是树的修正过程中一个重要的参考节点。
	 */
	rb_node *other;
	/**
	 * 循环条件:node不是红节点且node不是根节点。
	 * 解释:对于红节点或根节点,直接涂黑即可解决问题。
	 */
	while ((!node || rb_is_black(node)) && node != root->node) {
		/**
		 * 为方便程序处理各种情况,这里和节点插入一样将问题分成了左右对称的两类。
		 * if-else两个分枝里代码逻辑完全相同,只是左右相反。所以我们只研究node是parent左子的情况。
		 *
		 * 在开始之前,我们先总结一下当前状态:
		 * 1:因为删除的是黑色节点,所以node与parent都有可能是红色节点。
		 * 2:node与parent之间少了一个黑色节点,则所有通过node的路径都少了一个黑色节点,不妨画图时用-1标出来;
		 * 但node的兄弟节点(node一定有兄弟,可以根据删除前树的平衡性质来反推)高度并未变化,可以记作0。
		 *
		 * 提示:在进行旋转、涂色等操作时,可以画图观查平衡状态的变化。
		 */
		if (parent->left == node) {
			other = parent->right;
			if (rb_is_red(other)) {
				/**
				 * 如果兄弟节点是红色,则父节点是黑色,交换父、兄节点的颜色,并对父节点进行左旋。
				 * 旋转后,兄节点占了老的父节点位置,且和老的父节点颜色相同,所以不会向上造成颜色冲突。
				 * 我们仍然以老的父节点为父节点来看,现在的状态是:
				 * 父节点右子保持平衡,只有经过node的路径少了一个黑色节点。
				 * 现在问题和之前相似,但node有了一个黑色的兄弟(还有一个红色父亲)。
				 */
				rb_set_black(other);
				rb_set_red(parent);
				_rb_rotate_left(parent, root);
				/**
				 * other指向新的兄弟节点。other现在必然是一个黑色节点,而不会是空。这一点可以根据旋转之前树的性质反证。
				 */
				other = parent->right;
			}
			/**
			 * 此时状态:
			 * node有黑色兄弟,父亲可能黑也可能红。
			 */
			if ((!other->left || rb_is_black(other->left)) &&
				(!other->right || rb_is_black(other->right))) {
				/**
				 * 如果other没有红色子节点,那我们就可以把other涂红,并向上转移问题。
				 * other涂红的后果是,other分枝少了一个黑节点,与node分枝保持了平衡,但parent整体少了一个黑色节点。
				 * 细心的人可能会发现,如果父亲是红色的,父亲与兄弟有颜色冲突,直接向上转移能纠正吗?当然是可以的。
				 * 向上一层之后,while里的黑节点判断失败,会直接执行while后面的语句,直接将parent涂黑,则树恢复平衡。
				 */
				rb_set_red(other);
				node = parent;
				parent = rb_parent(node);
			} else {
				/**
				 * 现在黑兄弟有红子节点,父亲颜色未知。
				 */
				if (!other->right || rb_is_black(other->right)) {
					/**
					 * 如果黑兄弟右节点为空或为黑,则左节点一定是红的,我们想办法把它调整为右子为红。
					 * 至于为什么,看后面就知了。
					 * other->left与other交换颜色,对other进行右旋,other指向新的兄弟。根据右旋转的特点,
					 * 可知现在other仍然是黑色,且它有了一个红色右子。同时other分枝高度不变,颜色也没有冲突。
					 */
					rb_set_black(other->left);
					rb_set_red(other);
					_rb_rotate_right(other, root);
					other = parent->right;
				}
				/**
				 * 此时状态:黑兄弟有红色右子节点。
				 * 不管parent是什么颜色,把other涂成父亲的颜色(之后旋转,other占据父亲的位置,向上没有颜色冲突),
				 * 把父亲涂黑,把黑兄的other涂黑,这时node分枝高度可能有变化也可能没变化,other分枝多了一个黑节点。
				 * 现在对父亲进行左旋转。旋转后的情况是右边分枝(原other右子)少了一个黑节点,重归平衡;
				 * 左边分枝则增加了一个黑节点,也恢复了平衡。此时也没有颜色冲突
				 */
				rb_set_color(other, rb_color(parent));
				rb_set_black(parent);
				rb_set_black(other->right);
				_rb_rotate_left(parent, root);
				/**
				 * 树已平衡,node置为根节点,并跳出循环。
				 * 疑问:这里为什么还要检查根节点呢?
				 */
				node = root->node;
				break;
			}
		} else {
			other = parent->left;
			if (rb_is_red(other)) {
				rb_set_black(other);
				rb_set_red(parent);
				_rb_rotate_right(parent, root);
				other = parent->left;
			}
			if ((!other->left || rb_is_black(other->left)) &&
				(!other->right || rb_is_black(other->right))) {
				rb_set_red(other);
				node = parent;
				parent = rb_parent(node);
			} else {
				if (!other->left || rb_is_black(other->left)) {
					rb_set_black(other->right);
					rb_set_red(other);
					_rb_rotate_left(other, root);
					other = parent->left;
				}
				rb_set_color(other, rb_color(parent));
				rb_set_black(parent);
				rb_set_black(other->left);
				_rb_rotate_right(parent, root);
				node = root->node;
				break;
			}
		}
	}
	/**
	 * 对于红节点或根节点,直接涂黑即可解决问题。
	 */
	if (node)
		rb_set_black(node);
}

 

匆忙之中难免有疏漏,欢迎指正。

   发表时间:2012-01-06  
楼主辛苦了。估计这里好多人都看不懂。。
0 请登录后投票
   发表时间:2012-01-06  
这个有限时间内必须要看懂 虽然偶看了好久没弄懂
0 请登录后投票
   发表时间:2012-01-07  
不懂。。。。没基础的人路过。。。
0 请登录后投票
   发表时间:2012-01-09  
曾经学过。
0 请登录后投票
   发表时间:2012-01-10  
337240552 写道
这个有限时间内必须要看懂 虽然偶看了好久没弄懂

我也是绕了很久。每个变换场景都不难理解,但要自己做,把所有情况考虑好,代码又要写得精简的话,真的难。即使今天看懂了,明天还会忘,毕竟对于多数人来说几年都用不着一次。希望这份注解对我、对大家都能有帮助。
0 请登录后投票
   发表时间:2012-02-28  
我有Java版的,而且在项目中用过红黑树,要的话可以@我
0 请登录后投票
论坛首页 综合技术版

跳转论坛:
Global site tag (gtag.js) - Google Analytics