本章繼續承接上章的內容,具體實現TreeMap
中的方
1.簡單的TreeMap
這裏比較核心的一個方法是findNode
,用來尋找與鍵值相當的節點,下面是它的實現:
private Node findNode(Object target) {
if (target == null) {
throw new IllegalArgumentException();
}
@SuppressWarnings("unchecked")
Comparable<?super K> k = (Comparable<?super K>)target;
Node node = root;
while (node != null) {
int cmp = k.compareTo(node.key);
if (cmp <0)
node = node.left;
else if (cmp>0)
node = node.right;
else
return node;
}
return null;
}
- 在這個實現中,
null
不是鍵的合法值。 - 在我們可以在
target
上調用compareTo
之前,我們必須把它強制轉換爲某種形式的Comparable
。這裏使用的“類型通配符”會儘可能允許;也就是說,它適用於任何實現Comparable
類型,並且它的compareTo
接受K
或者任和K
的超類(可以同任何類型做比較)。
之後,實際搜索比較簡單。我們初始化一個循環變量node
來引用根節點。每次循環中,我們將目標與node.key
比較。如果目標小於當前鍵,我們移動到左子樹。如果它更大,我們移動到右子樹。如果相等,我們返回當前節點(這裏用的是迭代,不斷賦值)。
2.搜索值
findNode
運行時間與樹的高度成正比,而不是節點的數量,因爲我們不必搜索整個樹。但是對於containsValue
,我們必須搜索值,而不是鍵;BST 的特性不適用於值,因此我們必須搜索整個樹。
下面是containsValue
方法,這裏用遞歸實現:
public boolean containsValue(Object target) {
return containsValueHelper(root, target);
}
private boolean containsValueHelper(Node node, Object target) {
if (node == null) {
return false;
}
if (equals(target, node.value)) {
return true;
}
if (containsValueHelper(node.left, target)) {
return true;
}
if (containsValueHelper(node.right, target)) {
return true;
}
return false;
}
這是containsValueHelper
的工作原理:
- 第一個
if
語句檢查遞歸的邊界情況。如果node
是null
,那意味着我們已經遞歸到樹的底部,沒有找到target
,所以我們應該返回false
。請注意,這隻意味着目標沒有出現在樹的一條路徑上;它仍然可能會在另一條路徑上被發現。 - 第二種情況檢查我們是否找到了我們正在尋找的東西。如果是這樣,我們返回
true
。否則,我們必須繼續。 - 第三種情況是執行遞歸調用,在左子樹中搜索
target
。如果我們找到它,我們可以立即返回true
,而不搜索右子樹。否則我們繼續。 - 第四種情況是搜索右子樹。同樣,如果我們找到我們正在尋找的東西,我們返回
true
。否則,我們搜索完了整棵樹,返回false
。
該方法“訪問”了樹中的每個節點,所以它的所需時間與節點數成正比。
3.實現put
put
方法比起get
要複雜一些,因爲要處理兩種情況:
- 如果給定的鍵已經在樹中,則替換並返回舊值;
- 否則必須在樹中添加一個新的節點,在正確的地方。
public V put(K key, V value) {
if (key == null) {
throw new IllegalArgumentException();
}
if (root == null) {
root = new Node(key, value);
size++;
return null;
}
return putHelper(root, key, value);
}
private V putHelper(Node node, K key, V value) {
Comparable<? super K> k = (Comparable<? super K>) key;
int cmp = k.compareTo(node.key);
if (cmp < 0) {
if (node.left == null) {
node.left = new Node(key, value);
size++;
return null;
} else {
return putHelper(node.left, key, value);
}
}
if (cmp > 0) {
if (node.right == null) {
node.right = new Node(key, value);
size++;
return null;
} else {
return putHelper(node.right, key, value);
}
}
V oldValue = node.value;
node.value = value;
return oldValue;
}
第一個參數node
最初是樹的根,但是每次我們執行遞歸調用,它指向了不同的子樹。就像get
一樣,我們用compareTo
方法來弄清楚,跟隨哪一條樹的路徑。如果cmp < 0
,我們添加的鍵小於node.key
,那麼我們要走左子樹。有兩種情況:
- 如果左子樹爲空,那就是,如果
node.left
是null
,我們已經到達樹的底部而沒有找到key
。這個時候,我們知道key
不在樹上,我們知道它應該放在哪裏。所以我們創建一個新節點,並將它添加爲node
的左子樹。 - 否則我們進行遞歸調用來搜索左子樹。
如果cmp > 0
,我們添加的鍵大於node.key
,那麼我們要走右子樹。我們處理的兩個案例與上一個分支相同。最後,如果cmp == 0
,我們在樹中找到了鍵,那麼我們更改它並返回舊的值。
4.中序遍歷
這裏我們還剩最後一個方法KeySet
,它返回一個Set
,按升序包含樹中的鍵。在其他Map
實現中,keySet
返回的鍵沒有特定的順序,但是樹形實現的一個功能是,對鍵進行簡單而有效的排序。下面是如何實現它的:
public Set<K> keySet() {
Set<K> set = new LinkedHashSet<K>();
addInOrder(root, set);
return set;
}
private void addInOrder(Node node, Set<K> set) {
if (node == null) return;
addInOrder(node.left, set);
set.add(node.key);
addInOrder(node.right, set);
}
在keySet
中,我們創建一個LinkedHashSet
,這是一個Set
實現,使元素保持有序,第一個參數node
最初是樹的根,但正如你的期望,我們用它來遞歸地遍歷樹。addInOrder
對樹執行經典的“中序遍歷”。
- 按順序遍歷左子樹。
- 添加
node.key
。 - 按順序遍歷右子樹。
5.二叉搜索樹的問題
我們獲取最有查詢效率時,一般是O(log(n)),這種情況會在所搜索的樹爲平衡二叉樹時出現,若不是平衡二叉樹,搜索效率則會很低。
如果你思考put
如何工作,你可以弄清楚發生了什麼。每次添加一個新的鍵時,它都大於樹中的所有鍵,所以我們總是選擇右子樹,並且總是將新節點添加爲,最右邊的節點的右子節點。結果是一個“不平衡”的樹,只包含右子節點。
這種樹的高度正比於n
,不是logn
,所以get
和put
的性能是線性的,不是對數的
6.自平衡樹
這個問題有兩種可能的解決方案:
- 你可以避免向
Map
按順序添加鍵。但這並不總是可能的。 你可以製作一棵樹,如果碰巧按順序處理鍵,那麼它會更好地處理鍵。(按順序添加會導致這是一個極不平衡的樹) - 第二個解決方案是更好的,有幾種方法可以做到。最常見的是修改
put
,以便它檢測樹何時開始變得不平衡,如果是,則重新排列節點。具有這種能力的樹被稱爲“自平衡樹”。普通的自平衡樹包括 AVL 樹(“AVL”是發明者的縮寫),以及紅黑樹,這是 JavaTreeMap
所使用的。
總而言之,二叉搜索樹可以以對數時間實現get
和put
,但是隻能按照使得樹足夠平衡的順序添加鍵。自平衡樹通過每次添加新鍵時,進行一些額外的工作來避免這個問題。