Linux内核中红黑树节点的插入原理分析

  红黑树是一种特化的AVL树(平衡二叉树),都是在进行插入和删除操作时通过特定操作保持二叉查找树的平衡,从而获得较高的查找性能。它虽然是复杂的,但它的最坏情况运行时间也是非常良好的,并且在实践中是高效的: 它可以在O(log n)时间内做查找,插入和删除,这里的n 是树中元素的数目。普通的二叉查找树在极端情况下可退化成链表,此时的增删查效率比较低。平衡的二叉树(如AVL、红黑树等)能较好的解决这个问题。

一、红黑树的性质

  1,每个结点是红的或黑的
  2,根结点是黑的
  3,每个叶子结点是黑的
  4,如果一个结点是红的,则它的两个儿子都是黑的
  5,对每个结点,从该结点到其子孙结点所有路径上的包含相同数目的黑结点

二、红黑树结构体的定义

  红黑树的定义在/include/linux/rbtree.h中实现:

struct rb_node {
	unsigned long  __rb_parent_color;
	struct rb_node *rb_right;
	struct rb_node *rb_left;
} __attribute__((aligned(sizeof(long))));
    /* The alignment might seem pointless, but allegedly CRIS needs it */

struct rb_root {
	struct rb_node *rb_node;
};

  成员rb_parent_color同时存储两种数据,一是其双亲结点的地址,另一是此结点的着色。attribute((aligned(sizeof(long))))属性保证了红黑树中的每个结点的首地址都是32位对齐的(在32位机上),也就是说每个结点首地址的bit[1]和bit[0]都是0,因此就可以使用bit[0]来存储结点的颜色属性而不干扰到其双亲结点首地址的存储。

三、红黑树节点的插入

  红黑树使用时的插入方法在Documentation/rbtree.txt文件内有定义:

  int my_insert(struct rb_root *root, struct mytype *data)
  {
  	struct rb_node **new = &(root->rb_node), *parent = NULL;

  	/* Figure out where to put new node */
  	while (*new) {
  		struct mytype *this = container_of(*new, struct mytype, node);
  		int result = strcmp(data->keystring, this->keystring);

		parent = *new;
  		if (result < 0)
  			new = &((*new)->rb_left);
  		else if (result > 0)
  			new = &((*new)->rb_right);
  		else
  			return FALSE;
  	}

  	/* Add new node and rebalance tree. */
  	rb_link_node(&data->node, parent, new);
  	rb_insert_color(&data->node, root);

	return TRUE;
  }

  这个函数主要有三个步骤:
  第一,搜索新节点要插入的位置。
  第二,初始化新节点。
  第三、将新节点插入到红黑树内。

2.1、搜索新节点要插入的位置

  搜索新节点要插入的位置,主要是比较新节点的数据和已知红黑树的节点大小,从而判断出位置。

2.2、初始化新节点

  初始化新节点调用的是函数:

static inline void rb_link_node(struct rb_node * node, struct rb_node * parent,
				struct rb_node ** rb_link)

  该函数在linux/include/linux/rbtree.h文件内定义

static inline void rb_link_node(struct rb_node * node, struct rb_node * parent,
				struct rb_node ** rb_link)
{
	node->__rb_parent_color = (unsigned long)parent;
	node->rb_left = node->rb_right = NULL;

	*rb_link = node;
}

  在linux/include/linux/rbtree_augmented.h文件内有如下定义

#define	RB_RED		0
#define	RB_BLACK	1

  所以当在一个红黑树插入一个节点时初始化此节点的颜色为红色。

2.3、将新节点插入到红黑树内

  rb_insert_color是供外部调用的插入函数,真正实现插入算法的函数是__rb_insert。该函数在文件linux/lib/rbtree.c文件内:

static __always_inline void
__rb_insert(struct rb_node *node, struct rb_root *root,
	    void (*augment_rotate)(struct rb_node *old, struct rb_node *new))
{
	struct rb_node *parent = rb_red_parent(node), *gparent, *tmp;

	while (true) {
		/*
		 * Loop invariant: node is red
		 *
		 * If there is a black parent, we are done.
		 * Otherwise, take some corrective action as we don't
		 * want a red root or two consecutive red nodes.
		 */
		 /* while 循环退出的条件:(1)寻找到根节点 (2)parent 为黑色 */
		//---------------------------------------------------------- 情况1 & 情况2
		if (!parent) {
			rb_set_parent_color(node, NULL, RB_BLACK);
			break;
		} else if (rb_is_black(parent))
			break;
		/* 到了这里,node 的父节点颜色一定是 Red */
		gparent = rb_red_parent(parent);

		tmp = gparent->rb_right;
		if (parent != tmp) {	/* parent == gparent->rb_left */
			if (tmp && rb_is_red(tmp)) {
				//-------------------------------------------------- 情况3
				/*
				 * Case 1 - color flips
				 *
				 *       G            g
				 *      / \          / \
				 *     p   u  -->   P   U
				 *    /            /
				 *   n            n
				 *
				 * However, since g's parent might be red, and
				 * 4) does not allow this, we need to recurse
				 * at g.
				 */
				rb_set_parent_color(tmp, gparent, RB_BLACK);
				rb_set_parent_color(parent, gparent, RB_BLACK);
				node = gparent;
				parent = rb_parent(node);
				rb_set_parent_color(node, parent, RB_RED);
				continue;
			}

			tmp = parent->rb_right;
			if (node == tmp) {
				//-------------------------------------------------- 情况4
				/*
				 * Case 2 - left rotate at parent
				 *
				 *      G             G
				 *     / \           / \
				 *    p   U  -->    n   U
				 *     \           /
				 *      n         p
				 *
				 * This still leaves us in violation of 4), the
				 * continuation into Case 3 will fix that.
				 */
				parent->rb_right = tmp = node->rb_left;
				node->rb_left = parent;
				if (tmp)
					rb_set_parent_color(tmp, parent,
							    RB_BLACK);
				rb_set_parent_color(parent, node, RB_RED);
				augment_rotate(parent, node);
				parent = node;
				tmp = node->rb_right;
			}
			//------------------------------------------------------ 情况5
			/*
			 * Case 3 - right rotate at gparent
			 *
			 *        G           P
			 *       / \         / \
			 *      p   U  -->  n   g
			 *     /                 \
			 *    n                   U
			 */
			gparent->rb_left = tmp;  /* == parent->rb_right */
			parent->rb_right = gparent;
			if (tmp)
				rb_set_parent_color(tmp, gparent, RB_BLACK);
			__rb_rotate_set_parents(gparent, parent, root, RB_RED);
			augment_rotate(gparent, parent);
			break;
		} else {
			tmp = gparent->rb_left;
			if (tmp && rb_is_red(tmp)) {
				/* Case 1 - color flips */
				rb_set_parent_color(tmp, gparent, RB_BLACK);
				rb_set_parent_color(parent, gparent, RB_BLACK);
				node = gparent;
				parent = rb_parent(node);
				rb_set_parent_color(node, parent, RB_RED);
				continue;
			}

			tmp = parent->rb_left;
			if (node == tmp) {
				/* Case 2 - right rotate at parent */
				parent->rb_left = tmp = node->rb_right;
				node->rb_right = parent;
				if (tmp)
					rb_set_parent_color(tmp, parent,
							    RB_BLACK);
				rb_set_parent_color(parent, node, RB_RED);
				augment_rotate(parent, node);
				parent = node;
				tmp = node->rb_left;
			}

			/* Case 3 - left rotate at gparent */
			gparent->rb_right = tmp;  /* == parent->rb_left */
			parent->rb_left = gparent;
			if (tmp)
				rb_set_parent_color(tmp, gparent, RB_BLACK);
			__rb_rotate_set_parents(gparent, parent, root, RB_RED);
			augment_rotate(gparent, parent);
			break;
		}
	}
}

  当在一个红黑树插入一个节点时初始化此节点的颜色为红色,但如果新节点父节点也为红色,将会违背红黑树的性质:一条路径上不能出现相邻的两个红色节点。这时就要根据具体的情况进行分析处理,根据穷举的方法,有下面五种情况,通过这五种情况的不同操作来使红黑树保持平衡。

2.3.1、情况1

  当前红黑树为空,即插入的节点是根节点。此时需要将节点的颜色由红色变为黑色以满足性质2。
Linux内核中红黑树节点的插入原理分析_第1张图片
  代码为:

		if (!parent) {
			rb_set_parent_color(node, NULL, RB_BLACK);
			break;
		}

  当寻找到根节点时退出while循环。

2.3.2、情况2

  插入节点N的父节点P为黑色,此时满足性质4和性质5,不需要调整。
Linux内核中红黑树节点的插入原理分析_第2张图片
  代码为:

else if (rb_is_black(parent))
			break;

2.3.3、情况3

  插入节点N的父节点P是红色,叔叔节点U也是红色,由性质4得P和U的父节点G为黑色。
  此时由于N和P均为红色,破坏了性质4,需要进行调整。这种情况下,先将P和U的颜色染成黑色,再将G的颜色染成红色。此时经过G路径上的黑色节点的数量不变,性质5仍然满足。但需要注意的是G染成红色后,可能和它的父节点形成连续的红色节点,此时需要递归向上调整。
Linux内核中红黑树节点的插入原理分析_第3张图片
  代码为

		tmp = gparent->rb_right;//祖父节点的右孩子
		if (parent != tmp) {	/* parent == gparent->rb_left *///父节点是祖父节点的左孩子
			if (tmp && rb_is_red(tmp)) {//叔节点不为空,且叔节点为红色(此时父节点为红色)
				/*
				 * Case 1 - color flips
				 *
				 *       G            g
				 *      / \          / \
				 *     p   u  -->   P   U
				 *    /            /
				 *   n            n
				 *
				 * However, since g's parent might be red, and
				 * 4) does not allow this, we need to recurse
				 * at g.
				 */
				rb_set_parent_color(tmp, gparent, RB_BLACK);//设置叔节点为黑色
				rb_set_parent_color(parent, gparent, RB_BLACK);//设置父节点为黑色
				node = gparent;
				parent = rb_parent(node);
				rb_set_parent_color(node, parent, RB_RED);//设置祖父节点为红色
				continue;//向上递归循环调整
			}

以上代码是新插入节点N在其祖父节点G的左子树的情况,右子树情况对称同理。

2.3.4、情况4

  插入节点N的父节点为红色,叔叔节点U为黑色。节点N是P的右孩子,且节点P是G的左孩子。
  此时先对节点P进行左旋,调整N与P的位置。接下来按照情况5来处理,以满足性质4。
Linux内核中红黑树节点的插入原理分析_第4张图片
  代码为

			tmp = parent->rb_right;//父节点的右孩子
			if (node == tmp) {//节点为父节点的右孩子
				/*
				 * Case 2 - left rotate at parent
				 *
				 *      G             G
				 *     / \           / \
				 *    p   U  -->    n   U
				 *     \           /
				 *      n         p
				 *
				 * This still leaves us in violation of 4), the
				 * continuation into Case 3 will fix that.
				 */
				parent->rb_right = tmp = node->rb_left;//将节点的左孩子,赋值给temp和父节点的右孩子(之前父节点的右孩子为该节点)
				node->rb_left = parent;//将父节点赋值为节点的左孩子
				if (tmp)//如果赋值前该节点存在左孩子
					rb_set_parent_color(tmp, parent,
							    RB_BLACK);//将运算前节点左孩子(也是运算后父节点的右孩子变为黑色),
				rb_set_parent_color(parent, node, RB_RED);//设置父节点为红色
				augment_rotate(parent, node);
				parent = node;//将父节点赋值为该节点
				tmp = node->rb_right;
			}

2.3.5、情况5

  某次调整后,子树中节点N的父节点为红色,叔叔节点U为黑色。节点N是P的左孩子,且节点P是G的左孩子。
  此时对G进行右旋,调整P和G的位置,并交换颜色。使得性质4被满足。
Linux内核中红黑树节点的插入原理分析_第5张图片
  代码为

			/*
			 * Case 3 - right rotate at gparent
			 *
			 *        G           P
			 *       / \         / \
			 *      p   U  -->  n   g
			 *     /                 \
			 *    n                   U
			 */
			gparent->rb_left = tmp;  /* == parent->rb_right */
			parent->rb_right = gparent;
			if (tmp)
				rb_set_parent_color(tmp, gparent, RB_BLACK);
			__rb_rotate_set_parents(gparent, parent, root, RB_RED);
			augment_rotate(gparent, parent);
			break;

四、红黑树节点插入的实例流程分析

  通过插入12 1 9 2 0 完成红黑树流程分析。

4.1、添加12

Linux内核中红黑树节点的插入原理分析_第6张图片
  说明:符合2.3章节的情况一,添加的节点若是根节点,则直接将其设置为黑色。

4.2、添加1

  搜索新节点1要插入的位置为根节点的左子叶,且初始化为红色,则直接添加。
Linux内核中红黑树节点的插入原理分析_第7张图片
  说明:符合2.3章节的情况二,插入节点N的父节点P为黑色,不需要调整。

4.3、添加9

  搜索新节点9要插入的位置为节点1右子叶。
Linux内核中红黑树节点的插入原理分析_第8张图片

  调整红黑树步骤:
  第一步、符合2.3章节的情况四,插入节点9的父节点1为红色,叔叔节点U为黑色。节点9是1的右孩子,且节点1是12的左孩子。此时先对节点1进行左旋,调整1与9的位置。
  第二步、符合2.3章节的情况五,子树中节点1的父节点9为红色,叔叔节点U为黑色。节点1是9的左孩子,且节点9是节点12的左孩子。
此时对节点12进行右旋,调整节点9和节点12的位置,并交换颜色。

4.4、添加2

搜索新节点2要插入的位置为节点1右子叶。
  Linux内核中红黑树节点的插入原理分析_第9张图片
  调整红黑树步骤:
  第一步、符合2.3章节的情况三,插入节点2的父节点1是红色,叔叔节点12也是红色,此时由于节点2和节点1均为红色,破坏了性质4,需要进行调整。这种情况下,先将节点1和节点12的颜色染成黑色,再将节点9的颜色染成红色。此时经过节点9路径上的黑色节点的数量不变,性质5仍然满足。
  第二步、符合2.3章节的情况一,节点9为根节点,将节点9设为黑色。

4.5、添加0

  搜索新节点0要插入的位置为节点1左子叶。
Linux内核中红黑树节点的插入原理分析_第10张图片
  此时,不需要调整,已经是一课红黑树。
  以此类推,所有红黑树节点的添加,皆遵循以上流程。

你可能感兴趣的:(数据结构,linux,b树,数据结构)