在上一篇中咱們基於數組和鏈表實現了Map的相關操做,可是對於數據量稍大的狀況下,這兩種實現方式效率都比較低,爲了改進這個問題,本篇咱們未來學習二叉樹,並經過二叉樹來實現上一篇中定義的Map結構node
雖然你們都知道二叉樹是什麼,可是爲了保證文章的完整性,這裏仍是簡單說說什麼是二叉樹數組
二叉樹中每一個節點都包含了兩個指針指向本身的左子樹和右子樹。數據結構
二叉樹的每一個節點都包含了一個Key, 而且每一個節點的Key都大於其左子樹中的任意節點,小於右子樹中的任意節點。ide
節點的數據結構定義:post
class Node { private K key; private V value; private Node left; private Node right; private int size = 1; public Node(K key, V value) { this.key = key; this.value = value; } } 複製代碼
size
記錄當前節點所在子樹的節點個數,計算方式:size=左子樹的個數 + 1 + 右子樹的個數
性能
在上一篇《基於數組或鏈表實現Map》中咱們定義了Map的接口,本篇咱們繼續使用該map接口學習
public interface Map<K, V> { void put(K key, V value); V get(K key); void delete(K key); int size(); Iterable<K> keys(); Iterable<TreeNode> nodes(); default boolean contains(K key) { return get(key) != null; } default boolean isEmpty() { return size() == 0; } } public interface SortedMap<K extends Comparable<K>, V> extends Map<K, V> { int rank(K key); void deleteMin(); void deleteMax(); K min(); K max(); } 複製代碼
在二叉樹中查找一個鍵最簡單直接的方式就是使用遞歸,把查找的key和節點的key進行比較,若是較小就去左子樹中繼續遞歸查找,若是較大就在右子樹中查找,若是相等,表示已找到直接返回value,若是遞歸結束還未找到就返回nullthis
代碼實現:spa
@Override public V get(K key) { if (Objects.isNull(key)) { throw new IllegalArgumentException("key can not null"); } Node node = get(root, key); return Objects.isNull(node) ? null : node.value; } private Node get(Node node, K key) { if (Objects.isNull(node)) { return null; } int compare = key.compareTo(node.key); if (compare > 0) { return get(node.right, key); } else if (compare < 0) { return get(node.left, key); } else { return node; } } 複製代碼
在二叉樹中咱們可能會常常使用到查詢樹中的最大值和最小值,包括後面咱們的刪除操做也會使用到,因此這裏咱們須要實現這兩個方法;指針
最大值的實現: 從根節點開始沿着右子樹遞歸,直到遇到右子樹爲null的時候就結束,此時的節點就是最大值 最小值的實現: 從根節點開始沿着左子樹遞歸,知道遇到左子樹爲null的時候就結束,此時的節點就是最小值
@Override public K max() { Node max = max(root); return max.key; } protected Node min(Node node) { if (Objects.isNull(node.left)) { return node; } return min(node.left); } protected Node max(Node node) { if (Objects.isNull(node.right)) { return node; } return max(node.right); } 複製代碼
從上面的實現咱們能夠看出二叉樹的查詢方法和上篇中數組二分查找法實現的同樣簡單高效,這是二叉樹的一個重要特性,並且二叉樹的插入與查詢操做同樣簡單,理想狀況下插入和查詢操做時間複雜度都是log(N)
插入操做的實現思路: 與查詢操做相似,依然是遞歸,若是put的key值比當前節點大就須要去右子樹遞歸,若是較小就去左子樹遞歸,若是相等就直接更新節點的值。若是遞歸結束後還未找到值就新建一個節點並返回
private Node put(Node node, K key, V value) { if (Objects.isNull(node)) { return new Node(key, value); } int compare = key.compareTo(node.key); if (compare > 0) { node.right = put(node.right, key, value); } else if (compare < 0) { node.left = put(node.left, key, value); } else { node.value = value; } node.size = size(node.left) + 1 + size(node.right); return node; } private int size(Node node) { if (Objects.isNull(node)) { return 0; } return node.size; } 複製代碼
其中size的計算在前面已經說到,當前節點的size = 左子樹.size + 1 + 右子樹.size
二叉樹中相對比較麻煩的操做就是刪除操做,因此咱們先來了解下刪除最大值和最小值應該如何實現。
刪除最小值:和前面實現查找最小值有些類似,沿着左邊路徑一直深刻,直到遇到一個節點的左子樹爲null, 那麼這個當前節點就是最小值,在遞歸中把當前節點的右子樹返回便可;
最大值實現思路相似
代碼以下:
@Override public void deleteMin() { root = deleteMin(root); } public Node deleteMin(Node node) { if (Objects.isNull(node.left)) { return node.right; } node.left = deleteMin(node.left); node.size = size(node.left) + 1 + size(node.right); return node; } @Override public void deleteMax() { root = deleteMax(root); } public Node deleteMax(Node node) { if (Objects.isNull(node.right)) { return node.left; } node.right = deleteMax(node.right); node.size = size(node.left) + 1 + size(node.right); return node; } 複製代碼
咱們能夠經過相似的方式去刪除只有一個子節點或者是沒有子節點的節點;可是若是遇到須要刪除有兩個節點的節點應該怎麼操做呢?
兩種思路:用左子樹的最大值替換待刪除節點,而後刪除掉左子樹的最大值;或者是用右子樹中的最小值替換待刪除節點,而後刪除右子樹中的最小值
步驟:
代碼實現
@Override public void delete(K key) { root = delete(root, key); } private Node delete(Node node, K key) { if (Objects.isNull(node)) { return null; } int compare = key.compareTo(node.key); if (compare > 0) { node.right = delete(node.right, key); } else if (compare < 0) { node.left = delete(node.left, key); } else { if (Objects.isNull(node.left)) { return node.right; } if (Objects.isNull(node.right)) { return node.left; } Node max = max(node.left); node.key = max.key; node.value = max.value; node.left = deleteMax(node.left); } node.size = size(node.left) + 1 + size(node.right); return node; } 複製代碼
使用二叉樹實現的Map運行的效率取決於樹的形狀,而樹的形狀取決於數據輸入的順序;最好的狀況下二叉樹是平衡的,那麼get、put的時間複雜度都是log(N); 可是若是插入的數據是有序的,那麼二叉樹就會演變成鏈表,那麼get、put的性能將會大大減低;
基於這個問題,咱們會繼續改進咱們實現的Map,下一篇咱們將會學習使用紅黑樹來實現咱們的Map操做,不管數據插入的順序如何都能保證二叉樹近似平衡
參考:《2020最新Java基礎精講視頻教程和學習路線!》
連接:https://juejin.cn/post/694227...