【結構】查找二叉樹

載一棵小樹苗,精心培育,總有一天會長成參天大樹
                比如查找二叉、AVL、B+ - *、紅黑……

結構

繼線性結構之後,人們之所以又發明了樹形結構,是爲了方便查找。普通樹隨便生長,看着就眼暈,除了和自然界的樹結構相似對得起Tree這名號,沒太大價值,更別提方便查找了。

clipboard.png

自查找二叉樹起,可以說種族崛起了。結構上:從根節點起,小於父節點的在左,大於父節點的在右。如此結構,本身就是有序的,以中序遍歷(左->中->右)的方式走一遍,很方便把值從小到大排序。

clipboard.png

以下給出這種樹的關鍵方法的邏輯,以及具體的編碼實現。

編碼

(不善言辭,coding爲敬……)

首先代碼定義出查找二叉樹的結構:

// 結構
public class BinaryNode<V> {
    
    // key
    private Integer id;    
    // val,存儲業務數據
    private V val;
    // 左節點
    private BinaryNode<V> leftNode;
    // 右節點
    private BinaryNode<V> rightNode;
    
    public BinaryNode(Integer id, V val, BinaryNode<V> leftNode, BinaryNode<V> rightNode) {
        this.id = id;
        this.val = val;
        this.leftNode = leftNode;
        this.rightNode = rightNode;
    }
}
注意:
    查找二叉樹的結構可類比數據庫表結構理解。
    對於mysql中採用innodb引擎創建的表而言,以id爲主鍵的表數據會自動加持索引。(實際上索引是B+ tree結構,可視作變態版的查找二叉樹)
    這也是上面的代碼結構定義中,key採用id命名的原因,val則可視爲該數據庫表的其它字段。

新增節點

clipboard.png

每次新增節點,會從根節點開始,根據值的大小,尋找自己的位置。

舉個例子,上圖的樹再增加一個節點35,會經歷如下心路歷程:

  1. 與根節點50比較,新節點35較小,置左;很不幸,左邊已經有節點30雄踞。
  2. 新節點35繼續與節點30比較。其實此時可以忽略根節點50,把左子樹視作一顆新樹,節點30視作新的根節點……沒錯,就是遞歸,直到最終找到一個空位置,方安身立命。

clipboard.png

代碼實現如下:

//新增,先不考慮id重複的問題
//@param id:新增節點的key
//@param val:新增節點的值
BinaryNode<V> add(Integer id,V val,BinaryNode<V> tree){
    //該位置無節點,安身立命
    if(tree == null){
        tree = new BinaryNode<>(id,val,null,null);
    }

    //遞歸:新節點id大於當前節點,新節點置右
    if(id>tree.id){
        tree.rightNode = add(id,val,tree.rightNode);
    }
    
    //遞歸:新節點id小於當前節點,新節點置左
    if(id<tree.id){
        tree.leftNode = add(id,val,tree.leftNode);
    }

    return tree;
}

範圍查找

正如之前提過的,查找二叉樹的優勢就在於範圍查找。

怎麼在一顆查找二叉樹中找到min>=且<=max的全部值?具體步驟如下:

  1. 當前節點與min比較,如果大於min,則遞歸查看它的左節點;如果它沒有左節點,結束遞歸
  2. 當前節點在min和max範圍內,放入結果集
  3. 當前節點與max比較,如果小於max,則遞歸查看它的右節點;如果它沒有右節點,結束遞歸
Collection<V> searchRange(Integer min, Integer max, Collection<V> collection,BinaryNode<V> tree){
    //當前節點與min比較,如果大於min,遞歸查看當前節點的左節點(如果有左節點的話)
    if(min<tree.getId() && tree.leftNode!=null){
        searchRange(min,max,collection,tree.leftNode);
    }

    //當前節點在範圍內,則放入結果集
    if(min<=tree.getId() && max>=tree.getId()){
        collection.add(tree.getVal());
    }

    //當前節點與max比較,如果小於max,遞歸查看當前節點的右節點(如果有右節點的話)
    if(tree.getId()<max && tree.rightNode!=null){
        searchRange(min,max,collection,tree.rightNode);
    }

    return collection;
}

刪除節點

刪除節點相對比較複雜,涉及到子樹銜接問題,分幾種情況:

  1. 無子:被刪節點沒有子節點(葉子節點),直接移除就好。
  2. 單子:直接頂替被刪除節點的位置。
  3. 雙子

clipboard.png

BinaryNode<V> remove(Integer id,BinaryNode<V> tree){
    if(tree==null){
        return null;
    }

    if(id>tree.getId()){
        tree.rightNode = remove(id,tree.rightNode);
    }

    if(id<tree.getId()){
        tree.leftNode = remove(id,tree.leftNode);
    }

    if(id==tree.id){
        //單子
        if(tree.getLeftNode()==null && tree.getRightNode()!=null){
            tree = tree.rightNode;
        }else if(tree.getLeftNode()!=null && tree.getRightNode()==null){
            tree = tree.leftNode;
        }
        //雙子
        else if(tree.getLeftNode()!=null && tree.getRightNode()!=null){
            //方便起見:將id和val值改變,引用不動
            BinaryNode<V> min = findMin(tree.rightNode);    //找到最小節點
            tree.id = min.id;
            tree.val = min.val;

            tree.rightNode = remove(tree.id,tree.rightNode);
        }
        //無子
        else {
            tree = null;
        }
    }
    return tree;
}
注意:
    這裏有個小訣竅。在做節點替換時(`32`替換`30`),可直接修改id和val,這樣就不需要修改引用了!

不足

試想一下這種情況,查找二叉樹在新增節點時,假如一直增加更小的節點,我們將得到一個只有左節點的查找二叉樹(雖然二叉不起來)。這樣的樹與鏈表又有什麼區別呢?這種極端情況下,就失去了樹的優勢了。

clipboard.png
也就是說,在新增或刪除操作過程中,樹越不均衡(左傾或右傾),越影響查找效率!

如何彌補這種不足?需要保持樹的平衡,敬請期待AVL樹——平衡的查找二叉樹。

附錄

完整代碼實現見我的git練習項目com.evolution.tree包下,地址:evolution 暗夜君王的各種demo練習

囉嗦幾句,demo項目中查找二叉樹的實現com.evolution.tree.BinaryNode,模擬了數據庫表,會多一些id的唯一性驗證。另外,還增加了中序遍歷實現sort()方法。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章