本文主要包括以下內(nèi)容:
- 什么是2-3樹(shù)
- 2-3樹(shù)的插入操作
- 紅黑樹(shù)與2-3樹(shù)的等價(jià)關(guān)系
- 《算法4》和《算法導(dǎo)論》上關(guān)于紅黑樹(shù)的差異
- 紅黑樹(shù)的5條基本性質(zhì)的分析
- 紅黑樹(shù)與2-3-4樹(shù)的等價(jià)關(guān)系
- 紅黑樹(shù)的插入抵拘、刪除操作
- JDK TreeMap酒朵、TreeSet分析
今天我們來(lái)介紹下非常重要的數(shù)據(jù)結(jié)構(gòu):紅黑樹(shù)。
很多文章或書(shū)籍在介紹紅黑樹(shù)的時(shí)候直接上來(lái)就是紅黑樹(shù)的5個(gè)基本性質(zhì)男旗、插入萧朝、刪除操作等秘症。本文不是采用這樣的介紹方式垒棋,在介紹紅黑樹(shù)之前爬范,我們要了解紅黑樹(shù)是怎么發(fā)展出來(lái)的,進(jìn)而就能知道為什么會(huì)有紅黑樹(shù)的5條基本性質(zhì)椅邓。
這樣的介紹方式也是《算法4》的介紹方式柠逞。這也不奇怪,《算法4》的作者 Robert Sedgewick 就是紅黑樹(shù)的作者之一景馁。在介紹紅黑樹(shù)之前板壮,我們先來(lái)看下2-3樹(shù)
什么是2-3樹(shù)
在介紹紅黑樹(shù)之前為什么要先介紹 2-3樹(shù) 呢?因?yàn)榧t黑樹(shù)是 完美平衡的2-3樹(shù) 的一種實(shí)現(xiàn)合住。所以绰精,理解2-3樹(shù)對(duì)掌握紅黑樹(shù)是至關(guān)重要的。
2-3樹(shù) 的一個(gè)Node可能有多個(gè)子節(jié)點(diǎn)(可能大于2個(gè))透葛,而且一個(gè)Node可以包含2個(gè)鍵(元素)
可以把 紅黑樹(shù)(紅黑二叉查找樹(shù)) 當(dāng)作 2-3樹(shù) 的一種二叉結(jié)構(gòu)的實(shí)現(xiàn)笨使。
在前面介紹的二叉樹(shù)中,一個(gè)Node保存一個(gè)值僚害,在2-3樹(shù)中把這樣的節(jié)點(diǎn)稱之為 2- 節(jié)點(diǎn)
如果一個(gè)節(jié)點(diǎn)包含了兩個(gè)值(可以當(dāng)作兩個(gè)節(jié)點(diǎn)的融合)硫椰,在2-3樹(shù)中把這樣的節(jié)點(diǎn)稱之為 3- 節(jié)點(diǎn)。 完美平衡的2-3樹(shù)所有空鏈接到根節(jié)點(diǎn)的距離都應(yīng)該是相同的
下面看下《算法4》對(duì) 2-3-節(jié)點(diǎn)的定義:
- 2- 節(jié)點(diǎn)萨蚕,含有一個(gè)鍵(及其對(duì)應(yīng)的值)和兩條鏈接靶草。該節(jié)點(diǎn)的左鏈接小于該節(jié)點(diǎn)的鍵;該節(jié)點(diǎn)的右鏈接大于該節(jié)點(diǎn)的鍵
- 3- 節(jié)點(diǎn)岳遥,含有兩個(gè)鍵(及其對(duì)應(yīng)的值)和三條鏈接奕翔。左鏈接小于該節(jié)點(diǎn)的左鍵;中鏈接在左鍵和右鍵之間浩蓉;右鏈接大于該節(jié)點(diǎn)右鍵
如下面一棵 完美平衡的2-3樹(shù) :
2-3樹(shù) 是一棵多叉搜索樹(shù)派继,所以數(shù)據(jù)的插入類似二分搜索樹(shù)
2-3樹(shù)的插入操作
紅黑樹(shù)是對(duì) 完美平衡的2-3樹(shù) 的一種實(shí)現(xiàn)帮坚,所以我們主要介紹完美平衡的2-3樹(shù)的插入過(guò)程
完美平衡的2-3樹(shù)插入分為以下幾種情況(為了方便畫(huà)圖默認(rèn)把空鏈接去掉):
向 2- 結(jié)點(diǎn)中插入新鍵
向一棵只含有一個(gè)3-結(jié)點(diǎn)的樹(shù)中插入新鍵
因?yàn)?-3樹(shù)中節(jié)點(diǎn)只能是2-節(jié)點(diǎn)或者3-節(jié)點(diǎn)
往3-點(diǎn)中再插入一個(gè)鍵就成了4-節(jié)點(diǎn),需要對(duì)其進(jìn)行分解互艾,如下所示:
向一個(gè)父結(jié)點(diǎn)為 2- 結(jié)點(diǎn)的 3- 結(jié)點(diǎn)插入新鍵
往3-點(diǎn)中再插入一個(gè)鍵就成了4-節(jié)點(diǎn)试和,需要對(duì)其進(jìn)行分解,對(duì)中間的鍵向上融合
由于父結(jié)點(diǎn)是一個(gè) 2- 結(jié)點(diǎn) 纫普,融合后變成了 3- 結(jié)點(diǎn)阅悍,然后把 4- 結(jié)點(diǎn)的左鍵變成該 3- 節(jié)點(diǎn)的中間子結(jié)點(diǎn)
向一個(gè)父結(jié)點(diǎn)為3- 結(jié)點(diǎn)的 3- 結(jié)點(diǎn)中插入新鍵
在這種情況下,向3- 結(jié)點(diǎn)插入新鍵形成暫時(shí)的4- 結(jié)點(diǎn)昨稼,向上分解节视,父節(jié)點(diǎn)又形成一個(gè)4- 結(jié)點(diǎn),然后繼續(xù)上分解
一個(gè) 4- 結(jié)點(diǎn)分解為一棵2-3樹(shù)6種情況
紅黑樹(shù)(RedBlackTree)
完美平衡的2-3樹(shù)和紅黑樹(shù)的對(duì)應(yīng)關(guān)系
上面介紹完了2-3樹(shù)假栓,下面來(lái)看下紅黑樹(shù)是怎么來(lái)實(shí)現(xiàn)一棵完美平衡的2-3樹(shù)的
紅黑樹(shù)的背后的基本思想就是用標(biāo)準(zhǔn)的二分搜索樹(shù)和一些額外的信息來(lái)表示2-3樹(shù)的
這額外的信息指的是什么呢寻行?因?yàn)?-3樹(shù)不是二叉樹(shù)(最多有3叉),所以需要把 3- 結(jié)點(diǎn) 替換成 2- 結(jié)點(diǎn)
額外的信息就是指替換3-結(jié)點(diǎn)的方式
將2-3樹(shù)的鏈接定義為兩種類型:黑鏈接匾荆、紅鏈接
黑鏈接 是2-3樹(shù)中普通的鏈接拌蜘,可以把2-3樹(shù)中的 2- 結(jié)點(diǎn) 與它的子結(jié)點(diǎn)之間的鏈當(dāng)作黑鏈接
紅鏈接 2-3樹(shù)中 3- 結(jié)點(diǎn)分解成兩個(gè) 2- 結(jié)點(diǎn),這兩個(gè) 2- 結(jié)點(diǎn)之間的鏈接就是紅鏈接
那么如何將2-3樹(shù)和紅黑樹(shù)等價(jià)起來(lái)牙丽,我們規(guī)定:紅鏈接均為左鏈接
根據(jù)上面對(duì)完美平衡的2-3樹(shù)和紅鏈接的介紹可以得出結(jié)論:沒(méi)有一個(gè)結(jié)點(diǎn)同時(shí)和兩個(gè)紅鏈接相連
根據(jù)上面對(duì)完美平衡的2-3樹(shù)和黑鏈接的介紹可以得出結(jié)論:完美平衡的2-3樹(shù)是保持完美黑色平衡的简卧,任意空鏈接到根結(jié)點(diǎn)的路徑上的黑鏈接數(shù)量相同
據(jù)此,我們可以得出3條性質(zhì):
- 紅鏈接均為左鏈接
- 沒(méi)有一個(gè)結(jié)點(diǎn)同時(shí)和兩個(gè)紅鏈接相連
- 完美平衡的2-3樹(shù)是保持完美黑色平衡的烤芦,任意空鏈接到根結(jié)點(diǎn)的路徑上的黑鏈接數(shù)量相同
在紅黑樹(shù)中举娩,沒(méi)有一個(gè)對(duì)象來(lái)表示紅鏈接和黑鏈接,通過(guò)在結(jié)點(diǎn)上加上一個(gè)屬性(color)來(lái)標(biāo)識(shí)紅鏈接還是黑鏈接构罗,color值為red表示結(jié)點(diǎn)是紅結(jié)點(diǎn)铜涉,color值為black表示結(jié)點(diǎn)是黑結(jié)點(diǎn)。
黑結(jié)點(diǎn) 2-3樹(shù)中普通的 2-結(jié)點(diǎn) 的顏色
紅結(jié)點(diǎn) 2-3樹(shù)中 3- 結(jié)點(diǎn) 分解出兩個(gè) 2-結(jié)點(diǎn) 的最小 2-結(jié)點(diǎn)
下面是2-3樹(shù)和紅黑樹(shù)的一一對(duì)應(yīng)關(guān)系圖:
紅黑樹(shù)的5個(gè)基本性質(zhì)的分析
介紹完了2-3樹(shù)和紅黑樹(shù)的對(duì)應(yīng)關(guān)系后遂唧,我們?cè)賮?lái)看下紅黑樹(shù)的5個(gè)基本性質(zhì):
- 每個(gè)結(jié)點(diǎn)要么是紅色芙代,要么是黑色
- 根結(jié)點(diǎn)是黑色
- 每個(gè)葉子結(jié)點(diǎn)(最后的空節(jié)點(diǎn))是黑色
- 如果一個(gè)結(jié)點(diǎn)是紅色的,那么他的孩子結(jié)點(diǎn)都是黑色的
- 從任意一個(gè)結(jié)點(diǎn)到葉子結(jié)點(diǎn)蠢箩,經(jīng)過(guò)的黑色結(jié)點(diǎn)是一樣的
2-3樹(shù)和紅黑樹(shù)的對(duì)應(yīng)關(guān)系后我們也就知道了紅黑樹(shù)的5個(gè)基本性質(zhì)是怎么來(lái)的了
紅黑樹(shù)的第一條性質(zhì):每個(gè)節(jié)點(diǎn)要么是紅色链蕊,要么是黑色
因?yàn)槲覀冇媒Y(jié)點(diǎn)上的屬性來(lái)表示紅鏈還是黑鏈,所以紅黑樹(shù)的結(jié)點(diǎn)要么是紅色谬泌,要么是黑色是很自然的事情
紅黑樹(shù)的第二條性質(zhì):根結(jié)點(diǎn)是黑色
紅色節(jié)點(diǎn)的情況是 3- 結(jié)點(diǎn)分解出兩個(gè) 2- 結(jié)點(diǎn)的最小節(jié)點(diǎn)是紅色,根節(jié)點(diǎn)沒(méi)有父節(jié)點(diǎn)所以只能是黑色
紅黑樹(shù)的第三條性質(zhì):每個(gè)葉子結(jié)點(diǎn)(最后的空節(jié)點(diǎn))是黑色
葉子節(jié)點(diǎn)也就是2-3樹(shù)中的空鏈逻谦,如果空鏈?zhǔn)羌t色說(shuō)明下面還是有子結(jié)點(diǎn)的掌实,但是空鏈?zhǔn)菦](méi)有子結(jié)點(diǎn)的;另一方面如果
空鏈?zhǔn)羌t色邦马,空鏈指向的父結(jié)點(diǎn)結(jié)點(diǎn)如果也是紅色就會(huì)出現(xiàn)兩個(gè)連續(xù)的紅色鏈接贱鼻,就和上面介紹的 “沒(méi)有一個(gè)結(jié)點(diǎn)同時(shí)和兩個(gè)紅鏈接相連” 相違背
紅黑樹(shù)的第四條性質(zhì):如果一個(gè)結(jié)點(diǎn)是紅色的宴卖,那么他的孩子結(jié)點(diǎn)都是黑色的
上面介紹的‘沒(méi)有一個(gè)結(jié)點(diǎn)同時(shí)和兩個(gè)紅鏈接相連’,所以一個(gè)結(jié)點(diǎn)是紅色邻悬,那么他的孩子結(jié)點(diǎn)都是黑色
紅黑樹(shù)的第五條性質(zhì):從任意一個(gè)結(jié)點(diǎn)到葉子結(jié)點(diǎn)症昏,經(jīng)過(guò)的黑色結(jié)點(diǎn)是一樣的
在介紹完美平衡的2-3樹(shù)和黑鏈接我們得出的結(jié)論:‘完美平衡的2-3樹(shù)是保持完美黑色平衡的,任意空鏈接到根結(jié)點(diǎn)的路徑上的黑鏈接數(shù)量相同’父丰, 所以從任意一個(gè)結(jié)點(diǎn)到葉子結(jié)點(diǎn)肝谭,經(jīng)過(guò)的黑色結(jié)點(diǎn)數(shù)是一樣的
紅黑樹(shù)實(shí)現(xiàn)2-3樹(shù)過(guò)程中的結(jié)點(diǎn)旋轉(zhuǎn)和顏色翻轉(zhuǎn)
顏色翻轉(zhuǎn)
為什么要顏色翻轉(zhuǎn)(flipColor)?在插入的過(guò)程中可能出現(xiàn)如下情況:兩個(gè)左右子結(jié)點(diǎn)都是紅色
根據(jù)我們上面的描述蛾扇,紅鏈只允許是左鏈(也就是左子結(jié)點(diǎn)是紅色)
所以需要進(jìn)行顏色轉(zhuǎn)換:把該結(jié)點(diǎn)的左右子結(jié)點(diǎn)設(shè)置為黑色攘烛,自己設(shè)置為黑色
private void flipColor(Node<K, V> node) {
node.color = RED;
node.left.color = BLACK;
node.right.color = BLACK;
}
左旋轉(zhuǎn)
左旋情況大致有兩種:
結(jié)點(diǎn)是右子結(jié)點(diǎn)且是紅色
顏色翻轉(zhuǎn)后,結(jié)點(diǎn)變成紅色且它是父結(jié)點(diǎn)的右子節(jié)點(diǎn)
private Node<K, V> rotateLeft(Node<K, V> node) {
Node<K, V> x = node.right;
node.right = x.left;
x.left = node;
x.color = node.color;
node.color = RED;
return x;
}
右旋轉(zhuǎn)
需要右旋的情況:連續(xù)出現(xiàn)兩個(gè)左紅色鏈接
private Node<K, V> rotateRight(Node<K, V> node) {
Node<K, V> x = node.left;
node.left = x.right;
x.right = node;
x.color = node.color;
node.color = RED;
return x;
}
紅黑樹(shù)實(shí)現(xiàn)2-3樹(shù)插入操作
通過(guò)我們上面對(duì)紅黑樹(shù)和2-3樹(shù)的介紹镀首,紅黑樹(shù)實(shí)現(xiàn)2-3樹(shù)插入操作就很簡(jiǎn)單了
只要滿足不出現(xiàn) 兩個(gè)連續(xù)左紅色鏈接坟漱、右紅色鏈接、左右都是紅色鏈接 的情況就可以了
所以僅僅需要處理三種情況即可:
- 如果出現(xiàn)右側(cè)紅色鏈接更哄,需要左旋
- 如果出現(xiàn)兩個(gè)連續(xù)的左紅色鏈接芋齿,需要右旋
- 如果結(jié)點(diǎn)的左右子鏈接都是紅色,需要顏色翻轉(zhuǎn)
private Node<K, V> _add(Node<K, V> node, K key, V value) {
//向葉子結(jié)點(diǎn)插入新結(jié)點(diǎn)
if (node == null) {
size++;
return new Node<>(key, value);
}
//二分搜索的過(guò)程
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)兩個(gè)連續(xù)的左紅色鏈接沟突,右旋
if (isRed(node.left) && isRed(node.left.left)) {
node = rotateRight(node);
}
//3,如果結(jié)點(diǎn)的左右子鏈接都是紅色,顏色翻轉(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;
}
這樣下來(lái)紅黑樹(shù)依然保持著它的五個(gè)基本性質(zhì)捕传,下面我們來(lái)對(duì)比下JDK中的TreeMap的插入操作
先按照上面的紅黑樹(shù)插入邏輯插入三個(gè)元素 [14, 5, 20]惠拭,流程如下:
使用Java TreeMap來(lái)插入上面三個(gè)元素,流程如下:
通過(guò)對(duì)比我們發(fā)現(xiàn)兩者的插入后的結(jié)果不一樣庸论,而且Java TreeMap是允許左右子結(jié)點(diǎn)都是紅色結(jié)點(diǎn)职辅!
這就和我們一直在說(shuō)的用完美平衡的2-3樹(shù)作為紅黑樹(shù)實(shí)現(xiàn)的基礎(chǔ)結(jié)構(gòu)相違背了,我們一直在強(qiáng)調(diào)不允許右節(jié)點(diǎn)是紅色聂示,也不允許兩個(gè)連續(xù)的紅色左節(jié)點(diǎn)域携,不允許左右結(jié)點(diǎn)同時(shí)是紅色
這也是《算法4》在講到紅黑樹(shù)時(shí)遵循的。但是JDK TreeMap(紅黑樹(shù))是允許右結(jié)點(diǎn)是紅色鱼喉,也允許左右結(jié)點(diǎn)同時(shí)是紅色秀鞭,Java TreeMap的紅黑樹(shù)實(shí)現(xiàn)從它的代碼注釋(From CLR)說(shuō)明它的實(shí)現(xiàn)來(lái)自《算法導(dǎo)論》
說(shuō)明《算法4》和《算法導(dǎo)論》中的所介紹的紅黑樹(shù)產(chǎn)生了一些“出入”,給我們理解紅黑樹(shù)增加了一些困惑和難度
《算法4》在介紹紅黑樹(shù)之前先給我們?cè)敿?xì)介紹了2-3樹(shù)扛禽,然后接著講到完美平衡的2-3樹(shù)和紅黑樹(shù)的對(duì)應(yīng)關(guān)系(紅黑樹(shù)就等于完美平衡的2-3樹(shù))锋边,讓我們知道紅黑樹(shù)是怎么來(lái)的,根據(jù)這些介紹你自己是可以解釋紅黑樹(shù)的的5個(gè)基本性質(zhì)為什么是這樣的编曼。
而在《算法導(dǎo)論》中介紹紅黑樹(shù)的時(shí)候沒(méi)有提及2-3樹(shù)豆巨,直接就是紅黑樹(shù)的5個(gè)基本性質(zhì),以及紅黑樹(shù)的插入掐场、刪除操作往扔,感覺(jué)對(duì)初學(xué)者是不太合適的贩猎,因?yàn)槟悴恢罏槭裁词沁@樣的,只是知道有這個(gè)五個(gè)性質(zhì)萍膛,也許這就是為什么它叫導(dǎo)論的原因吧
而且在《算法4》中作者最后好像也沒(méi)有明確的給出紅黑樹(shù)的五個(gè)基本性質(zhì)吭服,在《算法導(dǎo)論》中在紅黑樹(shù)章節(jié)一開(kāi)始就貼出了5條性質(zhì),感覺(jué)像是一種遞進(jìn)和升華
這兩本書(shū)除了對(duì)紅黑樹(shù)講解的方式存在差異外蝗罗,我們還發(fā)現(xiàn)《算法4》和《算法導(dǎo)論》在紅黑樹(shù)的實(shí)現(xiàn)上也是有差異的艇棕,就如我們上面插入三個(gè)元素 [14, 5, 20] 產(chǎn)生不同的結(jié)果
在解釋這些差異之前,我們?cè)賮?lái)看些2-3-4樹(shù)绿饵,上面提到完美平衡的2-3樹(shù)和紅黑樹(shù)等價(jià)欠肾,更準(zhǔn)確的說(shuō)是2-3-4樹(shù)和紅黑樹(shù)等價(jià)
2-3-4樹(shù)
2-3-4樹(shù) 和 2-3樹(shù) 非常相像。2-3樹(shù)允許存在 2- 結(jié)點(diǎn) 和 3- 結(jié)點(diǎn)拟赊,類似的2-3-4樹(shù)允許存在 2- 結(jié)點(diǎn)刺桃、3- 結(jié)點(diǎn) 和 4- 結(jié)點(diǎn)
向2-結(jié)點(diǎn)、3-結(jié)點(diǎn)插入元素
向2-結(jié)點(diǎn)插入元素吸祟,這個(gè)和上面介紹的2-3樹(shù)是一樣的瑟慈,在這里就不敘述了
向3-結(jié)點(diǎn)插入元素,形成一個(gè)4-結(jié)點(diǎn)屋匕,因?yàn)?-3-4樹(shù)允許4-結(jié)點(diǎn)的存在葛碧,所以不需要向上分解
向4-結(jié)點(diǎn)插入元素
向4-結(jié)點(diǎn)插入元素,需要分解4-結(jié)點(diǎn)过吻, 因?yàn)?-3-4樹(shù)最多只允許存在4-結(jié)點(diǎn)进泼,如:
如果待插入的4-結(jié)點(diǎn),它的父結(jié)點(diǎn)也是一個(gè)4-結(jié)點(diǎn)呢纤虽?如下圖的2-3-4樹(shù)插入結(jié)點(diǎn)K:
主要有兩個(gè)方案:
- Bayer于1972年提出的方案:使用相同的辦法去分解父結(jié)點(diǎn)的4-結(jié)點(diǎn)乳绕,直到不需要分解為止,方向是自底向上
- Guibas 和 Sedgewick于1978年提出的方案:自上而下的方式逼纸,也就是在二分搜索的過(guò)程洋措,一旦遇到4-結(jié)點(diǎn)就分解它,這樣在最終插入的時(shí)候永遠(yuǎn)不會(huì)有父結(jié)點(diǎn)是4-結(jié)點(diǎn)的情況
Bayer全名叫做Rudolf Bayer(魯?shù)婪颉ぐ轄枺?/strong>杰刽,他在1972年發(fā)明的 對(duì)稱二叉B樹(shù)(symmetric binary B-tree) 就是 紅黑樹(shù)(red black tree) 的前身菠发。
紅黑樹(shù) 這個(gè)名字是由 Leo J. Guibas 和 Robert Sedgewick 于1978年的一篇論文中提出來(lái)的,
對(duì)該論文感興趣的可以查看這個(gè)鏈接:http://professor.ufabc.edu.br/~jesus.mena/courses/mc3305-2q-2015/AED2-13-redblack-paper.pdf
下面的圖就是 自上而下 方案的流程圖
2-3-4樹(shù)和紅黑樹(shù)的等價(jià)關(guān)系
在介紹2-3樹(shù)的時(shí)候我們也講解了2-3樹(shù)和紅黑樹(shù)的等價(jià)關(guān)系贺嫂,由于2-3樹(shù)和2-3-4樹(shù)非常類似滓鸠,所以2-3-4樹(shù)和紅黑樹(shù)的等價(jià)關(guān)系也是類似的。不同的是2-3-4的 4-結(jié)點(diǎn) 分解后的結(jié)點(diǎn)顏色變成如下形式:
所以可以得出下面一棵 2-3-4 樹(shù)和紅黑樹(shù)的等價(jià)關(guān)系圖:
上面在介紹紅黑樹(shù)實(shí)現(xiàn)2-3樹(shù)的時(shí)候講解了它的插入操作:
private Node<K, V> _add(Node<K, V> node, K key, V value) {
//向葉子結(jié)點(diǎn)插入新結(jié)點(diǎn)
if (node == null) {
size++;
return new Node<>(key, value);
}
//二分搜索的過(guò)程
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)兩個(gè)連續(xù)的左紅色鏈接哥力,右旋
if (isRed(node.left) && isRed(node.left.left)) {
node = rotateRight(node);
}
//3,如果結(jié)點(diǎn)的左右子鏈接都是紅色,顏色翻轉(zhuǎn)
if (isRed(node.left) && isRed(node.right)) {
flipColor(node);
}
}
我們可以很輕松的把它改成 2-3-4 的插入邏輯(只需要把顏色翻轉(zhuǎn)的邏輯提到二分搜索的前面即可):
private Node<K, V> _add(Node<K, V> node, K key, V value) {
//向葉子結(jié)點(diǎn)插入新結(jié)點(diǎn)
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);
}
//二分搜索的過(guò)程
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ù)插入數(shù)據(jù) [E,C,G,B,D,F,J,A]
RB2_3_4Tree<Character, Character> rbTree = 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ù)插入數(shù)據(jù) [E,C,G,B,D,F,J,A]
RBTree<Character, Character> 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樹(shù) 和 2-3樹(shù) 插入結(jié)果的對(duì)比圖:
所以我們一開(kāi)始用紅黑樹(shù)實(shí)現(xiàn)完美平衡的 2-3 樹(shù)墩弯,左右結(jié)點(diǎn)是不會(huì)都是紅色的
現(xiàn)在用紅黑樹(shù)實(shí)現(xiàn) 2-3-4 樹(shù)吩跋,左右結(jié)點(diǎn)的可以同時(shí)是紅色的,這樣的紅黑樹(shù)效率更高渔工。因?yàn)槿绻龅阶笥医Y(jié)點(diǎn)是紅色锌钮,就進(jìn)行顏色翻轉(zhuǎn),還需要對(duì)紅色的父結(jié)點(diǎn)進(jìn)行向上回溯引矩,因?yàn)楦附Y(jié)點(diǎn)染成紅色了梁丘,可能父結(jié)點(diǎn)的父結(jié)點(diǎn)也是紅色,可能需要進(jìn)行結(jié)點(diǎn)旋轉(zhuǎn)或者顏色翻轉(zhuǎn)操作旺韭,所以說(shuō) 2-3-4 樹(shù)式的紅黑樹(shù)效率更高氛谜。
所以回到上面我們提到《算法4》和《算法導(dǎo)論》在實(shí)現(xiàn)上的差異的問(wèn)題,就很好回答了区端,因?yàn)椤端惴?》是用紅黑樹(shù)實(shí)現(xiàn)2-3樹(shù)的值漫,并不是 2-3-4 樹(shù)。但是如果是用紅黑樹(shù)實(shí)現(xiàn) 2-3-4 樹(shù)就和《算法導(dǎo)論》上介紹的紅黑樹(shù)一樣嗎织盼?不一樣杨何。
下面繼續(xù)做一個(gè)測(cè)試,分別往上面紅黑樹(shù)實(shí)現(xiàn)的 2-3-4樹(shù) 和 JDK TreeMap 中插入 [E, D, R, O, S, X]
雖然兩棵樹(shù)都是紅黑樹(shù)沥邻,但是卻不一樣危虱。并且TreeMap允許右節(jié)點(diǎn)是紅色,在2-3-4樹(shù)中最多是左右子結(jié)點(diǎn)同時(shí)是紅色的情況唐全,不會(huì)出現(xiàn)左結(jié)點(diǎn)是黑色埃跷,右邊的兄弟結(jié)點(diǎn)是紅色的情況,為什么會(huì)有這樣的差異呢邮利?
從上面的2-3-4樹(shù)的插入邏輯可以看出弥雹,如果右節(jié)點(diǎn)是紅色會(huì)執(zhí)行左旋轉(zhuǎn)操作,所以不會(huì)出現(xiàn)單獨(dú)紅右結(jié)點(diǎn)的情況
也就是說(shuō)只會(huì)出現(xiàn)單獨(dú)的左結(jié)點(diǎn)是紅色的情況近弟,我們把這種形式的紅黑樹(shù)稱之為左傾紅黑樹(shù)(Left Leaning Red Black Tree)缅糟,包括上面的紅黑樹(shù)實(shí)現(xiàn)的完美平衡的2-3樹(shù)也是左傾紅黑樹(shù)
為什么在《算法4》中,作者規(guī)定所有的紅色鏈接都是左鏈接祷愉,這只是人為的規(guī)定窗宦,當(dāng)然也可以是右鏈接,規(guī)定紅鏈接都是左鏈二鳄,可以使用更少的代碼來(lái)實(shí)現(xiàn)黑色平衡赴涵,需要考慮的情況會(huì)更少,就如上面我們介紹的插入操作订讼,我們只需要考慮3中情況即可髓窜。
但是一般意義上的紅黑樹(shù)是不需要維持紅色左傾的這個(gè)性質(zhì)的,所以為什么TreeMap是允許單獨(dú)右紅結(jié)點(diǎn)的
如果還需要維護(hù)左傾情況,這樣的話就更多的操作寄纵,可能還需要結(jié)點(diǎn)旋轉(zhuǎn)和顏色的翻轉(zhuǎn)鳖敷,性能更差一些,雖然也是符合紅黑樹(shù)的性質(zhì)
介紹完了《算法4》上的紅黑樹(shù)程拭,下面就來(lái)分析下一般意義上的紅黑樹(shù)的 插入 和 刪除 操作定踱,也就是《算法導(dǎo)論》上介紹的紅黑樹(shù)。
紅黑樹(shù)插入操作
插入操作有兩種情況是非常簡(jiǎn)單的恃鞋,所以在這里單獨(dú)說(shuō)一下:
case 1. 如果插入的結(jié)點(diǎn)是根結(jié)點(diǎn)崖媚,直接把該結(jié)點(diǎn)設(shè)置為黑色,整個(gè)插入操作結(jié)束
如下圖所示:
case 2. 如果插入的結(jié)點(diǎn)的父結(jié)點(diǎn)是黑色恤浪,也無(wú)需調(diào)整畅哑,整個(gè)插入操作結(jié)束
如下圖所示:
下面開(kāi)始介紹比較復(fù)雜的情況
紅黑樹(shù)插入操作,我們只需要處理父結(jié)點(diǎn)是紅色的情況水由,因?yàn)橐婚_(kāi)始紅黑樹(shù)肯定是黑色平衡的荠呐,就是因?yàn)橥~子節(jié)點(diǎn)插入元素后可能出現(xiàn)兩個(gè)連續(xù)的紅色的結(jié)點(diǎn)
需要注意的是,我們把新插入的結(jié)點(diǎn)默認(rèn)設(shè)置為紅色绷杜,初始的時(shí)候直秆,正在處理的節(jié)點(diǎn)就是插入的結(jié)點(diǎn),在不斷調(diào)整的過(guò)程中鞭盟,正在處理的節(jié)點(diǎn)會(huì)不斷的變化圾结,且叔叔、爺爺齿诉、父結(jié)點(diǎn)都是相對(duì)于當(dāng)前正在處理的結(jié)點(diǎn)來(lái)說(shuō)的
case 3. 叔叔結(jié)點(diǎn)為紅色筝野,正在處理的節(jié)點(diǎn)可以是左也可以是右結(jié)點(diǎn)
調(diào)整策略:由于父結(jié)點(diǎn)是紅色,叔叔結(jié)點(diǎn)是紅色粤剧,爺爺結(jié)點(diǎn)是黑色歇竟,執(zhí)行顏色翻轉(zhuǎn)操作
然后把當(dāng)前正在處理的結(jié)點(diǎn)設(shè)置為爺爺結(jié)點(diǎn),如果爺爺?shù)母附Y(jié)點(diǎn)是黑色插入操作結(jié)束抵恋,如果是紅色繼續(xù)處理
case 4. 叔叔結(jié)點(diǎn)為黑色焕议,正在處理的結(jié)點(diǎn)是右結(jié)點(diǎn)
調(diào)整策略:由于父結(jié)點(diǎn)是紅色,叔叔結(jié)點(diǎn)為黑色弧关,那么爺爺結(jié)點(diǎn)肯定是黑色
把正在處理的節(jié)點(diǎn)設(shè)置為父結(jié)點(diǎn)盅安,然后左旋,形成Case5情況
case 5. 叔叔結(jié)點(diǎn)為黑色世囊,正在處理的結(jié)點(diǎn)是左孩子
調(diào)整策略:由于父結(jié)點(diǎn)是紅色别瞭,叔叔結(jié)點(diǎn)為黑色,那么爺爺結(jié)點(diǎn)肯定是黑色
把父結(jié)點(diǎn)染黑株憾,爺爺結(jié)點(diǎn)染紅蝙寨,然后爺爺結(jié)點(diǎn)右旋
Case3晒衩、Case4、Case5如果單獨(dú)來(lái)理解的話比較困難墙歪,就算單獨(dú)為每一個(gè)Case畫(huà)圖听系,我覺(jué)得也很難完整的理解,很多博客上都是這種方式箱亿,感覺(jué)不太好理解跛锌。我將這三種情況通過(guò)一張流程圖串聯(lián)起來(lái)弃秆,將這三個(gè)Case形成一個(gè)整體届惋,藍(lán)色箭頭表示正在處理的結(jié)點(diǎn),如下所示:
紅黑樹(shù)刪除操作
上面介紹完了紅黑樹(shù)的插入操作菠赚,接下來(lái)看下紅黑樹(shù)的刪除操作
紅黑樹(shù)的刪除操作比插入操作更加復(fù)雜一些
為了描述方便脑豹,我們把正在處理的結(jié)點(diǎn)稱之為 X,父結(jié)點(diǎn)為 P(Parent)衡查,兄弟節(jié)點(diǎn)稱之為 S(Sibling)瘩欺,左侄子稱之為 LN(Left Nephew),右侄子稱之為 RN(Right Nephew)
如果刪除的結(jié)點(diǎn)是黑色拌牲,那么就導(dǎo)致本來(lái)保持黑平衡的紅黑樹(shù)失衡了俱饿,從下圖可以看出結(jié)點(diǎn)P到左子樹(shù)的葉子結(jié)點(diǎn)經(jīng)過(guò)的黑節(jié)點(diǎn)數(shù)量為 4(2+2)
,到右子樹(shù)的葉子節(jié)點(diǎn)經(jīng)過(guò)的黑色節(jié)點(diǎn)數(shù)量是 5(2+3)
塌忽,如下圖所示:
紅黑樹(shù)的刪除操作拍埠,如果刪除的是黑色會(huì)導(dǎo)致紅黑樹(shù)就不能保持黑色平衡了,需要進(jìn)行調(diào)整了土居;
如果刪除的是紅色枣购,那么就無(wú)需調(diào)整,直接刪除即可擦耀,因?yàn)闆](méi)有沒(méi)有破壞黑色平衡
刪除結(jié)點(diǎn)后棉圈,無(wú)需調(diào)整的情況
case 1 刪除的結(jié)點(diǎn)是紅色結(jié)點(diǎn),直接刪除即可
case 2 刪除的節(jié)點(diǎn)是黑色眷蜓,如果當(dāng)前處理的節(jié)點(diǎn)X是根結(jié)點(diǎn)
無(wú)論根結(jié)點(diǎn)是什么顏色分瘾,都將根結(jié)點(diǎn)設(shè)置為黑色
case 3 刪除的結(jié)點(diǎn)是黑色,如果當(dāng)前處理的結(jié)點(diǎn)是紅色結(jié)點(diǎn)吁系,將該結(jié)點(diǎn)設(shè)置為黑色
因?yàn)閯h除黑色結(jié)點(diǎn)后德召,就打破了黑色平衡,黑高少了1
所以把一個(gè)紅色節(jié)點(diǎn)設(shè)置為黑色垮抗,這樣黑高又平衡了
刪除節(jié)點(diǎn)后氏捞,需要調(diào)整的情況
正在處理的結(jié)點(diǎn)為X,要?jiǎng)h除的結(jié)點(diǎn)是左結(jié)點(diǎn)冒版,分為4中情況:
case 4 兄弟結(jié)點(diǎn)為紅色
調(diào)整方案:兄弟設(shè)置為黑色液茎,父結(jié)點(diǎn)設(shè)置為紅色,父結(jié)點(diǎn)進(jìn)行左旋轉(zhuǎn)
轉(zhuǎn)化為 case5、case6捆等、case7
case 5 兄弟結(jié)點(diǎn)為黑色滞造,左侄子LN為黑色,右侄子RN為黑色
在這種條件下栋烤,還有兩種情況:父結(jié)點(diǎn)是紅色或黑色谒养,不管是那種情況,調(diào)整方案都是一致的
調(diào)整方案:將兄弟結(jié)點(diǎn)設(shè)置為紅色明郭,把當(dāng)前處理的結(jié)點(diǎn)設(shè)置為父結(jié)P
case 6 兄弟結(jié)點(diǎn)為黑色买窟,左侄子為紅色,右侄子RN為黑色
調(diào)整方案:將左侄子結(jié)點(diǎn)設(shè)置為黑色薯定,兄弟結(jié)點(diǎn)設(shè)置為紅色始绍,兄弟結(jié)點(diǎn)右旋轉(zhuǎn),這樣就轉(zhuǎn)化成了case7
case 7 兄弟結(jié)點(diǎn)為黑色话侄,左侄子不管紅黑亏推,右侄子為紅色
處理方式:兄弟結(jié)點(diǎn)變成父結(jié)點(diǎn)的顏色,然后父結(jié)點(diǎn)設(shè)置黑色年堆,右侄子設(shè)置黑色吞杭,父結(jié)點(diǎn)進(jìn)行左旋轉(zhuǎn)
和插入操作一樣,下面通過(guò)一張流程圖把刪除需要調(diào)整的情況串聯(lián)起來(lái):
上面處理的所有情況都是基于正在處理的結(jié)點(diǎn)是左結(jié)點(diǎn)
如果要調(diào)整正在處理的結(jié)點(diǎn)是右節(jié)點(diǎn)的情況变丧,就是上面的處理的鏡像芽狗。插入操作也是同理,所以就省略了
Java TreeMap锄贷、TreeSet源碼分析
TreeMap底層就是用紅黑樹(shù)實(shí)現(xiàn)的译蒂,它在插入后調(diào)整操作主要在fixAfterInsertion方法里,我為每種情況都添加注釋谊却,如下所示:
/** From CLR */
private void fixAfterInsertion(Entry<K,V> x) {
x.color = RED;
while (x != null && x != root && x.parent.color == RED) {
if (parentOf(x) == leftOf(parentOf(parentOf(x)))) {
Entry<K,V> 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(Entry<K,V> x) {
while (x != root && colorOf(x) == BLACK) {
if (x == leftOf(parentOf(x))) {
Entry<K,V> 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 來(lái)實(shí)現(xiàn)的柔昼,往TreeSet添加進(jìn)的元素當(dāng)作TreeMap的key,TreeMap的value是一個(gè)常量Object炎辨。掌握了紅黑樹(shù)捕透,對(duì)于這兩個(gè)集合的原理就不難理解了。
最后
本文從一開(kāi)始講的2-3樹(shù)和紅黑樹(shù)的對(duì)應(yīng)關(guān)系碴萧,再到2-3-4樹(shù)和紅黑樹(shù)的對(duì)應(yīng)關(guān)系乙嘀,再到《算法4》和《算法導(dǎo)論》JDK TreeMap在紅黑樹(shù)上的差異
然后詳細(xì)介紹了紅黑樹(shù)的插入、刪除操作破喻,最后分析了下Java中的TreeMap和TreeSet集合類虎谢。
人生當(dāng)如紅黑樹(shù),當(dāng)過(guò)于自喜或過(guò)于自卑的時(shí)候曹质,應(yīng)當(dāng)自我調(diào)整婴噩,尋求平衡擎场。
我很丑,紅黑樹(shù)卻很美几莽。 希望本文對(duì)你有 些許幫助迅办。
參考資料:
- https://www.cs.princeton.edu/~rs/talks/LLRB/RedBlack.pdf
- http://professor.ufabc.edu.br/~jesus.mena/courses/mc3305-2q-2015/AED2-13-redblack-paper.pdf
- 《算法4》、《算法導(dǎo)論》