TreeMap关键源码解析-红黑树操作

Posted 秋风兮月

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了TreeMap关键源码解析-红黑树操作相关的知识,希望对你有一定的参考价值。

这篇博文的定位是把一些TreeMap的关键操作做个解析,而不是把所有红黑树以及TreeMap的源码全部解释一遍。所以建议在看之前,首先可以参考下面三篇博客,这篇博文中的一些配图也借鉴了其中的配图。

史上最清晰的红黑树讲解(上)

史上最清晰的红黑树讲解(下)

Java提高篇(二七)—–TreeMap

总体介绍

我们知道,TreeMap的底层是通过红黑树(Red-Black Tree)实现。要想明白TreeMap一些代码操作具体的含义需要理解红黑树的基本性质,在插入或者删除红黑树节点时会碰到的具体情况,以及在每一种情况下如何对红黑树进行操作。只有理解了这些操作步骤,那么代码也就会比较容易理解了。

红黑树是一种近似平衡的二叉查找树,它能够确保任何一个节点的左右子树的高度差不会超过二者中较低那个的一倍。具体来说,红黑树是满足如下条件的二叉查找树(binary search tree):

  1. 每个节点要么是红色,要么是黑色。
  2. 根节点必须是黑色
  3. 红色节点不能连续(也即是,红色节点的孩子和父亲都不能是红色)。
  4. 对于每个节点,从该点至null(树尾端)的任何路径,都含有相同个数的黑色节点。
在树的结构发生改变时(插入或者删除操作),往往会破坏上述条件3或条件4,需要通过调整使得查找树重新满足红黑树的条件。

基本操作封装

对于红黑树的基本操作,主要是两类,一类是颜色变换,一类是旋转操作:左旋和右旋。

颜色变换

在TreeMap中变换颜色的函数是setColor。这个函数很简单:

private static <K,V> void setColor(Entry<K,V> p, boolean c) 
	if (p != null)
	p.color = c;

旋转操作

旋转操作包括左旋转和右旋转。下面分别介绍。

1.左旋


左旋操作的过程就是将h节点的右子树x以h为中心进行逆时针旋转,使得右子树节点x反倒变成了h的父节点。同时修改相关引用,将原来x节点的左子树变为h的右子树。

在TreeMap中左旋操作对应的函数是rotateLeft,其具体实现如下:

/** From CLR */
private void rotateLeft(Entry<K,V> p) 
	if (p != null) 
		Entry<K,V> r = p.right;
		p.right = r.left;
		if (r.left != null)
			r.left.parent = p;
		r.parent = p.parent;
		if (p.parent == null)
			root = r;
		else if (p.parent.left == p)
			p.parent.left = r;
		else
			p.parent.right = r;
		r.left = p;
		p.parent = r;
	

对照上面的示意图,这个函数就很容易理解了。

2.右旋


右旋操作的过程就是将h的左子树x以h为中心顺时针旋转,使得左子树节点x反倒成了h的父节点。同时修改相关引用,将原来x节点的右子树变成h的左子树。

在TreeMap中左旋操作对应的函数是rotateRight,其具体实现如下:

/** From CLR */
private void rotateRight(Entry<K,V> p) 
	if (p != null) 
		Entry<K,V> l = p.left;
		p.left = l.right;
		if (l.right != null) l.right.parent = p;
		l.parent = p.parent;
		if (p.parent == null)
			root = l;
		else if (p.parent.right == p)
			p.parent.right = l;
		else p.parent.left = l;
		l.right = p;
		p.parent = l;
	
对照上面的示意图,这个函数也就很容易理解了。

在理解了这些基本操作后,下面就可以深入去理解红黑树插入和删除的关键操作了。

插入操作关键步骤解析

在TreeMap中插入操作对应的函数是put,这个函数大致的实现过程是根据指定的key对红黑树进行查找,如果找到,那么就去直接更新对应的value,然后返回。如果没有找到,那么就将新创建的Entry节点对象插入到红黑树上,当然插入的节点一定是树的叶子节点,并且新插入的节点一定是红色节点。那么在插入新节点后,就有可能破坏红黑树的性质,这样就需要对插入新节点的红黑树进行调整,所以这就是TreeMap中的fixAfterInsertion函数的作用。当新插入节点的父节点颜色是BLACK,则不需要调整。需要调整总共包括六种情况:

其中有三种情况是插入的节点x的父节点p,p为其父节点pp的左孩子,对应下面的1,2,3三种情况。另外三种是p为其父节点pp的右孩子,对应下面的4,5,6三种情况。其实与1,2,3是对称的。

1.插入的节点x和其父节点p都是RED,且p为其父节点pp的左孩子,且p的兄弟节点(即x的叔父节点)y为RED。

这种情况下,进行的操作是调整p和y节点颜色为BLACK,调整pp节点颜色为RED


在fixAfterInsertion函数所对应的代码片段如下:

if (parentOf(x) == leftOf(parentOf(parentOf(x)))) 
	Entry<K,V> y = rightOf(parentOf(parentOf(x)));
	if (colorOf(y) == RED) 
		setColor(parentOf(x), BLACK);
		setColor(y, BLACK);
		setColor(parentOf(parentOf(x)), RED);
		x = parentOf(parentOf(x));
	 
	...

2.节点x和其父节点p都是RED,且p为其父节点pp的左孩子,节点x是其父节点p的右孩子,且其叔父节点y颜色是BLACK

这种情况下需要进行的操作是将x设置成x的父节点p,然后对x进行左旋操作。这样就会进入到情况3,然后按照情况3进行处理


3.节点x和其父节点p都是RED,且p为其父节点pp的左孩子,节点x是其父节点p的 左孩子,且其叔父节点y颜色是BLACK

这种情况下需要进行的操作是将x的父节点p设为BLACK,p的父节点pp设置为RED,然后对pp进行右旋操作

(注意这里画的几个图只是一个片段,不是完整的红黑树,不然红黑树的性质四都不满足了)

对于2,3情况在fixAfterInsertion函数所对应的代码片段如下:

...
else 
	if (x == rightOf(parentOf(x))) 
		x = parentOf(x);
		rotateLeft(x);
	
	setColor(parentOf(x), BLACK);
	setColor(parentOf(parentOf(x)), RED);
	rotateRight(parentOf(parentOf(x)));
4.是与 情况1是对称的,即插入的节点x和其父节点p都是RED,且p为其父节点pp的右孩子,且p的兄弟节点(即x的叔父节点)y为RED。

这种情况下,进行的操作是调整p和y节点颜色为BLACK,调整pp节点颜色为RED

在fixAfterInsertion函数所对应的代码片段如下:

Entry<K,V> y = leftOf(parentOf(parentOf(x)));
if (colorOf(y) == RED) 
	setColor(parentOf(x), BLACK);
	setColor(y, BLACK);
	setColor(parentOf(parentOf(x)), RED);
	x = parentOf(parentOf(x));

...

5.是与情况2是对称的,即节点x和其父节点p都是RED,且p为其父节点pp的右孩子,节点x是其父节点p的左孩子,且其叔父节点y颜色是BLACK

这种情况下需要进行的操作是将x设置成x的父节点p,然后对x进行右旋操作。这样就会进入到情况6,然后按照情况6进行处理


6.是与情况3对称的,即节点x和其父节点p都是RED,且p为其父节点pp的右孩子,节点x是其父节点p的右孩子,且其叔父节点y颜色是BLACK

这种情况下需要进行的操作是将x的父节点p设为BLACK,p的父节点pp设置为RED,然后对pp进行左旋操作

(注意这里画的几个图只是一个片段,不是完整的红黑树,不然红黑树的性质四都不满足了)

对于5,6情况在fixAfterInsertion函数所对应的代码片段如下:

...
else 
	if (x == leftOf(parentOf(x))) 
		x = parentOf(x);
		rotateRight(x);
	
	setColor(parentOf(x), BLACK);
	setColor(parentOf(parentOf(x)), RED);
	rotateLeft(parentOf(parentOf(x)));
然后对整个红黑树回溯向上进行调整,最终重新设置红黑树的root为BLACK,这时候调整完毕。

最终对于红黑树的插入操作就算成功了。

删除操作关键步骤解析

对于删除操作的总体操作步骤是首先判断待删除的节点是否左右子树都不为空,如果都不为空,那么首先找到待删除节点的后继节点(这样找到的后继节点肯定在该节点的右子树里,且后继节点只有左子树或者这有右子树或者左右子树都没有,至于为什么见后继节点的讲解),然后使用后继节点将待删除的节点替换掉,然后去删除后继节点;然后就可以针对只有单个子树或者叶子节点情况进行删除操作(因为这样的情况容易去做删除操作,左右子树都不为空的话,不容易做,所以需要去找后继节点进行替换,这种情况下的后继节点肯定满足只有一个子树或者没有子树的条件),然后对红黑树进行调整。 后继节点的定义是:树中大于当前待删除节点value值的最小的那个节点。

查找后继节点

对于一棵二叉查找树,给定节点t,其后继(树种比大于t的最小的那个元素)可以通过如下方式找到:
  1. t的右子树不空,则t的后继是其右子树中最小的那个元素。
  2. t的右孩子为空,则t的后继是其第一个向左走的祖先。

在TreeMap中对应的查找后继节点的函数是successor函数。

/**
 * Returns the successor of the specified Entry, or null if no such.
 */
static <K,V> TreeMap.Entry<K,V> successor(Entry<K,V> t) 
	if (t == null)
		return null;
	//右子树不为空,则后继节点存在于右子树中最小的那个元素。即右子树的左子树
	else if (t.right != null) 
		Entry<K,V> p = t.right;
		while (p.left != null)
			p = p.left;
		return p;
	 else 
		//右子树为空,则回溯去寻找第一个祖先节点,该祖先节点的左子树中包含节点t
		Entry<K,V> p = t.parent;
		Entry<K,V> ch = t;
		while (p != null && ch == p.right) 
			ch = p;
			p = p.parent;
		
		return p;
	

删除操作

删除操作对应的函数是remove函数,remove函数是找到相应的Entry,然后调用deleteEntry进行红黑树的删除操作。具体的deleteEntry代码如下:

/**
 * Delete node p, and then rebalance the tree.
 */
private void deleteEntry(Entry<K,V> p) 
	modCount++;
	size--;

	// If strictly internal, copy successor's element to p and then make p
	// point to successor.
	if (p.left != null && p.right != null) 
		//当待删除节点的左右子树都不为空时,去查找后继节点
		Entry<K,V> s = successor (p);
		//把当前节点替换为后继节点
		p.key = s.key;
		p.value = s.value;
		//实际上后面做删除操作的节点是在其后继节点的位置
		p = s;
	 // p has 2 children
	
	// Start fixup at replacement node, if it exists.
	Entry<K,V> replacement = (p.left != null ? p.left : p.right);
	//现在的情况是只有一个子树
	if (replacement != null) 
		//将待删除节点的子树挂在待删除节点的父节点上
		// Link replacement to parent
		replacement.parent = p.parent;
		if (p.parent == null)
			root = replacement;
		else if (p == p.parent.left)
			p.parent.left  = replacement;
		else
			p.parent.right = replacement;

		// Null out links so they are OK to use by fixAfterDeletion.
		p.left = p.right = p.parent = null;

		// Fix replacement 
		//p如果是红节点的话不需要调整,因为红节点的子节点肯定是黑色的,黑色的不会影响红黑树的结构
		//而如果p是黑色的话,那么子节点就可能是红色的,而p的父节点也可能是红色的
		//这样将p的子树挂在p的父节点上,就可能会出现两个红色节点相连的情况,因此需要调整
		if (p.color == BLACK)
			fixAfterDeletion(replacement);
	//根节点的情况
	 else if (p.parent == null)  // return if we are the only node. 
		root = null;
	//无子树的情况
	 else  //  No children. Use self as phantom replacement and unlink. 
		//如果p是黑色的删除的话,那么就可能不满足
		//对于每个节点,从该点至null(树尾端)的任何路径,都含有相同个数的黑色节点
		//这样的条件了。所以需要调整,红色的删除没有影响
		if (p.color == BLACK)
			fixAfterDeletion(p);

		if (p.parent != null) 
			if (p == p.parent.left)
				p.parent.left = null;
			else if (p == p.parent.right)
				p.parent.right = null;
			p.parent = null;
		
	
这里面比较重要的一个操作就是fixAfterDeletion函数,这个也是最难理解的,下面对这个函数进行深入的解析。这个和fixAfterInsert一样,在删除的时候也存在很多种情况。这里总共有八种情况。下面分别进行说明。

这八种情况也是对称的,分别是节点x是其父节点p的左孩子或者是右孩子。节点x的颜色是BLACK

1.节点x是父节点p的左孩子,x的兄弟节点为sib,且sib的颜色为RED。

在这种情况下进行的操作是将sib设为BLACK,将x的父节点p设为RED,左旋x的父节点p,然后将sib赋值为p的右孩子节点。然后就会进入情况2或者3、4的操作。


在fixAfterDeletion函数中所对应的代码片段如下:

if (x == leftOf(parentOf(x))) 
	Entry<K,V> sib = rightOf(parentOf(x));

	if (colorOf(sib) == RED) 
		setColor(sib, BLACK);
		setColor(parentOf(x), RED);
		rotateLeft(parentOf(x));
		sib = rightOf(parentOf(x));
	
	...

2.节点x为BLACK,其兄弟节点sib为BLACK,且sib的左孩子和右孩子都为BLACK。

这种情况下所做的操作是将sib设置为RED,并且将x设置为x的父节点p。这样如果情况2是从1来的,那么循环就结束了,因为p是RED。最后跳出循环后,将x设置为BLACK,那么调整结束。



在fixAfterDeletion函数中所对应的代码片段如下:

if (x == leftOf(parentOf(x))) 
	Entry<K,V> sib = rightOf(parentOf(x));

	...

	if (colorOf(leftOf(sib))  == BLACK &&
		colorOf(rightOf(sib)) == BLACK) 
		setColor(sib, RED);
		x = parentOf(x);
	 
	...

3.节点x为BLACK,其兄弟节点sib为BLACK,且sib的左孩子为RED,sib的右孩子为BLACK。

这种情况下,设置sib的左孩子为BLACK,设置sib为RED,然后右旋sib,然后设置sib为x的父节点p的右子树,然后进入情况4的处理步骤。


在fixAfterDeletion函数中所对应的代码片段如下:

if (x == leftOf(parentOf(x))) 
	Entry<K,V> sib = rightOf(parentOf(x));

	...

	else 
		if (colorOf(rightOf(sib)) == BLACK) 
			setColor(leftOf(sib), BLACK);
			setColor(sib, RED);
			rotateRight(sib);
			sib = rightOf(parentOf(x));
		
		...
	
4.节点x为BLACK,其兄弟节点sib为BLACK,且sib的左右孩子均为RED,或者由情况3进入情况4。

在这种情况下,将sib节点的颜色设置成和x的父节点p相同的颜色,设置x的父节点颜色为BLACK,设置sib右孩子的颜色为BLACK,左旋x的父节点p,然后将x赋值为root,循环结束。



在fixAfterDeletion函数中所对应的代码片段如下:

if (x == leftOf(parentOf(x))) 
	Entry<K,V> sib = rightOf(parentOf(x));

	...

	else 
		...
		setColor(sib, colorOf(parentOf(x)));
		setColor(parentOf(x), BLACK);
		setColor(rightOf(sib), BLACK);
		rotateLeft(parentOf(x));
		x = root;
	
至此,删除操作对于红黑树结构的调整就完成了。对于fixAfterDeletion中当节点x是右孩子的操作对应的也是有四组,是分别和上述的1,2,3,4所对称的。这里就不在赘述了。请大家看代码就可以了。

虽说代码现在基本搞明白了,但是如果让我自己去写实现的话,这个步骤还是搞不定。主要还是因为这样操作的原理还没有彻底弄明白。这个还需后续再研究


以上是关于TreeMap关键源码解析-红黑树操作的主要内容,如果未能解决你的问题,请参考以下文章

Java - HashTree源码解析 + 红黑树

TreeMap源码分析之一 —— 排序二叉树平衡二叉树红黑树

JAVA集合:TreeMap红黑树深度解析

死磕 java集合之TreeMap源码分析- 内含红黑树分析全过程

TreeMap核心源码实现解析

数据结构-红黑树解析