了解 Tree 之前我們必須了解 紅黑樹 因為Tree 的數據結構就是紅黑樹
-
紅黑樹的特性
- (1)每個節點或者是黑色,或者是紅色。
- (2)根節點是黑色。
- (3)每個葉子節點(NIL)是黑色。 [注意:這里葉子節點,是指為空(NIL或NULL)的葉子節點!]
- (4)如果一個節點是紅色的,則它的子節點必須是黑色的。
- (5)從一個節點到該節點的子孫節點的所有路徑上包含相同數目的黑節點。
-
注意:
- (01) 特性(3)中的葉子節點,是只為空(NIL或null)的節點。
- (02) 特性(5),確保沒有一條路徑會比其他路徑長出倆倍。因而,紅黑樹是相對是接近平衡的二叉樹。
當我們 進行插入 刪除的 我們必定會改變紅黑樹的 規則 而,我們的措施就是調整策略就是
- 1.改變某個節點的顏色使之符合規則
- 2.改變樹的結構關系 進行左旋或者有旋
紅黑樹的基本操作(一) 左旋和右旋
java 版本左旋代碼(TreeMap 中的)
左旋的過程是將 E 的右子樹繞E逆時針旋轉,使得 E 的右子樹成為x的父親,同時修改相關節點的引用。旋轉之后,二叉查找樹的屬性仍然滿足。
//傳入 動圖中的 E
private void rotateLeft(Entry<K,V> p) {
if (p != null) {
//E的右子樹
Entry<K,V> r = p.right;
// E的 右子樹改變 改變成為S 的左子樹
p.right = r.left;
//判斷S 的左子樹是否空 不為空改變 S 的左子樹的parent 為E
if (r.left != null)
r.left.parent = p;
//使S成為主節點
r.parent = p.parent;
//判斷 是否原先的父節點為空 為空的話 S為父節點
if (p.parent == null)
root = r;
// 判斷 E到底是 父節點的左節點還是右節點 將父節點的左(右)節點編程S節點
else if (p.parent.left == p)
p.parent.left = r;
else
p.parent.right = r;
//將S的左節點 設為 E
r.left = p;
//E的parent 節點 設為S
p.parent = r;
}
}
基本和左旋代碼保持一致
/** 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;
}
}
紅黑樹的基本操作 查(Tree get())
插入回導致紅黑樹的性質發生改變 有以下幾種情況
public V get(Object key) {
Entry<K,V> p = getEntry(key);
return (p==null ? null : p.value);
}
--
final Entry<K,V> getEntry(Object key) {
// Offload comparator-based version for sake of performance
// comparator 這個是 個成員變量 外部設置特定的 比較器 有就用這個 這個變量 可以初始化的時候 放進去
if (comparator != null)
return getEntryUsingComparator(key);
if (key == null)
throw new NullPointerException();
@SuppressWarnings("unchecked")
Comparable<? super K> k = (Comparable<? super K>) key;
Entry<K,V> p = root;
// 利用比較器的特性開始比較大小 相同 return 小于 從左子樹開始 大了 從右子樹開始
while (p != null) {
int cmp = k.compareTo(p.key);
if (cmp < 0)
p = p.left;
else if (cmp > 0)
p = p.right;
else
return p;
}
return null;
}
紅黑樹的基本操作(二) 添加 put
-
第一步: 將紅黑樹當作一顆二叉查找樹,將節點插入。
紅黑樹本身就是一顆二叉查找樹,將節點插入后,該樹仍然是一顆二叉查找樹。 也就意味著,樹的鍵值仍然是有序的。 此外,無論是左旋還是右旋,若旋轉之前這棵樹是二叉查找樹,旋轉之后它一定還是二叉查找樹。 這也就意味著,任何的旋轉和重新著色操作,都不會改變它仍然是一顆二叉查找樹的事實。 好吧?那接下來,我們就來想方設法的旋轉以及重新著色,使這顆樹重新成為紅黑樹!
-
第二步:將插入的節點著色為"紅色"。
將插入的節點著色為紅色,不會違背"特性(5)"
選項 | 現象說明 | 問題分析 | 處理策略 |
---|---|---|---|
1 | 空樹中插入根節點 | 初始插入的節點均為紅色,因此簡單將紅色重涂為黑色即可。 | |
2 | 插入節點的父節點是黑色 | 插入的紅色節點,未違反任何性質 無需調整。 | |
3 | 當前節點的父節點是紅色,且叔叔節點(祖父節點的另一個子節點)也是紅色。 | 此時祖父節點一定存在,否則插入前就已不是紅黑樹。與此同時,又分為父節點是祖父節點的左子還是右子,由于對稱性,我們只要解開一個方向就可以了。在此,我們只考慮父節點為祖父左子的情況。同時,還可以分為當前結點是其父結點的左子還是右子,但是處理方式是一樣的。我們將此歸為同一類。 |
(01) 將“父節點”設為黑色. (02) 將“叔叔節點”設為黑色。 (03) 將“祖父節點”設為“紅色”。 (04) 將“祖父節點”設為“當前節點”(紅色節點); 之后繼續對“當前節點”進行操作。 |
4 | 當前節點的父節點是紅色,叔叔節點是黑色,當前節點是右子節點。 |
(01) 將“父節點”作為“新的當前節點”。 (02) 以“新的當前節點”為支點進行左旋。
|
|
5 | 當前節點的父節點是紅色,叔叔節點是黑色,當前節點是左子節點。 |
(01) 將“父節點”設為“黑色”。 (02) 將“祖父節點”設為“紅色”。 (03) 以“祖父節點”為支點進行右旋。
|
情況三:
有情況三的圖我們可以看到當前節點7的父節點也為紅色,出現父子節點都為紅色的情況,且叔叔節點為黑色,
因此適用于情況4:當前節點的父節點是紅色,叔叔節點是黑色,當前節點是右子節點,
那么按照情況4的恢復策略,進行新一輪的旋轉或涂色,如下看情況4如何進行調整。
情況四:
這里作的操作為:當前節點由原來的7變換為其父節點2,以新的當前節點2,作左旋操作.
如上圖。操作完成后,發現父子節點仍都是紅色,繼續進行旋轉或涂色。
這里適用于情況5:當前節點的父節點是紅色,叔叔節點是黑色,當前節點是左子節點來進行再次調整,請看下面的情況5如何進行調整。
下面談談為什么要這樣處理。(建議理解的時候,通過下面的圖進行對比)
首先,將“父節點”作為“新的當前節點”;接著,以“新的當前節點”為支點進行左旋。 為了便于理解,我們先說明第(02)步,再說明第(01)步;為了便于說明,我們設置“父節點”的代號為F(Father),“當前節點”的代號為S(Son)。
為什么要“以F為支點進行左旋”呢?根據已知條件可知:S是F的右孩子。而之前我們說過,我們處理紅黑樹的核心思想:
將紅色的節點移到根節點;然后,將根節點設為黑色。
既然是“將紅色的節點移到根節點”,那就是說要不斷的將破壞紅黑樹特性的紅色節點上移(即向根方向移動)。
而S又是一個右孩子,因此,我們可以通過“左旋”來將S上移!
按照上面的步驟(以F為支點進行左旋)處理之后:若S變成了根節點,那么直接將其設為“黑色”,就完全解決問題了;
若S不是根節點,那我們需要執行步驟(01),即“將F設為‘新的當前節點’”。
那為什么不繼續以S為新的當前節點繼續處理,而需要以F為新的當前節點來進行處理呢?
這是因為“左旋”之后,F變成了S的“子節點”,即S變成了F的父節點;
而我們處理問題的時候,需要從下至上(由葉到根)方向進行處理;
也就是說,必須先解決“孩子”的問題,再解決“父親”的問題;
所以,我們執行步驟(01):將“父節點”作為“新的當前節點”。
情況五:
下面談談為什么要這樣處理。(建議理解的時候,通過下面的圖進行對比)
為了便于說明,我們設置“當前節點”為S(OriginalSon),“兄弟節點”為B(Brother),
“叔叔節點”為U(Uncle),“父節點”為F(Father),祖父節點為G(Grand-Father)。
S和F都是紅色,違背了紅黑樹的“特性(4)”,我們可以將F由“紅色”變為“黑色”,就解決了“違背‘特性(4)’”的問題;
但卻引起了其它問題:違背特性(5),因為將F由紅色改為黑色之后,所有經過F的分支的黑色節點的個數增加了1。
那我們如何解決“所有經過F的分支的黑色節點的個數增加了1”的問題呢? 我們可以通過“將G由黑色變成紅色”,同時“以G為支點進行右旋”來解決。
此時,樹已經滿足紅黑樹的性質,如果仍不滿足,則仍按照情況1——情況5的方式進行旋轉和重新涂色。
put代碼
public V put(K key, V value) {
Entry<K,V> t = root;
//根節點為空
if (t == null) {
compare(key, key); // type (and possibly null) check
root = new Entry<>(key, value, null);
size = 1;
modCount++;
return null;
}
int cmp;
Entry<K,V> parent;
// split comparator and comparable paths
Comparator<? super K> cpr = comparator; //設置的比較器
// 如果比較器不為空 按照 設置的比較器進行比較 如果為空 按照 key 實現的比較器方法進行比較
if (cpr != null) {
do {
parent = t;
cmp = cpr.compare(key, t.key);
//小于走左樹
if (cmp < 0)
t = t.left;
//大于走右樹
else if (cmp > 0)
t = t.right;
else //相同的話替換掉 value 的值
return t.setValue(value);
} while (t != null);
}
else {//和上面一樣的 邏輯 只不過 比較器的代碼 換了一下
if (key == null)
throw new NullPointerException();
@SuppressWarnings("unchecked")
Comparable<? super K> k = (Comparable<? super K>) key;
do {
parent = t;
cmp = k.compareTo(t.key);
if (cmp < 0)
t = t.left;
else if (cmp > 0)
t = t.right;
else
return t.setValue(value);
} while (t != null);
}
// 沒有在樹上找到 new 出一個entry 根據最后比較結果 設置這個是在左樹 還是右樹
Entry<K,V> e = new Entry<>(key, value, parent);
if (cmp < 0)
parent.left = e;
else
parent.right = e;
// fix 就是 修正二叉樹
fixAfterInsertion(e);
size++;// 增加 size
modCount++; // 返回修改次數
return null;
}
修正二叉樹結構
/** 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)))) {
// 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)); // 改變當前節點位置 將“祖父節點”設為“當前節點
} else { // 情況四 或 五 叔叔是黑色,
if (x == rightOf(parentOf(x))) { //情況四
x = parentOf(x);
rotateLeft(x); //左旋轉
}
setColor(parentOf(x), BLACK);
setColor(parentOf(parentOf(x)), RED);
rotateRight(parentOf(parentOf(x)));
}
} else {
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));
} 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.color = BLACK;
}
尋找節點后繼(樹中比大于 當前節點 的最小的那個元素)
t的右子樹不空,則t的后繼是其右子樹中最小的那個元素。
t的右孩子為空,則t的后繼是其第一個向左走的祖先。
為什么尋找中序后繼節點? 后繼節點在紅黑樹的刪除操作中將會用到。
static <K,V> TreeMap.Entry<K,V> successor(Entry<K,V> t) {
if (t == null)
return null;
// 如果right 不為空 往左
else if (t.right != null) {
Entry<K,V> p = t.right;
// while 循環找到中序后繼結點 一直往左找
while (p.left != null)
p = p.left;
return p;
} else {
Entry<K,V> p = t.parent;
Entry<K,V> ch = t;
// while 循環找到中序后繼結點 一直往右找
while (p != null && ch == p.right) {
ch = p;
p = p.parent;
}
return p;
}
}
Remove
-
第一步:將紅黑樹當作一顆二叉查找樹,將節點刪除。
這和"刪除常規二叉查找樹中刪除節點的方法是一樣的"。分3種情況:- ① 被刪除節點沒有兒子,即為葉節點。那么,直接將該節點刪除就OK了。
- ② 被刪除節點只有一個兒子。那么,直接刪除該節點,并用該節點的唯一子節點頂替它的位置。
- ③ 被刪除節點有兩個兒子。那么,先找出它的后繼節點;然后把“它的后繼節點的內容”復制給“該節點的內容”;之后,刪除“它的后繼節點”。在這里,后繼節點相當于替身,在將后繼節點的內容復制給"被刪除節點"之后,再將后繼節點刪除。這樣就巧妙的將問題轉換為"刪除后繼節點"的情況了,下面就考慮后繼節點。 在"被刪除節點"有兩個非空子節點的情況下,它的后繼節點不可能是雙子非空。既然"的后繼節點"不可能雙子都非空,就意味著"該節點的后繼節點"要么沒有兒子,要么只有一個兒子。若沒有兒子,則按"情況① "進行處理;若只有一個兒子,則按"情況② "進行處理。
第二步:通過"旋轉和重新著色"等一系列來修正該樹,使之重新成為一棵紅黑樹。
因為"第一步"中刪除節點之后,可能會違背紅黑樹的特性。所以需要通過"旋轉和重新著色"來修正該樹,使之重新成為一棵紅黑樹。
public V remove(Object key) {
Entry<K,V> p = getEntry(key);
if (p == null)
return null;
V oldValue = p.value;
deleteEntry(p);
//彈出舊值
return oldValue;
}
private void deleteEntry(Entry<K,V> p) {
modCount++;
size--;
// 刪除點p的左右子樹都非空
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
if (p.color == BLACK)
fixAfterDeletion(replacement);
} else if (p.parent == null) { // 只有一個節點
root = null;
} else { //左右子樹 都為空
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;
}
}
}
// todo 關于刪除 跟多解釋可以查看參考文章
TreeSet
TreeSet是對TreeMap的簡單包裝,對TreeSet的函數調用都會轉換成合適的TreeMap方法,因此TreeSet的實現非常簡單。和實現LinkedSet 的方式 一樣這里不再贅述。
public class TreeSet<E> extends AbstractSet<E>
implements NavigableSet<E>, Cloneable, java.io.Serializable
{
private transient NavigableMap<E,Object> m;
public TreeSet() {
this(new TreeMap<E,Object>());
}
TreeSet(NavigableMap<E,Object> m) {
this.m = m;
}
public int size() {
return m.size();
}
public boolean isEmpty() {
return m.isEmpty();
}
public boolean remove(Object o) {
return m.remove(o)==PRESENT;
}
public boolean add(E e) {
return m.put(e, PRESENT)==null;
}
.........
.........
}
總結
看了TreeMap 其實我首先是不太愿意分析整的 ,為什么呢? 因為我數據結構學的不太好
特別是 紅黑樹 在我理解里面 屬于高級數據結構了 這篇文章 距離 我上篇文章就開始寫了 寫了三四天 也看了很多 文章 自己也對紅黑樹加深了了解 O(logn) 這也算是給HashMap 給了個尾巴
因為HashMap 到八個就會鏈表轉 紅黑樹 那時候就想分析來著,剛還TreeMap 就是紅黑樹 一起分析了 ,TreeMap從源碼因為注釋
也看出了 源碼也是參考過其他代碼的 其實 并不難 我們只要一句句分析 結合紅黑樹特點 我們也能看懂
(看代碼的時候我就在想 我什么時候能寫出這種 代碼 臥槽還有這種操作 之類的 )
到此 java 集合框架 1.8 的源碼 我們基本上看了一遍
基本上 主要由 這幾種 數據結構 鏈表 數組 Hash表 紅黑樹
這些數據結構組合成為我們高可用的容器框架
關于 下一階段的 寫作計劃 我可能 會寫一個系列的 并發 (同時自己也深入了解) spring 框架也可能會寫一系列 還有 日常bug 總結