摘要:很多文章或書籍在介紹紅黑樹的時候直接上來就是紅黑樹的個基本性質(zhì)插入刪除操作等。這也不奇怪,算法的作者就是紅黑樹的作者之一。所以,理解樹對掌握紅黑樹是至關(guān)重要的。
本文主要包括以下內(nèi)容:
什么是2-3樹
2-3樹的插入操作
紅黑樹與2-3樹的等價關(guān)系
《算法4》和《算法導論》上關(guān)于紅黑樹的差異
紅黑樹的5條基本性質(zhì)的分析
紅黑樹與2-3-4樹的等價關(guān)系
紅黑樹的插入、刪除操作
JDK TreeMap、TreeSet分析
今天我們來介紹下非常重要的數(shù)據(jù)結(jié)構(gòu):紅黑樹。
很多文章或書籍在介紹紅黑樹的時候直接上來就是紅黑樹的5個基本性質(zhì)、插入、刪除操作等。本文不是采用這樣的介紹方式,在介紹紅黑樹之前,我們要了解紅黑樹是怎么發(fā)展出來的,進而就能知道為什么會有紅黑樹的5條基本性質(zhì)。
這樣的介紹方式也是《算法4》的介紹方式。這也不奇怪,《算法4》的作者 Robert Sedgewick 就是紅黑樹的作者之一。在介紹紅黑樹之前,我們先來看下2-3樹
什么是2-3樹在介紹紅黑樹之前為什么要先介紹 2-3樹 呢?因為紅黑樹是 完美平衡的2-3樹 的一種實現(xiàn)。所以,理解2-3樹對掌握紅黑樹是至關(guān)重要的。
2-3樹 的一個Node可能有多個子節(jié)點(可能大于2個),而且一個Node可以包含2個鍵(元素)
可以把 紅黑樹(紅黑二叉查找樹) 當作 2-3樹 的一種二叉結(jié)構(gòu)的實現(xiàn)。
在前面介紹的二叉樹中,一個Node保存一個值,在2-3樹中把這樣的節(jié)點稱之為 2- 節(jié)點
如果一個節(jié)點包含了兩個值(可以當作兩個節(jié)點的融合),在2-3樹中把這樣的節(jié)點稱之為 3- 節(jié)點。 完美平衡的2-3樹所有空鏈接到根節(jié)點的距離都應該是相同的
下面看下《算法4》對 2-3-節(jié)點的定義:
2- 節(jié)點,含有一個鍵(及其對應的值)和兩條鏈接。該節(jié)點的左鏈接小于該節(jié)點的鍵;該節(jié)點的右鏈接大于該節(jié)點的鍵
3- 節(jié)點,含有兩個鍵(及其對應的值)和三條鏈接。左鏈接小于該節(jié)點的左鍵;中鏈接在左鍵和右鍵之間;右鏈接大于該節(jié)點右鍵
如下面一棵 完美平衡的2-3樹 :
2-3樹 是一棵多叉搜索樹,所以數(shù)據(jù)的插入類似二分搜索樹
2-3樹的插入操作紅黑樹是對 完美平衡的2-3樹 的一種實現(xiàn),所以我們主要介紹完美平衡的2-3樹的插入過程
完美平衡的2-3樹插入分為以下幾種情況(為了方便畫圖默認把空鏈接去掉):
向 2- 結(jié)點中插入新鍵 向一棵只含有一個3-結(jié)點的樹中插入新鍵因為2-3樹中節(jié)點只能是2-節(jié)點或者3-節(jié)點
往3-點中再插入一個鍵就成了4-節(jié)點,需要對其進行分解,如下所示:
向一個父結(jié)點為 2- 結(jié)點的 3- 結(jié)點插入新鍵往3-點中再插入一個鍵就成了4-節(jié)點,需要對其進行分解,對中間的鍵向上融合
由于父結(jié)點是一個 2- 結(jié)點 ,融合后變成了 3- 結(jié)點,然后把 4- 結(jié)點的左鍵變成該 3- 節(jié)點的中間子結(jié)點
向一個父結(jié)點為3- 結(jié)點的 3- 結(jié)點中插入新鍵在這種情況下,向3- 結(jié)點插入新鍵形成暫時的4- 結(jié)點,向上分解,父節(jié)點又形成一個4- 結(jié)點,然后繼續(xù)上分解
一個 4- 結(jié)點分解為一棵2-3樹6種情況 紅黑樹 完美平衡的2-3樹和紅黑樹的對應關(guān)系上面介紹完了2-3樹,下面來看下紅黑樹是怎么來實現(xiàn)一棵完美平衡的2-3樹的
紅黑樹的背后的基本思想就是用標準的二分搜索樹和一些額外的信息來表示2-3樹的
這額外的信息指的是什么呢?因為2-3樹不是二叉樹(最多有3叉),所以需要把 3- 結(jié)點 替換成 2- 結(jié)點
額外的信息就是指替換3-結(jié)點的方式
將2-3樹的鏈接定義為兩種類型:黑鏈接、紅鏈接
黑鏈接 是2-3樹中普通的鏈接,可以把2-3樹中的 2- 結(jié)點 與它的子結(jié)點之間的鏈當作黑鏈接
紅鏈接 2-3樹中 3- 結(jié)點分解成兩個 2- 結(jié)點,這兩個 2- 結(jié)點之間的鏈接就是紅鏈接
那么如何將2-3樹和紅黑樹等價起來,我們規(guī)定:紅鏈接均為左鏈接
根據(jù)上面對完美平衡的2-3樹和紅鏈接的介紹可以得出結(jié)論:沒有一個結(jié)點同時和兩個紅鏈接相連
根據(jù)上面對完美平衡的2-3樹和黑鏈接的介紹可以得出結(jié)論:完美平衡的2-3樹是保持完美黑色平衡的,任意空鏈接到根結(jié)點的路徑上的黑鏈接數(shù)量相同
據(jù)此,我們可以得出3條性質(zhì):
紅鏈接均為左鏈接
沒有一個結(jié)點同時和兩個紅鏈接相連
完美平衡的2-3樹是保持完美黑色平衡的,任意空鏈接到根結(jié)點的路徑上的黑鏈接數(shù)量相同
在紅黑樹中,沒有一個對象來表示紅鏈接和黑鏈接,通過在結(jié)點上加上一個屬性(color)來標識紅鏈接還是黑鏈接,color值為red表示結(jié)點是紅結(jié)點,color值為black表示結(jié)點是黑結(jié)點。
黑結(jié)點 2-3樹中普通的 2-結(jié)點 的顏色
紅結(jié)點 2-3樹中 3- 結(jié)點 分解出兩個 2-結(jié)點 的最小 2-結(jié)點
下面是2-3樹和紅黑樹的一一對應關(guān)系圖:
紅黑樹的5個基本性質(zhì)的分析介紹完了2-3樹和紅黑樹的對應關(guān)系后,我們再來看下紅黑樹的5個基本性質(zhì):
每個結(jié)點要么是紅色,要么是黑色
根結(jié)點是黑色
每個葉子結(jié)點(最后的空節(jié)點)是黑色
如果一個結(jié)點是紅色的,那么他的孩子結(jié)點都是黑色的
從任意一個結(jié)點到葉子結(jié)點,經(jīng)過的黑色結(jié)點是一樣的
2-3樹和紅黑樹的對應關(guān)系后我們也就知道了紅黑樹的5個基本性質(zhì)是怎么來的了
紅黑樹的第一條性質(zhì):每個節(jié)點要么是紅色,要么是黑色
因為我們用結(jié)點上的屬性來表示紅鏈還是黑鏈,所以紅黑樹的結(jié)點要么是紅色,要么是黑色是很自然的事情
紅黑樹的第二條性質(zhì):根結(jié)點是黑色
紅色節(jié)點的情況是 3- 結(jié)點分解出兩個 2- 結(jié)點的最小節(jié)點是紅色,根節(jié)點沒有父節(jié)點所以只能是黑色
紅黑樹的第三條性質(zhì):每個葉子結(jié)點(最后的空節(jié)點)是黑色
葉子節(jié)點也就是2-3樹中的空鏈,如果空鏈是紅色說明下面還是有子結(jié)點的,但是空鏈是沒有子結(jié)點的;另一方面如果
空鏈是紅色,空鏈指向的父結(jié)點結(jié)點如果也是紅色就會出現(xiàn)兩個連續(xù)的紅色鏈接,就和上面介紹的 “沒有一個結(jié)點同時和兩個紅鏈接相連” 相違背
紅黑樹的第四條性質(zhì):如果一個結(jié)點是紅色的,那么他的孩子結(jié)點都是黑色的
上面介紹的‘沒有一個結(jié)點同時和兩個紅鏈接相連’,所以一個結(jié)點是紅色,那么他的孩子結(jié)點都是黑色
紅黑樹的第五條性質(zhì):從任意一個結(jié)點到葉子結(jié)點,經(jīng)過的黑色結(jié)點是一樣的
在介紹完美平衡的2-3樹和黑鏈接我們得出的結(jié)論:‘完美平衡的2-3樹是保持完美黑色平衡的,任意空鏈接到根結(jié)點的路徑上的黑鏈接數(shù)量相同’, 所以從任意一個結(jié)點到葉子結(jié)點,經(jīng)過的黑色結(jié)點數(shù)是一樣的紅黑樹實現(xiàn)2-3樹過程中的結(jié)點旋轉(zhuǎn)和顏色翻轉(zhuǎn) 顏色翻轉(zhuǎn)
為什么要顏色翻轉(zhuǎn)(flipColor)?在插入的過程中可能出現(xiàn)如下情況:兩個左右子結(jié)點都是紅色
根據(jù)我們上面的描述,紅鏈只允許是左鏈(也就是左子結(jié)點是紅色)
所以需要進行顏色轉(zhuǎn)換:把該結(jié)點的左右子結(jié)點設置為黑色,自己設置為黑色
private void flipColor(Node左旋轉(zhuǎn)node) { node.color = RED; node.left.color = BLACK; node.right.color = BLACK; }
左旋情況大致有兩種:
結(jié)點是右子結(jié)點且是紅色
顏色翻轉(zhuǎn)后,結(jié)點變成紅色且它是父結(jié)點的右子節(jié)點
private Node右旋轉(zhuǎn)rotateLeft(Node node) { Node x = node.right; node.right = x.left; x.left = node; x.color = node.color; node.color = RED; return x; }
需要右旋的情況:連續(xù)出現(xiàn)兩個左紅色鏈接
private Node紅黑樹實現(xiàn)2-3樹插入操作rotateRight(Node node) { Node x = node.left; node.left = x.right; x.right = node; x.color = node.color; node.color = RED; return x; }
通過我們上面對紅黑樹和2-3樹的介紹,紅黑樹實現(xiàn)2-3樹插入操作就很簡單了
只要滿足不出現(xiàn) 兩個連續(xù)左紅色鏈接、右紅色鏈接、左右都是紅色鏈接 的情況就可以了
所以僅僅需要處理三種情況即可:
如果出現(xiàn)右側(cè)紅色鏈接,需要左旋
如果出現(xiàn)兩個連續(xù)的左紅色鏈接,需要右旋
如果結(jié)點的左右子鏈接都是紅色,需要顏色翻轉(zhuǎn)
private Node_add(Node node, K key, V value) { //向葉子結(jié)點插入新結(jié)點 if (node == null) { size++; return new Node<>(key, value); } //二分搜索的過程 if (key.compareTo(node.key) < 0) node.left = _add(node.left, key, value); else if (key.compareTo(node.key) > 0) node.right = _add(node.right, key, value); else node.value = value; //1,如果出現(xiàn)右側(cè)紅色鏈接,左旋 if (isRed(node.right) && !isRed(node.left)) { node = rotateLeft(node); } //2,如果出現(xiàn)兩個連續(xù)的左紅色鏈接,右旋 if (isRed(node.left) && isRed(node.left.left)) { node = rotateRight(node); } //3,如果結(jié)點的左右子鏈接都是紅色,顏色翻轉(zhuǎn) if (isRed(node.left) && isRed(node.right)) { flipColor(node); } } public void add(K key, V value) { root = _add(root, key, value); root.color = BLACK; }
這樣下來紅黑樹依然保持著它的五個基本性質(zhì),下面我們來對比下JDK中的TreeMap的插入操作
先按照上面的紅黑樹插入邏輯插入三個元素 [14, 5, 20],流程如下:
使用Java TreeMap來插入上面三個元素,流程如下:
通過對比我們發(fā)現(xiàn)兩者的插入后的結(jié)果不一樣,而且Java TreeMap是允許左右子結(jié)點都是紅色結(jié)點!
這就和我們一直在說的用完美平衡的2-3樹作為紅黑樹實現(xiàn)的基礎結(jié)構(gòu)相違背了,我們一直在強調(diào)不允許右節(jié)點是紅色,也不允許兩個連續(xù)的紅色左節(jié)點,不允許左右結(jié)點同時是紅色
這也是《算法4》在講到紅黑樹時遵循的。但是JDK TreeMap(紅黑樹)是允許右結(jié)點是紅色,也允許左右結(jié)點同時是紅色,Java TreeMap的紅黑樹實現(xiàn)從它的代碼注釋(From CLR)說明它的實現(xiàn)來自《算法導論》
說明《算法4》和《算法導論》中的所介紹的紅黑樹產(chǎn)生了一些“出入”,給我們理解紅黑樹增加了一些困惑和難度
《算法4》在介紹紅黑樹之前先給我們詳細介紹了2-3樹,然后接著講到完美平衡的2-3樹和紅黑樹的對應關(guān)系(紅黑樹就等于完美平衡的2-3樹),讓我們知道紅黑樹是怎么來的,根據(jù)這些介紹你自己是可以解釋紅黑樹的的5個基本性質(zhì)為什么是這樣的。
而在《算法導論》中介紹紅黑樹的時候沒有提及2-3樹,直接就是紅黑樹的5個基本性質(zhì),以及紅黑樹的插入、刪除操作,感覺對初學者是不太合適的,因為你不知道為什么是這樣的,只是知道有這個五個性質(zhì),也許這就是為什么它叫導論的原因吧
而且在《算法4》中作者最后好像也沒有明確的給出紅黑樹的五個基本性質(zhì),在《算法導論》中在紅黑樹章節(jié)一開始就貼出了5條性質(zhì),感覺像是一種遞進和升華
這兩本書除了對紅黑樹講解的方式存在差異外,我們還發(fā)現(xiàn)《算法4》和《算法導論》在紅黑樹的實現(xiàn)上也是有差異的,就如我們上面插入三個元素 [14, 5, 20] 產(chǎn)生不同的結(jié)果
在解釋這些差異之前,我們再來看些2-3-4樹,上面提到完美平衡的2-3樹和紅黑樹等價,更準確的說是2-3-4樹和紅黑樹等價
2-3-4樹2-3-4樹 和 2-3樹 非常相像。2-3樹允許存在 2- 結(jié)點 和 3- 結(jié)點,類似的2-3-4樹允許存在 2- 結(jié)點、3- 結(jié)點 和 4- 結(jié)點
向2-結(jié)點、3-結(jié)點插入元素向2-結(jié)點插入元素,這個和上面介紹的2-3樹是一樣的,在這里就不敘述了
向3-結(jié)點插入元素,形成一個4-結(jié)點,因為2-3-4樹允許4-結(jié)點的存在,所以不需要向上分解
向4-結(jié)點插入元素向4-結(jié)點插入元素,需要分解4-結(jié)點, 因為2-3-4樹最多只允許存在4-結(jié)點,如:
如果待插入的4-結(jié)點,它的父結(jié)點也是一個4-結(jié)點呢?如下圖的2-3-4樹插入結(jié)點K:
主要有兩個方案:
Bayer于1972年提出的方案:使用相同的辦法去分解父結(jié)點的4-結(jié)點,直到不需要分解為止,方向是自底向上
Guibas 和 Sedgewick于1978年提出的方案:自上而下的方式,也就是在二分搜索的過程,一旦遇到4-結(jié)點就分解它,這樣在最終插入的時候永遠不會有父結(jié)點是4-結(jié)點的情況
Bayer全名叫做Rudolf Bayer(魯?shù)婪颉ぐ轄枺?/strong>,他在1972年發(fā)明的 對稱二叉B樹(symmetric binary B-tree) 就是 紅黑樹(red black tree) 的前身。
紅黑樹 這個名字是由 Leo J. Guibas 和 Robert Sedgewick 于1978年的一篇論文中提出來的,
對該論文感興趣的可以查看這個鏈接:http://professor.ufabc.edu.br...
下面的圖就是 自上而下 方案的流程圖
2-3-4樹和紅黑樹的等價關(guān)系在介紹2-3樹的時候我們也講解了2-3樹和紅黑樹的等價關(guān)系,由于2-3樹和2-3-4樹非常類似,所以2-3-4樹和紅黑樹的等價關(guān)系也是類似的。不同的是2-3-4的 4-結(jié)點 分解后的結(jié)點顏色變成如下形式:
所以可以得出下面一棵2-3-4樹和紅黑樹的等價關(guān)系圖:
上面在介紹紅黑樹實現(xiàn)2-3樹的時候講解了它的插入操作:
private Node_add(Node node, K key, V value) { //向葉子結(jié)點插入新結(jié)點 if (node == null) { size++; return new Node<>(key, value); } //二分搜索的過程 if (key.compareTo(node.key) < 0) node.left = _add(node.left, key, value); else if (key.compareTo(node.key) > 0) node.right = _add(node.right, key, value); else node.value = value; //1,如果出現(xiàn)右側(cè)紅色鏈接,左旋 if (isRed(node.right) && !isRed(node.left)) { node = rotateLeft(node); } //2,如果出現(xiàn)兩個連續(xù)的左紅色鏈接,右旋 if (isRed(node.left) && isRed(node.left.left)) { node = rotateRight(node); } //3,如果結(jié)點的左右子鏈接都是紅色,顏色翻轉(zhuǎn) if (isRed(node.left) && isRed(node.right)) { flipColor(node); } }
我們可以很輕松的把它改成2-3-4的插入邏輯(只需要把顏色翻轉(zhuǎn)的邏輯提到二分搜索的前面即可):
private Node_add(Node node, K key, V value) { //向葉子結(jié)點插入新結(jié)點 if (node == null) { size++; return new Node<>(key, value); } //split 4-nodes on the way down if (isRed(node.left) && isRed(node.right)) { flipColor(node); } //二分搜索的過程 if (key.compareTo(node.key) < 0) node.left = _add(node.left, key, value); else if (key.compareTo(node.key) > 0) node.right = _add(node.right, key, value); else node.value = value; //fix right-leaning reds on the way up if (isRed(node.right) && !isRed(node.left)) { node = rotateLeft(node); } //fix two reds in a row on the way up if (isRed(node.left) && isRed(node.left.left)) { node = rotateRight(node); } }
//使用2-3-4樹插入數(shù)據(jù) [E,C,G,B,D,F,J,A] RB2_3_4TreerbTree = new RB2_3_4Tree<>(); rbTree.add("E", "E"); rbTree.add("C", "C"); rbTree.add("G", "G"); rbTree.add("B", "B"); rbTree.add("D", "D"); rbTree.add("F", "F"); rbTree.add("J", "J"); rbTree.add("A", "A"); rbTree.levelorder(rbTree.root); //使用2-3樹插入數(shù)據(jù) [E,C,G,B,D,F,J,A] RBTree rbTree = new RBTree<>(); rbTree.add("E", "E"); rbTree.add("C", "C"); rbTree.add("G", "G"); rbTree.add("B", "B"); rbTree.add("D", "D"); rbTree.add("F", "F"); rbTree.add("J", "J"); rbTree.add("A", "A"); rbTree.levelorder(rbTree.root);
下面是 2-3-4樹 和 2-3樹 插入結(jié)果的對比圖:
所以我們一開始用紅黑樹實現(xiàn)完美平衡的2-3樹,左右結(jié)點是不會都是紅色的
現(xiàn)在用紅黑樹實現(xiàn)2-3-4樹,左右結(jié)點的可以同時是紅色的,這樣的紅黑樹效率更高。因為如果遇到左右結(jié)點是紅色,就進行顏色翻轉(zhuǎn),還需要對紅色的父結(jié)點進行向上回溯,因為父結(jié)點染成紅色了,可能父結(jié)點的父結(jié)點也是紅色,可能需要進行結(jié)點旋轉(zhuǎn)或者顏色翻轉(zhuǎn)操作,所以說2-3-4樹式的紅黑樹效率更高。
所以回到上面我們提到《算法4》和《算法導論》在實現(xiàn)上的差異的問題,就很好回答了,因為《算法4》是用紅黑樹實現(xiàn)2-3樹的,并不是2-3-4樹。但是如果是用紅黑樹實現(xiàn)2-3-4樹就和《算法導論》上介紹的紅黑樹一樣嗎?不一樣。
下面繼續(xù)做一個測試,分別往上面紅黑樹實現(xiàn)的 2-3-4樹 和 JDK TreeMap 中插入[E, D, R, O, S, X]
雖然兩棵樹都是紅黑樹,但是卻不一樣。并且TreeMap允許右節(jié)點是紅色,在2-3-4樹中最多是左右子結(jié)點同時是紅色的情況,不會出現(xiàn)左結(jié)點是黑色,右邊的兄弟結(jié)點是紅色的情況,為什么會有這樣的差異呢?
從上面的2-3-4樹的插入邏輯可以看出,如果右節(jié)點是紅色會執(zhí)行左旋轉(zhuǎn)操作,所以不會出現(xiàn)多帶帶紅右結(jié)點的情況
也就是說只會出現(xiàn)多帶帶的左結(jié)點是紅色的情況,我們把這種形式的紅黑樹稱之為左傾紅黑樹(Left Leaning Red Black Tree),包括上面的紅黑樹實現(xiàn)的完美平衡的2-3樹也是左傾紅黑樹
為什么在《算法4》中,作者規(guī)定所有的紅色鏈接都是左鏈接,這只是人為的規(guī)定,當然也可以是右鏈接,規(guī)定紅鏈接都是左鏈,可以使用更少的代碼來實現(xiàn)黑色平衡,需要考慮的情況會更少,就如上面我們介紹的插入操作,我們只需要考慮3中情況即可。
但是一般意義上的紅黑樹是不需要維持紅色左傾的這個性質(zhì)的,所以為什么TreeMap是允許多帶帶右紅結(jié)點的
如果還需要維護左傾情況,這樣的話就更多的操作,可能還需要結(jié)點旋轉(zhuǎn)和顏色的翻轉(zhuǎn),性能更差一些,雖然也是符合紅黑樹的性質(zhì)
介紹完了《算法4》上的紅黑樹,下面就來分析下一般意義上的紅黑樹的 插入 和 刪除 操作,也就是《算法導論》上介紹的紅黑樹。
紅黑樹插入操作插入操作有兩種情況是非常簡單的,所以在這里多帶帶說一下:
case 1. 如果插入的結(jié)點是根結(jié)點,直接把該結(jié)點設置為黑色,整個插入操作結(jié)束
如下圖所示:
case 2. 如果插入的結(jié)點的父結(jié)點是黑色,也無需調(diào)整,整個插入操作結(jié)束
如下圖所示:
下面開始介紹比較復雜的情況
紅黑樹插入操作,我們只需要處理父結(jié)點是紅色的情況,因為一開始紅黑樹肯定是黑色平衡的,就是因為往葉子節(jié)點插入元素后可能出現(xiàn)兩個連續(xù)的紅色的結(jié)點
需要注意的是,我們把新插入的結(jié)點默認設置為紅色,初始的時候,正在處理的節(jié)點就是插入的結(jié)點,在不斷調(diào)整的過程中,正在處理的節(jié)點會不斷的變化,且叔叔、爺爺、父結(jié)點都是相對于當前正在處理的結(jié)點來說的
case 3. 叔叔結(jié)點為紅色,正在處理的節(jié)點可以是左也可以是右結(jié)點
調(diào)整策略:由于父結(jié)點是紅色,叔叔結(jié)點是紅色,爺爺結(jié)點是黑色,執(zhí)行顏色翻轉(zhuǎn)操作 然后把當前正在處理的結(jié)點設置為爺爺結(jié)點,如果爺爺?shù)母附Y(jié)點是黑色插入操作結(jié)束,如果是紅色繼續(xù)處理
case 4. 叔叔結(jié)點為黑色,正在處理的結(jié)點是右結(jié)點
調(diào)整策略:由于父結(jié)點是紅色,叔叔結(jié)點為黑色,那么爺爺結(jié)點肯定是黑色 把正在處理的節(jié)點設置為父結(jié)點,然后左旋,形成Case5情況
case 5. 叔叔結(jié)點為黑色,正在處理的結(jié)點是左孩子
調(diào)整策略:由于父結(jié)點是紅色,叔叔結(jié)點為黑色,那么爺爺結(jié)點肯定是黑色 把父結(jié)點染黑,爺爺結(jié)點染紅,然后爺爺結(jié)點右旋
Case3、Case4、Case5如果多帶帶來理解的話比較困難,就算多帶帶為每一個Case畫圖,我覺得也很難完整的理解,很多博客上都是這種方式,感覺不太好理解。我將這三種情況通過一張流程圖串聯(lián)起來,將這三個Case形成一個整體,藍色箭頭表示正在處理的結(jié)點,如下所示:
紅黑樹刪除操作上面介紹完了紅黑樹的插入操作,接下來看下紅黑樹的刪除操作
紅黑樹的刪除操作比插入操作更加復雜一些
為了描述方便,我們把正在處理的結(jié)點稱之為 X,父結(jié)點為 P(Parent),兄弟節(jié)點稱之為 S(Sibling),左侄子稱之為 LN(Left Nephew),右侄子稱之為 RN(Right Nephew)
如果刪除的結(jié)點是黑色,那么就導致本來保持黑平衡的紅黑樹失衡了,從下圖可以看出結(jié)點P到左子樹的葉子結(jié)點經(jīng)過的黑節(jié)點數(shù)量為4(2+2),到右子樹的葉子節(jié)點經(jīng)過的黑色節(jié)點數(shù)量是5(2+3),如下圖所示:
紅黑樹的刪除操作,如果刪除的是黑色會導致紅黑樹就不能保持黑色平衡了,需要進行調(diào)整了;
如果刪除的是紅色,那么就無需調(diào)整,直接刪除即可,因為沒有沒有破壞黑色平衡
刪除結(jié)點后,無需調(diào)整的情況
case 1 刪除的結(jié)點是紅色結(jié)點,直接刪除即可
case 2 刪除的節(jié)點是黑色,如果當前處理的節(jié)點X是根結(jié)點
無論根結(jié)點是什么顏色,都將根結(jié)點設置為黑色
case 3 刪除的結(jié)點是黑色,如果當前處理的結(jié)點是紅色結(jié)點,將該結(jié)點設置為黑色
因為刪除黑色結(jié)點后,就打破了黑色平衡,黑高少了1 所以把一個紅色節(jié)點設置為黑色,這樣黑高又平衡了
刪除節(jié)點后,需要調(diào)整的情況
正在處理的結(jié)點為X,要刪除的結(jié)點是左結(jié)點,分為4中情況:
case 4 兄弟結(jié)點為紅色
調(diào)整方案:兄弟設置為黑色,父結(jié)點設置為紅色,父結(jié)點進行左旋轉(zhuǎn) 轉(zhuǎn)化為 case5、case6、case7
case 5 兄弟結(jié)點為黑色,左侄子LN為黑色,右侄子RN為黑色
在這種條件下,還有兩種情況:父結(jié)點是紅色或黑色,不管是那種情況,調(diào)整方案都是一致的
調(diào)整方案:將兄弟結(jié)點設置為紅色,把當前處理的結(jié)點設置為父結(jié)P
case 6 兄弟結(jié)點為黑色,左侄子為紅色,右侄子RN為黑色
調(diào)整方案:將左侄子結(jié)點設置為黑色,兄弟結(jié)點設置為紅色,兄弟結(jié)點右旋轉(zhuǎn),這樣就轉(zhuǎn)化成了case7
case 7 兄弟結(jié)點為黑色,左侄子不管紅黑,右侄子為紅色
處理方式:兄弟結(jié)點變成父結(jié)點的顏色,然后父結(jié)點設置黑色,右侄子設置黑色,父結(jié)點進行左旋轉(zhuǎn)
和插入操作一樣,下面通過一張流程圖把刪除需要調(diào)整的情況串聯(lián)起來:
上面處理的所有情況都是基于正在處理的結(jié)點是左結(jié)點Java TreeMap、TreeSet源碼分析
如果要調(diào)整正在處理的結(jié)點是右節(jié)點的情況,就是上面的處理的鏡像。插入操作也是同理,所以就省略了
TreeMap底層就是用紅黑樹實現(xiàn)的,它在插入后調(diào)整操作主要在fixAfterInsertion方法里,我為每種情況都添加注釋,如下所示:
/** From CLR */ private void fixAfterInsertion(Entryx) { x.color = RED; while (x != null && x != root && x.parent.color == RED) { if (parentOf(x) == leftOf(parentOf(parentOf(x)))) { Entry y = rightOf(parentOf(parentOf(x))); //-----Case3情況----- if (colorOf(y) == RED) { setColor(parentOf(x), BLACK); setColor(y, BLACK); setColor(parentOf(parentOf(x)), RED); x = parentOf(parentOf(x)); } else { //-----Case4情況----- if (x == rightOf(parentOf(x))) { x = parentOf(x); rotateLeft(x); } //-----Case5情況----- setColor(parentOf(x), BLACK); setColor(parentOf(parentOf(x)), RED); rotateRight(parentOf(parentOf(x))); } } else { //省略鏡像情況 } } root.color = BLACK; }
它的刪除后調(diào)整操作主要在fixAfterDeletion方法:
/** From CLR */ private void fixAfterDeletion(Entryx) { while (x != root && colorOf(x) == BLACK) { if (x == leftOf(parentOf(x))) { Entry sib = rightOf(parentOf(x)); //-----Case4的情況----- if (colorOf(sib) == RED) { setColor(sib, BLACK); setColor(parentOf(x), RED); rotateLeft(parentOf(x)); sib = rightOf(parentOf(x)); } //-----Case5的情況----- if (colorOf(leftOf(sib)) == BLACK && colorOf(rightOf(sib)) == BLACK) { setColor(sib, RED); x = parentOf(x); } else { //-----Case6的情況----- if (colorOf(rightOf(sib)) == BLACK) { setColor(leftOf(sib), BLACK); setColor(sib, RED); rotateRight(sib); sib = rightOf(parentOf(x)); } //-----Case7的情況----- setColor(sib, colorOf(parentOf(x))); setColor(parentOf(x), BLACK); setColor(rightOf(sib), BLACK); rotateLeft(parentOf(x)); x = root; } } else { // symmetric //省略鏡像的情況 } } setColor(x, BLACK); }
TreeSet 底層就是用 TreeMap 來實現(xiàn)的,往TreeSet添加進的元素當作TreeMap的key,TreeMap的value是一個常量Object。掌握了紅黑樹,對于這兩個集合的原理就不難理解了。
最后本文從一開始講的2-3樹和紅黑樹的對應關(guān)系,再到2-3-4樹和紅黑樹的對應關(guān)系,再到《算法4》和《算法導論》JDK TreeMap在紅黑樹上的差異
然后詳細介紹了紅黑樹的插入、刪除操作,最后分析了下Java中的TreeMap和TreeSet集合類。
人生當如紅黑樹,當過于自喜或過于自卑的時候,應當自我調(diào)整,尋求平衡。
我很丑,紅黑樹卻很美。希望本文對你有 些許幫助。
參考資料:
https://www.cs.princeton.edu/...
http://professor.ufabc.edu.br...
《算法4》、《算法導論》
文章版權(quán)歸作者所有,未經(jīng)允許請勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。
轉(zhuǎn)載請注明本文地址:http://systransis.cn/yun/73380.html
摘要:三系列用于保存鍵值對,無論是,還是已棄用的或者線程安全的等,都是基于紅黑樹。是完全基于紅黑樹的,并在此基礎上實現(xiàn)了接口。可以看到,只有紅黑樹,且紅黑樹是通過內(nèi)部類來實現(xiàn)的。 JDK容器 前言 閱讀JDK源碼有段時間了,準備以博客的形式記錄下來,也方便復習時查閱,本文參考JDK1.8源碼。 一、Collection Collection是所有容器的基類,定義了一些基礎方法。List、Se...
摘要:下面總結(jié)一下集合常用的三個子類吧無序,允許為,底層是散列表紅黑樹,非線程同步有序,不允許為,底層是紅黑樹非線程同步迭代有序,允許為,底層是雙向鏈表,非線程同步從結(jié)論而言我們就可以根據(jù)自己的實際情況來使用了。 前言 聲明,本文用的是jdk1.8 前面章節(jié)回顧: Collection總覽 List集合就這么簡單【源碼剖析】 Map集合、散列表、紅黑樹介紹 HashMap就是這么簡單【源碼...
摘要:底層的數(shù)據(jù)結(jié)構(gòu)就是數(shù)組鏈表紅黑樹,紅黑樹是在中加進來的。負載因子哈希表中的填滿程度。 前言 把 Java 容器的學習筆記放到 github 里了,還在更新~其他的目前不打算抽出來作為文章寫,感覺挖的還不夠深,等對某些東西理解的更深了再寫文章吧Java 容器目錄如下: Java 容器 一、概述 二、源碼學習 1. Map 1.1 HashMap 1.2 LinkedHashM...
摘要:若遇到哈希沖突,則將沖突的值加到鏈表中即可。之后相比于之前的版本,之后在解決哈希沖突時有了較大的變化,當鏈表長度大于閾值默認為時,將鏈表轉(zhuǎn)化為紅黑樹,以減少搜索時間。有序,唯一紅黑樹自平衡的排序二叉樹。 本文是最最最常見Java面試題總結(jié)系列第三周的文章。主要內(nèi)容: Arraylist 與 LinkedList 異同 ArrayList 與 Vector 區(qū)別 HashMap的底層...
閱讀 743·2021-10-14 09:42
閱讀 1995·2021-09-22 15:04
閱讀 1606·2019-08-30 12:44
閱讀 2167·2019-08-29 13:29
閱讀 2762·2019-08-29 12:51
閱讀 577·2019-08-26 18:18
閱讀 733·2019-08-26 13:43
閱讀 2846·2019-08-26 13:38