最近斷斷續續花了一個禮拜的時間去看紅黑樹算法,關於此算法仍是比較難,由於涉及到諸多場景要考慮,同時接下來咱們要講解的HashMap、TreeMap等原理都涉及到紅黑樹算法,因此咱們不得不瞭解其原理,關於一些基礎知識這裏再也不講解,本文參考博文:《http://www.javashuo.com/article/p-yfyjkmtq-gv.html》,參考連接太多文字描述,看過不少關於紅黑樹的文章,有些越講越懵逼,有些講的挺好關鍵是不說人話(這裏不是罵人哈,指的是文章講解的仍是有點抽象),在這裏但願經過我我的的理解既讓閱讀本文的您可以充分理解其原理也能徹底快速記住各類場景。html
紅黑樹是一種自平衡二進制搜索樹(BST),紅黑樹與AVL樹相比,AVL樹更加平衡,可是它們可能會在插入和刪除過程當中引發更多旋轉。所以,若是咱們的應用程序涉及許多頻繁的插入和刪除操做,則應首選紅黑樹。可是,若是插入和刪除操做的頻率較低,而搜索操做的頻率較高,則AVL樹應優先於紅黑樹。咱們需牢記紅黑樹的每一個節點所遵循的如下規則算法
(1)每一個節點或者是黑色,或者是紅色。
(2)根節點是黑色。
(3)每一個葉子節點是黑色。 [注意:這裏葉子節點,是指爲空的葉子節點,在算法原理中空用Nil表示,可是在面嚮對象語言中空都用NULL表示]
(4)若是一個節點是紅色的,則它的子節點必須是黑色的(注意:這裏指的是不能有兩個連續的紅色節點)。
(5)從一個節點到該節點的子孫節點的全部路徑上包含相同數目的黑節點。數據結構
瞭解如上規則後,接下來將進入紅黑樹的插入和刪除操做,插入操做還好,最複雜的在於刪除操做,莫慌,咱們一步步來,不管是插入仍是刪除操做均可能會引發樹的再次不平衡即會打破以上紅黑樹的規則,在進行插入或刪除操做時,爲使得樹再次平衡咱們使用【變色】和【旋轉】方法來解決。假如Z爲插入節點,在這裏咱們作以下命名約定:父親節點、祖父節點、叔叔節點。好了,接下來咱們首先來看插入操做。優化
一說到插入咱們立馬就有了疑惑,根據紅黑樹規則一來看,每一個節點非紅即黑,那麼咱們插入的節點究竟是紅色仍是黑色呢?若是爲黑色將很大可能性會破壞規則五,此時咱們爲使樹再次平衡將花費很大功夫,可是若是爲紅色,也頗有可能性破壞以上規則二和四,可是比插入節點爲黑色更加易於修復。因此這就是爲何插入節點爲紅色的緣由。因此第一步,咱們執行標準的BST插入且節點顏色爲紅色,插入操做分爲如下四種場景。this
(1)Z是根節點spa
(2)Z的父親爲紅色節點、叔叔爲紅色節點code
(3)Z的父親爲紅色節點、叔叔爲黑色節點(直線)htm
(4)Z的父親爲紅色節點、叔叔爲黑色節點(三角形)對象
當Z是根節點時,由於默認插入節點爲紅色,但根據紅黑樹規則二根節點爲黑色,因此進行變色,直接將紅色變爲黑色,以下:blog
不區分Z是在其父親節點左側或者右側,也不區分Z的父親節點是在Z的祖父節點左側或者右側都進行以下相同處理操做。
(1) 將「父親節點」設爲黑色。
(2) 將「叔叔節點」設爲黑色。
(3) 將「祖父節點」設爲「紅色」。
(4) 將「祖父節點」設爲「當前節點」(紅色節點);即,以後繼續對「當前節點」進行操做。
或者
根據如上大前提,有的童鞋可能分爲Z在其父親節點左側和右側兩種狀況,這裏我採用的是Z、Z的父親節點、Z的祖父節點在同一條直線上時的兩種對稱狀況,同理以下講解三角形時也是同樣,將Z、Z的父親節點、Z的祖父節點構成三角形時的兩種對稱狀況,這樣在腦海中思考並畫一筆是否是會更好理解一點呢。因爲對稱分爲兩種狀況:
(1)當Z的父親節點在Z的祖父節點左側時:【1】將「父親節點」設置爲黑色 【2】將「祖父節點」設置爲紅色 【3】以「父親節點」右旋
(2)當Z的父親節點在Z的祖父節點右側時:【1】將「父親節點」設置爲黑色 【2】將「祖父節點」設置爲紅色 【3】以「祖父節點」左旋
或者
(1)當Z的父親節點在Z的祖父節點左側時:【1】將「父親節點」左旋 【2】將「父親節點」設置爲當前節點(即以下A節點)【3】演變爲如上直線第1種狀況,繼續操做
(2)當Z的父親節點在Z的祖父節點右側時:【1】將「父親節點」右旋 【2】將「父親節點」設置爲當前節點(即以下A節點)【3】演變爲如上直線第2種狀況,繼續操做
或者
首先咱們須要定義節點元素,每個節點有左孩子、右孩子、父親節點、節點顏色和存儲的元素,因此咱們對節點進行以下定義:
class RedBlackNode<T extends Comparable<T>> { //黑色節點 public static final int BLACK = 0; //紅色節點 public static final int RED = 1; //元素 public T key; //父節點 RedBlackNode<T> parent; //左孩子 RedBlackNode<T> left; //右孩子 RedBlackNode<T> right; //節點顏色 public int color; RedBlackNode(){ color = BLACK; parent = null; left = null; right = null; } RedBlackNode(T key){ this(); this.key = key; } }
接下來是定義紅黑樹,關於左旋和右旋方法就不給出了,紙上畫兩筆就能搞定的事情,咱們簡單進行以下定義
public class RedBlackTree<T extends Comparable<T>> { private RedBlackNode<T> root = null; private void rotateLeft(RedBlackNode<T> x) { } private void rotateRight(RedBlackNode<T> x) { } }
當進行插入操做時,咱們須要明確插入節點的具體位置,也就是說咱們須要查找插入節點的父親節點、左孩子和右孩子且默認插入節點爲紅色,最後經過變色或旋轉來進行修復,以下:
private void insert(RedBlackNode<T> z) { RedBlackNode<T> y = null; RedBlackNode<T> x = root; //若根節點不爲空,則循環查找插入節點的父節點 while (!isNull(x)) { y = x; // 若是元素值小於當前元素值則從左孩子繼續查找 if (z.key.compareTo(x.key) < 0) { x = x.left; } // 若是元素值小於當前元素值則從右孩子繼續查找 else { x = x.right; } } // 以y做爲z的父親節點 z.parent = y; // 若父親節點爲空,說明插入節點爲根節點 if (isNull(y)) root = z; else if (z.key.compareTo(y.key) < 0) y.left = z; else y.right = z; z.left = null; z.right = null; z.color = RedBlackNode.RED; insertFixup(z); }
接下來則是實現上述插入修復方法,上述咱們分析插入操做幾種的狀況的前提是插入節點的父親節點爲紅色,因此這裏咱們經過循環插入節點的父親節點若爲紅色來進行修復,同時呢,不管是插入仍是刪除都是有其對稱狀況,也就是說咱們可將插入和刪除的節點分爲是在其父親節點的左側仍是右側兩種大的狀況,毫無疑問這兩種操做將一定對稱,最淺顯易懂的插入修復方法以下(已加上註釋,可再次藉助於上述分析來看)
private void insertFixup(RedBlackNode<T> z) { RedBlackNode<T> y = null; while (z.parent.color == RedBlackNode.RED) { //若是Z的父親節點在Z祖父節點左側 if (z.parent == z.parent.parent.left) { //定義Z的父親兄弟節點 y = z.parent.parent.right; //若是y是紅色 if (y.color == RedBlackNode.RED) { //z的父親變爲黑色 z.parent.color = RedBlackNode.BLACK; //y變爲黑色 y.color = RedBlackNode.BLACK; //z的祖父變爲紅色 z.parent.parent.color = RedBlackNode.RED; //將z的祖父做爲z z = z.parent.parent; } // 若是y是黑色且z是右孩子 else if (z == z.parent.right) { // 將z的父親做爲z z = z.parent; //以z的父親節點進行左旋 rotateLeft(z); } // 不然若是y黑色且z是左孩子 else { //z的父親變爲黑色 z.parent.color = RedBlackNode.BLACK; //z的祖父變爲紅色 z.parent.parent.color = RedBlackNode.RED; //以z的祖父右旋 rotateRight(z.parent.parent); } } // 若是Z的父親節點在Z祖父節點右側 else { // 定義Z的父親兄弟節點 y = z.parent.parent.left; // 若是y是紅色 if (y.color == RedBlackNode.RED) { //z的父親變爲黑色 z.parent.color = RedBlackNode.BLACK; //y變爲黑色 y.color = RedBlackNode.BLACK; //z的祖父變爲紅色 z.parent.parent.color = RedBlackNode.RED; //以z的父親節點進行左旋 z = z.parent.parent; } // 若是y是黑色且z是左孩子 else if (z == z.parent.left) { // 將z的父親做爲z z = z.parent; //以z的父親節點進行右旋 rotateRight(z); } //不然若是y黑色且z是右孩子 else { //z的父親變爲黑色 z.parent.color = RedBlackNode.BLACK; //z的祖父變爲紅色 z.parent.parent.color = RedBlackNode.RED; //以z的祖父左旋 rotateLeft(z.parent.parent); } } } // 操做完畢後,根節點從新變爲黑色 root.color = RedBlackNode.BLACK; }
在上述插入操做中,咱們主要是檢查叔叔的顏色從而考慮不一樣的狀況,也就是說插入後違反的主要是兩個連續的紅色。在刪除操做中,咱們檢查同級的顏色也就是說檢查兄弟節點的顏色從而考慮不一樣的狀況,刪除主要違反的屬性是子樹中黑色高度的更改,由於刪除黑色節點可能會致使根到葉路徑的黑色高度下降,換言之就是破壞了紅黑樹規則五,那麼咱們到底應該如何刪除呢?執行標準的BST刪除,當咱們在BST中執行標準刪除操做時,咱們最終老是刪除一個葉子節點或只有一個孩子的節點(對於內部節點,咱們複製後繼節點,而後遞歸調用刪除後繼節點,後繼節點始終是葉節點或一個有一個孩子的節點),所以,咱們只須要處理一個節點爲葉子或有一個孩子的狀況,由於刪除是一個至關複雜的過程,爲了理解刪除,咱們引入雙重黑色的概念,當刪除黑色節點並用黑色子節點替換時,該子節點被標記爲double black,此時黑色的高度將不變,因此對於刪除咱們主要的任務就是將雙黑色轉換爲單黑色便可。好像聽起來感受仍是一臉懵逼,莫慌,接下來我依然將用詳細的圖解給你們講解到底雙黑是怎樣的一個神奇存在。刪除操做總的來講分爲如下三種狀況:
(1) 被刪除節點沒有兒子,即爲葉節點。(直接刪除)
(2) 被刪除節點只有一個兒子。(直接刪除該節點,並用該節點的惟一子節點頂替它的位置)
(3) 被刪除節點有兩個兒子。
以上第一和第二種狀況就不用我多講,對於第三種狀況就涉及到上述咱們引入的雙黑的概念,參考連接中這樣描述:好比刪除節點v(黑色),則將後繼節點u佔據v節點,因爲刪除節點u爲黑色,因此致使通過此節點的黑色節點數目減小了一個,爲了解決這個問題,咱們將佔據的v節點額外引入一個黑色節點,雖然這樣解決了紅黑樹規則五的問題,可是咱們知道紅黑樹規則一爲每一個節點非紅即黑,因此破壞了規則一,而後咱們經過變色或旋轉解決。咱們將佔據u節點上額外引入一個黑色節點,因此出現雙黑,是否是有點疑惑,這說的究竟是什麼意思呢,咱們看看以下圖來理解將一目瞭然,那麼在紅黑樹中如何將以下出現的雙黑變爲單黑的呢?請往下看。
【1】左左狀況(A節點是其父節點的左節點,C是A的左節點)
(1)將Z兄弟節點即A節點的左孩子變爲黑色(2)以Z的父親節點即B節點進行右旋(注:咱們將看到右旋時D節點將搭接到B節點上,此時將Z節點上的雙黑給出一個黑色節點來讓D進行搭接,最終雙黑演變成單黑)
【2】 右右狀況(A節點是其父節點的右節點,C是A的右節點)
(1)將Z兄弟節點即A節點的右孩子變爲黑色(2)以Z的父親節點即B節點進行左旋(注:咱們將看到左旋時D節點將搭接到B節點上,此時將Z節點上的雙黑給出一個黑色節點來讓D進行搭接,最終雙黑演變成單黑)
【3】左右狀況(A節點是其父節點的左節點,C是A的右節點)
(1)將Z兄弟節點即A節點變爲紅色(2)將Z兄弟節點即A節點的右孩子變爲黑色(3)以Z的兄弟節點A進行左旋(4)演變成如上左左狀況,繼續操做
【4】右左狀況(A節點是其父節點的右節點,C是A的左節點)
(1)將Z兄弟節點即A節點變爲紅色(2)將Z兄弟節點即A節點的左孩子變爲黑色(3)以Z的兄弟節點A進行右旋(4)演變成如上右右狀況,繼續操做
【1】父節點爲紅色狀況(變色)
(1)將Z節點的父親節點即B節點變爲紅色(2)將Z節點的兄弟節點即A節點變爲紅色(3)只需變色:紅色+雙黑色=單個黑色
【2】父節點爲黑色狀況(父節點雙黑,繼續遞歸)
(1)將Z節點的兄弟節點即A節點變爲紅色(2)將Z的父親節點即B節點賦給Z節點,繼續進行遞歸操做
【1】Z節點的兄弟節點即A節點在Z節點的父親節點左邊狀況
(1)將Z節點的兄弟節點即A節點變爲黑色(2)將Z的父親節點即B節點變爲黑色(3)以Z節點的父親節點即B節點進行右旋(4)演變成上述父親節點爲紅色狀況,繼續操做
【2】Z節點的兄弟節點即A節點在Z節點的父親節點右邊狀況
(1)將Z節點的兄弟節點即A節點變爲黑色(2)將Z的父親節點即B節點變爲黑色(3)以Z節點的父親節點即B節點進行左旋(4)演變成上述父親節點爲紅色狀況,繼續操做
對於刪除操做,首先咱們須要查找到須要刪除的節點 ,如咱們所分析的那樣,若刪除節點孩子只有其一直接刪除便可,若存在兩個孩子,除了找到後繼執行標準的刪除操做外,還需進行刪除修復操做,以下:
public void remove(RedBlackNode<T> v) { RedBlackNode<T> z = search(v.key); RedBlackNode<T> x = null; RedBlackNode<T> y = null; //若是z的孩子之一爲null,則必須刪除z if (isNull(z.left) || isNull(z.right)) y = z; //不然咱們須要刪除z的後繼 else y = findSuccessor(z); // 令x爲y的左或右的孩子(y只能有一個子代) if (!isNull(y.left)) x = y.left; else x = y.right; // 設置y的父親是x的父親 x.parent = y.parent; // 若是y的父親節點是null,說明x就是根節點 if (isNull(y.parent)) root = x; //若是y是左孩子,設置x是y的左兄弟 else if (!isNull(y.parent.left) && y.parent.left == y) y.parent.left = x; //若是y是右孩子,設置x是y的右兄弟 else if (!isNull(y.parent.right) && y.parent.right == y) y.parent.right = x;
// 若是y是黑色,則違反紅黑樹規則需修復 if (y.color == RedBlackNode.BLACK) removeFixup(x); }
private void removeFixup(RedBlackNode<T> x) { RedBlackNode<T> w; // 當刪除節點不是根節點且爲黑色時 while (x != root && x.color == RedBlackNode.BLACK) { //若是x在其父親節點左側 if (x == x.parent.left) { //定義x的兄弟節點 w = x.parent.right; //w是紅色時 if (w.color == RedBlackNode.RED) { //w變爲黑色 w.color = RedBlackNode.BLACK; //x的父親變爲紅色 x.parent.color = RedBlackNode.RED; //以x的父親左旋 rotateLeft(x.parent);
w = x.parent.right; } //w兩個孩子都是黑色時 if (w.left.color == RedBlackNode.BLACK && w.right.color == RedBlackNode.BLACK) { //w變爲黑色 w.color = RedBlackNode.RED; //x的父親做爲x x = x.parent; } else { // w的右孩子爲黑色時 if (w.right.color == RedBlackNode.BLACK) { //w的左孩子變爲黑色 w.left.color = RedBlackNode.BLACK; //w變爲紅色 w.color = RedBlackNode.RED; //以w右旋 rotateRight(w); //從新將x的父親右側孩子賦給w w = x.parent.right; } // w是黑色,右黑子爲紅色時 //w變爲x父親的顏色 w.color = x.parent.color; //x的父親變爲黑色 x.parent.color = RedBlackNode.BLACK; //w的右孩子變爲黑色 w.right.color = RedBlackNode.BLACK; //以x的父親左旋 rotateLeft(x.parent);
x = root; } } //若是x在其父親節點右側 else { //定義x的兄弟節點 w = x.parent.left; //w是紅色時 if (w.color == RedBlackNode.RED) { //w變爲黑色 w.color = RedBlackNode.BLACK; //x的父親變爲紅色 x.parent.color = RedBlackNode.RED; //以x的父親右旋 rotateRight(x.parent); //從新將x的父親左側孩子賦給w w = x.parent.left; } //w兩個孩子都是黑色時 if (w.right.color == RedBlackNode.BLACK && w.left.color == RedBlackNode.BLACK) { //w變爲黑色 w.color = RedBlackNode.RED; //x的父親做爲x x = x.parent; } else { // w的右孩子爲黑色時 if (w.left.color == RedBlackNode.BLACK) { //w的左孩子變爲黑色 w.right.color = RedBlackNode.BLACK; //w變爲紅色 w.color = RedBlackNode.RED; //以w左旋 rotateLeft(w); w = x.parent.left; } // w是黑色,左黑子爲紅色時 //w變爲x父親的顏色 w.color = x.parent.color; //x的父親變爲黑色 x.parent.color = RedBlackNode.BLACK; //w的左孩子變爲黑色 w.left.color = RedBlackNode.BLACK; //以x的父親右旋 rotateRight(x.parent); x = root; } } } // 操做完畢後,x節點從新變爲黑色 x.color = RedBlackNode.BLACK; }
本節咱們詳細分析了紅黑樹原理,同時給出了大部分僞代碼,爲了讓看文章的童鞋能立馬看的懂,並未作進一步的優化。紙上得來終覺淺,得知此事要躬行,看網上其餘人的分析和本身再次進行分析效果可想而知,這篇文章斷斷續續搞了個把月纔出來,在這裏我只是經過圖解的方式去理解,看到這篇文章的童鞋再結合網上紅黑樹大量文字的描述估計也可以理解的七七八八了。有了本節的理解,接下來咱們再去分析其餘底層實現,必將垂手可得,文中如有敘述不當或錯誤之處,可在評論中提出。感謝您的閱讀,咱們下節再會。