<em>Mac</em>Book项目 2009年学校开始实施<em>Mac</em>Book项目,所有师生配备一本<em>Mac</em>Book,并同步更新了校园无线网络。学校每周进行电脑技术更新,每月发送技术支持资料,极大改变了教学及学习方式。因此2011
2021-06-01 09:32:01
二元搜尋樹結合了無序連結串列插入便捷和有序陣列二分查詢快速的特點,較為高效地實現了有序符號表。下圖顯示了二元搜尋樹的結構特點(圖片來自《演演算法第四版》):
可以看到每個父節點下都可以連著兩個子節點,鍵寫在節點上,其中左邊的子節點的鍵小於父節點的鍵,右節點的鍵大於父節點的鍵。每個父節點及其後代節點組成了一顆子樹,根節點及其後代節點則組成了完整的二元搜尋樹。
在程式碼層面看來,就是每個節點物件中包含另外兩個子節點的指標,同時包含一些要用到的資料,比如鍵值對和方便後續操作的整課子樹的節點數量。
private class Node { int N = 1; K key; V value; Node left; Node right; public Node(K key, V value) { this.key = key; this.value = value; } }
上述程式碼實現了一個節點類,這個類是二元搜尋樹類 BinarySearchTree 的內部類,使用者無需知道這個節點類的存在,所以存取許可權宣告為 private。
先來看下無序符號表的 API,這些方法宣告了無序符號表的基本操作,包括插入、查詢和刪除,為了方便符號表的迭代,介面中還有 Iterable<K> keys() 方法用於 foreach 迴圈:
package com.zhiyiyo.collection.symboltable; public interface SymbolTable<K, V>{ void put(K key, V value); V get(K key); void delete(K key); boolean contains(K key); boolean isEmpty(); int size(); Iterable<K> keys(); }
接下來是有序符號表的 API,其中每個節點的鍵必須實現了 Comparable 介面:
package com.zhiyiyo.collection.symboltable; public interface OrderedSymbolTable<K extends Comparable<K>, V> extends SymbolTable<K, V>{ /** * 獲取符號表中最小的鍵 * @return 最小的鍵 */ K min(); /** * 獲取符號表中最大的鍵 * @return 最大的鍵 */ K max(); /** * 獲取小於或等於 key 的最大鍵 * @param key 鍵 * @return 小於或等於 key 的最大鍵 */ K floor(K key); /** * 獲取大於或等於 key 的最小鍵 * @param key 鍵 * @return 大於或等於 key 的最小鍵 */ K ceiling(K key); /** * 獲取小於或等於 key 的鍵數量 * @param key 鍵 * @return 小於或等於 key 的鍵數量 */ int rank(K key); /** * 獲取排名為 k 的鍵,k 的取值範圍為 [0, N-1] * @param k 排名 * @return 排名為 k 的鍵 */ K select(int k); /** * 刪除最小的鍵 */ void deleteMin(); /** * 刪除最大的鍵 */ void deleteMax(); /** * [low, high] 區間內的鍵數量 * @param low 最小的鍵 * @param high 最大的鍵 * @return 鍵數量 */ int size(K low, K high); /** * [low, high] 區間內的所有鍵,升序排列 * @param low 最小的鍵 * @param high 最大的鍵 * @return 區間內的鍵 */ Iterable<K> keys(K low, K high); }
類的基本結構如下述程式碼所示,可以看到只需用一個根節點 root 即可代表一整棵二元搜尋樹:
public class BinarySearchTree<K extends Comparable<K>, V> implements OrderedSymbolTable<K, V> { private Node root; private class Node{ ... } ... }
從根節點出發,拿著給定的鍵 key 和根節點的鍵進行比較,會出現以下三種情況:
當我們去左子樹或者右子樹查詢時,只需將子樹的根節點視為新的根節點,然後重複上述步驟即可。如果找到最後都沒找到擁有和 key 相等的鍵的節點,返回 null 即可。在《演演算法第四版》中使用遞迴實現了上述步驟,這裡換為迭代法:
@Override public V get(K key) { Node node = root; while (node != null) { int cmp = node.key.compareTo(key); // 到左子樹搜尋 if (cmp > 0) { node = node.left; } // 到右子樹搜尋 else if (cmp < 0) { node = node.right; } else { return node.value; } } return null; }
將鍵值對放入二元搜尋樹時會發生兩種情況:
所以在插入的時候要從根節點出發,比較根節點的鍵和給定的 key 之間的大小關係,和查詢相似,比較會有三種情況發生:
如果找到最後都沒能找到那個擁有相同 key 的節點,就需要建立一個新的節點,把這個節點,接到子樹的根節點上,用迭代法實現上述過程的程式碼如下所示:
@Override public put(K key, V value){ if (root == null) { root = new Node(key, value); return; } Node node = root; Node parent = root; int cmp = 0; while (node != null){ parent = node; cmp = node.key.compareTo(key); // 到左子樹搜尋 if (cmp > 0){ node = node.left; } // 到右子樹搜尋 else if (cmp < 0){ node = node.right; } else { node.value = value; return; } } // 新建節點並接到父節點上 if (cmp > 0) { parent.left = new Node(key, value); } else{ parent.right = new Node(key, value); } }
可以看到上述過程用了兩個指標,一個指標 node 用於探路,一個指標 parent 用於記錄子樹的根節點,不然當 node 為空時我們是找不到他的父節點的,也就沒法把新的節點接到父節點上。
上述程式碼有個小問題,就是我們新建節點之後沒辦法更新這一路上所經過的父節點的 N,也就是每一顆子樹的節點數。怎麼辦呢,要麼用一個容器儲存一下經過的父節點,要麼老老實實用遞迴,這裡選擇用遞迴。遞迴的想法很直接:
別忘了,使用遞迴的原因是我們要更新父節點的 N,所以遞迴的返回值應該是更新後的子樹根節點,所以就有了下述程式碼:
@Override public void put(K key, V value) { root = put(root, key, value); } private Node put(Node node, K key, V value) { if (node == null) return new Node(key, value); int cmp = node.key.compareTo(key); if (cmp > 0) { node.left = put(node.left, key, value); } else if (cmp < 0) { node.right = put(node.right, key, value); } else { node.value = value; } node.N = size(node.left) + size(node.right) + 1; return node; } private int size(Node node) { return node == null ? 0 : node.N; }
從根節點出發,一路向左,鍵會是一個遞減的序列,當我們走到整棵樹的最左邊,也就是 left 為 null 的那個節點時,我們就已經找到了鍵最小的節點。上述過程的迭代法程式碼如下:
@Override public K min() { if (root == null) { return null; } Node node = root; while (node.left != null) { node = node.left; } return node.key; }
查詢最大鍵的節點過程和上述過程類似,只是我們這次得向右走,直到找到 right 為 null 的那個節點:
@Override public K max() { if (root == null) { return null; } Node node = root; while (node.right != null) { node = node.right; } return node.key; }
演演算法書中給出的 min() 實現程式碼是用遞迴實現的,因為在刪除節點時會用到。遞迴的過程就是一直朝左子樹走的的過程,直到遇到一個節點沒有左子樹為止,然後返回該節點即可。
@Override public K min() { if (root == null) { return null; } return min(root).key; } private Node min(Node node) { if (node.left == null) return node; return min(node.left); }
從根節點出發,拿著根節點的的鍵和 key 進行比較,會出現三種情況:
@Override public K floor(K key) { if (root == null) { return null; } Node node = root; Node candidate = root; while (node != null) { int cmp = node.key.compareTo(key); if (cmp > 0) { node = node.left; } else if (cmp < 0) { candidate = node; node = node.right; } else { return node.key; } } return candidate.key.compareTo(key) <= 0 ? candidate.key : null; }
《演演算法第四版》中給出了一個範例圖,可以更直觀地看到上述查詢過程:
查詢大於等於 key 的最小鍵的方法和上述過程很像,拿著根節點的的鍵和 key 進行比較,會出現三種情況:
@Override public K ceiling(K key) { if (root == null) { return null; } Node node = root; Node candidate = root; while (node != null) { int cmp = node.key.compareTo(key); if (cmp < 0) { node = node.right; } else if (cmp > 0) { candidate = node; node = node.left; } else { return node.key; } } return candidate.key.compareTo(key) >= 0 ? candidate.key : null; }
假設一棵二元搜尋樹中有 N 個節點,那麼節點的鍵排名區間就是 [0, N-1],也就是說,key 的排名可以看做小於 key 的鍵的個數。所以我們應該如何根據排名獲得其對應的鍵呢?這時候每個節點中的維護的 N 屬性就可以派上用場了。
從根節點向左看,左子樹的節點數就是小於根節點鍵的鍵個數,也就是根節點的鍵排名。所以拿著根節點的左子樹節點數 N 和排名 k 進行比較,會出現三種情況:
k = k - N - 1
即可。@Override public K select(int k) { Node node = root; while (node != null) { // 父節點左子樹的大小就是父節點的鍵排名 int N = size(node.left); if (N > k) { node = node.left; } else if (N < k) { node = node.right; k = k - N - 1; } else { return node.key; } } return null; }
把根據排名獲取鍵的過程寫作key = select(k),那麼根據鍵獲取排名的過程就是k = select-1(key) = rank(key)。說明這兩個函數互為反函數。
從根節點出發,拿著根節點的鍵和 key 進行比較會出現三種情況:
@Override public int rank(K key) { Node node = root; int N = 0; while (node != null) { int cmp = node.key.compareTo(key); if (cmp > 0) { node = node.left; } else if (cmp < 0) { N += size(node.left) + 1; node = node.right; } else { return size(node.left) + N; } } return N; }
刪除操作較為複雜,先來看下較為簡單的刪除鍵最小的節點的過程。從根節點出發,一路向左,知道遇到左子樹為 null 的節點,由於這個節點可能還有右子樹,所以需要把右子樹接到父節點上。接完之後還得把這一路上遇到的父節點上的 N - 1。由於沒有其他節點參照了被刪除的節點,所以這個節點會被 java 的垃圾回收機制自動回收。演演算法書中給出了一個刪除的範例圖:
使用迭代法可以實現尋找最小節點和將右子樹連線到父節點的操作,但是不好處理每一顆子樹的 N 的更新操作,所以還是得靠遞迴法。由於我們需要將最小節點的右子樹接到父節點上,所以滿足終止條件時 deleteMin(Node node)
函數應該把右子樹的根節點返回,否則就應該返回更新之後的節點。
@Override public void deleteMin() { if (root == null) return; root = deleteMin(root); } private Node deleteMin(Node node) { if (node.left == null) return node.right; node.left = deleteMin(node.left); node.N = size(node.left) + size(node.right) + 1; return node; }
刪除最大的節點的過程和上面相似,只不過我們應該將最大節點的左子樹接到父節點上。
@Override public void deleteMax() { if (root == null) return; root = deleteMax(root); } private Node deleteMax(Node node) { if (node.right == null) return node.left; node.right = deleteMax(node.right); node.N = size(node.left) + size(node.right) + 1; return node; }
討論完上面兩個較為簡單的刪除操作,我們來看下如何刪除任意節點。從根節點出發,通過比較根節點的鍵和給定的 key,會發生三種情況:
根節點的鍵大於 key,接著去左子樹刪除 key
根節點的鍵小於 key,接著去右子樹刪除 key
根節點的鍵等於 key ,說明我們找到了要被刪除的那個節點,這時候我們又會遇到三種情況:
演演算法書中給出了第三種情況(右子樹和左子樹都不為空)的範例圖:
使用遞迴實現的程式碼如下所示:
@Override public void delete(K key) { root = delete(root, key); } private Node delete(Node node, K key) { if (node == null) return null; // 先找到 key 對應的節點 int cmp = node.key.compareTo(key); if (cmp > 0) { node.left = delete(node.left, key); } else if (cmp < 0) { node.right = delete(node.right, key); } else { if (node.right == null) return node.left; if (node.left == null) return node.right; Node x = node; node = min(x.right); // 移除右子樹的最小節點 node,並將該節點作為右子樹的根節點 node.right = deleteMin(x.right); // 設定左子樹的根節點為 node node.left = x.left; } node.N = size(node.left) + size(node.right) + 1; return node; }
如果在插入鍵值對的時候運氣較好,二元搜尋樹的左右子樹高度相近,那麼插入和查詢的比較次數為 (sim2ln N) ;如果運氣非常差,差到所有的節點連成了一條單向連結串列,那麼插入和查詢的比較次數就是 (sim N)。所以就有了自平衡二元樹的出現,不過這已經超出本文的探討範圍了(絕對不是因為寫不動了,以上~~
到此這篇關於在Java中實現二元搜尋樹的文章就介紹到這了,更多相關Java二元搜尋樹內容請搜尋it145.com以前的文章或繼續瀏覽下面的相關文章希望大家以後多多支援it145.com!
相關文章
<em>Mac</em>Book项目 2009年学校开始实施<em>Mac</em>Book项目,所有师生配备一本<em>Mac</em>Book,并同步更新了校园无线网络。学校每周进行电脑技术更新,每月发送技术支持资料,极大改变了教学及学习方式。因此2011
2021-06-01 09:32:01
综合看Anker超能充系列的性价比很高,并且与不仅和iPhone12/苹果<em>Mac</em>Book很配,而且适合多设备充电需求的日常使用或差旅场景,不管是安卓还是Switch同样也能用得上它,希望这次分享能给准备购入充电器的小伙伴们有所
2021-06-01 09:31:42
除了L4WUDU与吴亦凡已经多次共事,成为了明面上的厂牌成员,吴亦凡还曾带领20XXCLUB全队参加2020年的一场音乐节,这也是20XXCLUB首次全员合照,王嗣尧Turbo、陈彦希Regi、<em>Mac</em> Ova Seas、林渝植等人全部出场。然而让
2021-06-01 09:31:34
目前应用IPFS的机构:1 谷歌<em>浏览器</em>支持IPFS分布式协议 2 万维网 (历史档案博物馆)数据库 3 火狐<em>浏览器</em>支持 IPFS分布式协议 4 EOS 等数字货币数据存储 5 美国国会图书馆,历史资料永久保存在 IPFS 6 加
2021-06-01 09:31:24
开拓者的车机是兼容苹果和<em>安卓</em>,虽然我不怎么用,但确实兼顾了我家人的很多需求:副驾的门板还配有解锁开关,有的时候老婆开车,下车的时候偶尔会忘记解锁,我在副驾驶可以自己开门:第二排设计很好,不仅配置了一个很大的
2021-06-01 09:30:48
不仅是<em>安卓</em>手机,苹果手机的降价力度也是前所未有了,iPhone12也“跳水价”了,发布价是6799元,如今已经跌至5308元,降价幅度超过1400元,最新定价确认了。iPhone12是苹果首款5G手机,同时也是全球首款5nm芯片的智能机,它
2021-06-01 09:30:45