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

情況1

在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進行處理

情況2

3.節點x和其父節點p都是RED,且p爲其父節點pp的左孩子,節點x是其父節點p的左孩子,且其叔父節點y顏色是BLACK

這種情況下需要進行的操作是將x的父節點p設爲BLACK,p的父節點pp設置爲RED,然後對pp進行右旋操作

情況2

(注意這裏畫的幾個圖只是一個片段,不是完整的紅黑樹,不然紅黑樹的性質四都不滿足了)

對於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

情況4

在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進行處理

情況5

6.是與情況3對稱的,即節點x和其父節點p都是RED,且p爲其父節點pp的右孩子,節點x是其父節點p的右孩子,且其叔父節點y顏色是BLACK

這種情況下需要進行的操作是將x的父節點p設爲BLACK,p的父節點pp設置爲RED,然後對pp進行左旋操作
情況6

(注意這裏畫的幾個圖只是一個片段,不是完整的紅黑樹,不然紅黑樹的性質四都不滿足了)

對於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的操作。

刪除情況1

在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,那麼調整結束。

刪除情況2


在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的處理步驟。

刪除操作3

在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,循環結束。

刪除操作4



在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所對稱的。這裏就不在贅述了。請大家看代碼就可以了。

雖說代碼現在基本搞明白了,但是如果讓我自己去寫實現的話,這個步驟還是搞不定。主要還是因爲這樣操作的原理還沒有徹底弄明白。這個還需後續再研究


發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章