TreeMap的原理
一、 數據結構
源碼定義如下
public class TreeMap<K,V>
extends AbstractMap<K,V>
implements NavigableMap<K,V>, Cloneable, java.io.Serializable
TreeMap繼承AbstractMap,實現NavigableMap、Cloneable、Serializable三個接口。其中AbstractMap表明TreeMap爲一個Map即支持key-value的集合, NavigableMap則意味着它支持一系列的導航方法,具備針對給定搜索目標返回最接近匹配項的導航方法 。
TreeMap中同時也包含了如下幾個重要的屬性:
//比較器,因爲TreeMap是有序的,通過comparator接口我們可以對TreeMap的內部排序進行精密的控制
private final Comparator<? super K> comparator;
//TreeMap紅-黑節點,爲TreeMap的內部類
private transient Entry<K,V> root = null;
//容器大小
private transient int size = 0;
//TreeMap修改次數
private transient int modCount = 0;
//紅黑樹的節點顏色--紅色
private static final boolean RED = false;
//紅黑樹的節點顏色--黑色
private static final boolean BLACK = true;
對於葉子節點Entry是TreeMap的內部類,它有幾個重要的屬性:
//鍵
K key;
//值
V value;
//左孩子
Entry<K,V> left = null;
//右孩子
Entry<K,V> right = null;
//父親
Entry<K,V> parent;
//顏色
boolean color = BLACK;
二、存儲
先看源碼
public V put(K key, V value) {
Entry<K,V> t = root;
/**
* 如果根節點都爲null,還沒建立起來紅黑樹,我們先new Entry並賦值給root把紅黑樹建立起來,這個時候紅
* 黑樹中已經有一個節點了,同時修改操作+1。
*/
if (t == null) {
compare(key, key);
root = new Entry<>(key, value, null);
size = 1;
modCount++;
return null;
}
/**
* 如果節點不爲null,定義一個cmp,這個變量用來進行二分查找時的比較;定義parent,是new Entry時必須
* 要的參數
*/
int cmp;
Entry<K,V> parent;
// cpr表示有無自己定義的排序規則,分兩種情況遍歷執行
Comparator<? super K> cpr = comparator;
if (cpr != null) {
/**
* 從root節點開始遍歷,通過二分查找逐步向下找
* 第一次循環:從根節點開始,這個時候parent就是根節點,然後通過自定義的排序算法
* cpr.compare(key, t.key)比較傳入的key和根節點的key值,如果傳入的key<root.key,那麼
* 繼續在root的左子樹中找,從root的左孩子節點(root.left)開始:如果傳入的key>root.key,
* 那麼繼續在root的右子樹中找,從root的右孩子節點(root.right)開始;如果恰好key==root.key,
* 那麼直接根據root節點的value值即可。
* 後面的循環規則一樣,當遍歷到的當前節點作爲起始節點,逐步往下找
*
* 需要注意的是:這裏並沒有對key是否爲null進行判斷,建議自己的實現Comparator時應該要考慮在內
*/
do {
parent = t;
cmp = cpr.compare(key, t.key);
if (cmp < 0)
t = t.left;
else if (cmp > 0)
t = t.right;
else
return t.setValue(value);
} while (t != null);
}
else {
//從這裏看出,當默認排序時,key值是不能爲null的
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);
}
/**
* 能執行到這裏,說明前面並沒有找到相同的key,節點已經遍歷到最後了,我們只需要new一個Entry放到
* parent下面即可,但放到左子節點上還是右子節點上,就需要按照紅黑樹的規則來。
*/
Entry<K,V> e = new Entry<>(key, value, parent);
if (cmp < 0)
parent.left = e;
else
parent.right = e;
/**
* 節點加進去了,並不算完,我們在前面紅黑樹原理章節提到過,一般情況下加入節點都會對紅黑樹的結構造成
* 破壞,我們需要通過一些操作來進行自動平衡處置,如【變色】【左旋】【右旋】
*/
fixAfterInsertion(e);
size++;
modCount++;
return null;
}
put方法源碼中通過fixAfterInsertion(e)方法來進行自平衡處理
再重點看下 fixAfterInsertion方法的源碼
private void fixAfterInsertion(Entry<K,V> x) {
//新插入的節點爲紅色節點
x.color = RED;
//我們知道父節點爲黑色時,並不需要進行樹結構調整,只有當父節點爲紅色時,才需要調整
while (x != null && x != root && x.parent.color == RED) {
//如果父節點是左節點,對應上表中情況1和情況2
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));
} 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;
}
光看文字有點不好理解,請結合下圖,會看得更清晰
三、讀取
get方法是通過二分查找的思想,我們看一下源碼
public V get(Object key) {
Entry<K,V> p = getEntry(key);
return (p==null ? null : p.value);
}
/**
* 從root節點開始遍歷,通過二分查找逐步向下找
* 第一次循環:從根節點開始,這個時候parent就是根節點,然後通過k.compareTo(p.key)比較傳入的key和
* 根節點的key值;
* 如果傳入的key<root.key, 那麼繼續在root的左子樹中找,從root的左孩子節點(root.left)開始;
* 如果傳入的key>root.key, 那麼繼續在root的右子樹中找,從root的右孩子節點(root.right)開始;
* 如果恰好key==root.key,那麼直接根據root節點的value值即可。
* 後面的循環規則一樣,當遍歷到的當前節點作爲起始節點,逐步往下找
*/
//默認排序情況下的查找
final Entry<K,V> getEntry(Object key) {
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;
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;
}
/**
* 從root節點開始遍歷,通過二分查找逐步向下找
* 第一次循環:從根節點開始,這個時候parent就是根節點,然後通過自定義的排序算法
* cpr.compare(key, t.key)比較傳入的key和根節點的key值,如果傳入的key<root.key,那麼
* 繼續在root的左子樹中找,從root的左孩子節點(root.left)開始:如果傳入的key>root.key,
* 那麼繼續在root的右子樹中找,從root的右孩子節點(root.right)開始;如果恰好key==root.key,
* 那麼直接根據root節點的value值即可。
* 後面的循環規則一樣,當遍歷到的當前節點作爲起始節點,逐步往下找
*/
//自定義排序規則下的查找
final Entry<K,V> getEntryUsingComparator(Object key) {
@SuppressWarnings("unchecked")
K k = (K) key;
Comparator<? super K> cpr = comparator;
if (cpr != null) {
Entry<K,V> p = root;
while (p != null) {
int cmp = cpr.compare(k, p.key);
if (cmp < 0)
p = p.left;
else if (cmp > 0)
p = p.right;
else
return p;
}
}
return null;
}
四、刪除
remove方法可以分爲兩個步驟,先是找到這個節點,直接調用了上面介紹的getEntry(Object key),這個步驟我們就不說了,直接說第二個步驟,找到後的刪除操作。
public V remove(Object key) {
Entry<K,V> p = getEntry(key);
if (p == null)
return null;
V oldValue = p.value;
deleteEntry(p);
return oldValue;
}
通過deleteEntry(p)進行刪除操作,刪除操作的原理我們在前面已經講過
- 刪除的是根節點,則直接將根節點置爲null;
- 待刪除節點的左右子節點都爲null,刪除時將該節點置爲null;
- 待刪除節點的左右子節點有一個有值,則用有值的節點替換該節點即可;
- 待刪除節點的左右子節點都不爲null,則找前驅或者後繼,將前驅或者後繼的值複製到該節點中,然後刪除前驅或者後繼(前驅:左子樹中值最大的節點,後繼:右子樹中值最小的節點);
private void deleteEntry(Entry<K,V> p) { modCount++; size--; //當左右子節點都不爲null時,通過successor(p)遍歷紅黑樹找到前驅或者後繼 if (p.left != null && p.right != null) { Entry<K,V> s = successor(p); //將前驅或者後繼的key和value複製到當前節點p中,然後刪除節點s(通過將節點p引用指向s) p.key = s.key; p.value = s.value; p = s; } Entry<K,V> replacement = (p.left != null ? p.left : p.right); /** * 至少有一個子節點不爲null,直接用這個有值的節點替換掉當前節點,給replacement的parent屬性賦值,給 * parent節點的left屬性和right屬性賦值,同時要記住葉子節點必須爲null,然後用fixAfterDeletion方法 * 進行自平衡處理 */ if (replacement != null) { //將待刪除節點的子節點掛到待刪除節點的父節點上。 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; p.left = p.right = p.parent = null; /** * p如果是紅色節點的話,那麼其子節點replacement必然爲紅色的,並不影響紅黑樹的結構 * 但如果p爲黑色節點的話,那麼其父節點以及子節點都可能是紅色的,那麼很明顯可能會存在紅色相連的情 * 況,因此需要進行自平衡的調整 */ if (p.color == BLACK) fixAfterDeletion(replacement); } else if (p.parent == null) {//這種情況就不用多說了吧 root = null; } else { /** * 如果p節點爲黑色,那麼p節點刪除後,就可能違背每個節點到其葉子節點路徑上黑色節點數量一致的規則, * 因此需要進行自平衡的調整 */ 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; } } }