二叉樹詳解


我們先了解有序數組和鏈表兩種數據結構:有序數組,可以通過二分查找法快速的查詢特定的值,時間復雜度為O(logN),可是插入刪除時效率低,平均要移動N/2個元素,時間復雜度為O(N)。鏈表:查詢效率低,平均要比較N/2個元素,時間復雜度O(N),插入和刪除效率較高,O(1)。二叉樹的特點是結合了有序數組和鏈表的優點,能像有序數組那樣快速的查找,又能像鏈表那樣快速的插入和刪除。操作二叉搜索樹的時間復雜度是O(logN)。

1.定義

二叉樹:樹中的每個節點最多只能有兩個子節點,這樣的樹是二叉樹。

二叉搜索樹:一個節點的左子節點的關鍵字值小於這個父節點,右子節點的關鍵字值大於等於這個父節點。

平衡二叉搜索樹:它是一顆裸空樹或它的左右兩個子樹的高度差的絕對值不超過1,並且左右兩棵子樹都是平衡二叉樹。它的實現方式有:紅黑樹,AVL

2.結構圖

二叉樹由節點和邊構成,每個節點包含的有關鍵字值,以及其他數據。接下來我們通過代碼看如何對二叉搜索樹進行插入,刪除,查找,遍歷等操作。

3.查找操作

先定義節點Node

public class Node {
    int key; //key 關鍵字值
    double data; //存儲的數據
    Node leftChild; //左子節點的引用
    Node rightChild; //右子節點的引用

    //顯示該節點內容
    public void displayNode(){
        System.out.println("key="+key+",data="+data);
    }
}

定義二叉樹Tree

public class Tree {
    
    //根節點
    public Node root;
    
}

 查找方法:將關鍵字值key與根節點的關鍵字值做比較,小於root節點的關鍵字值,進入root節點的左子樹進行比較;否則如果大於,進入root節點的右子樹進行比較。依次比較下去,直到key的值等於某個節點的關鍵字值,則將該節點Node返回。如果沒有找到

符合條件的節點,那么返回null

//查詢效率和有序數組中的二分查找法一樣,時間復雜度為O(log2N)
    public Node find(int key){
        Node current = root;
        while (current.key != key) {
            if(key < current.key)
                current = current.leftChild;
            else{
                current = current.rightChild;
            }
            //表示沒有找到符合條件的節點
            if (null == current)
                return null;
        }
        return current;
    }

4.插入操作

插入操作,先要確定新節點要插入的位置,這個就是查找的過程;然后確定位置后,將新節點newNode作為父節點parent的的左子節點或者右子節點

 

   //插入方法:查找最后一個不為null的節點parent作為要插入節點的父節點,newNode作為它的左子節點或者右子節點
    public void insert(int key,double data){
        Node newNode = new Node();
        newNode.key = key;
        newNode.data = data;
        if(null == root){
            root = newNode;
        }else{
            Node current = root;
            Node parent;
            while(true){
                parent = current;
                if(key < current.key){
                    //go left
                    current = current.leftChild;
                    if(null == current){
                        parent.leftChild = newNode;
                        return;
                    }
                    
                }else{
                    //go right
                    current = current.rightChild;
                    if(null == current){
                        parent.rightChild = newNode;
                        return;
                    }
                }
                
            }
        }
    }

 

5.遍歷操作

遍歷操作,是指根據特定的順序訪問樹的每一個節點。遍歷方法有:中序遍歷,前序遍歷,后序遍歷

中序遍歷,會使所有節點按照關鍵字值的升序被訪問到。遍歷時,通常使用遞歸調用的方法,初始參數是樹的根節點。中序遍歷會有三個步驟:

調用自身來遍歷該節點的左子樹

* 訪問這個節點

* 調用自身來遍歷該節點的右子樹

    //中序遍歷二叉樹:按key值的升序排序
    public void orderByMiddle(Node localRoot){
        if(null != localRoot){
            orderByMiddle(localRoot.leftChild);
            System.out.println(localRoot.data);
            orderByMiddle(localRoot.rightChild);
        }
    }
    
    //前序遍歷二叉樹
    public void preOrder(Node localRoot){
        if(null != localRoot){
            System.out.println(localRoot.data);
            preOrder(localRoot.leftChild);
            preOrder(localRoot.rightChild);
        }
    }
    
    //后序遍歷二叉樹
    public void postOrder(Node localRoot){
        if(null != localRoot){
            postOrder(localRoot.leftChild);
            postOrder(localRoot.rightChild);
            System.out.println(localRoot.data);
        }
    }

6.查找最大值和最小值

查找最大值,從根節點開始走向右子節點,然后一直走向右子節點,直到最后一個不為null的右子節點,就是最大值。查找最小值,是類似的,一直走向左子節點。

    //查找最大值
    public Node maxNum() {
        Node current,last = null;
        current = root;
        while (null != current) {
            last = current;
            current = current.rightChild;
        }
        return last;
    }
    
    //查找最小值
    public Node miniNum() {
        Node current,last = null;
        current = root;
        while (null != current) {
            last = current;
            current = current.leftChild;
        }
        return last;
    }

 7.刪除操作

對於刪除操作的邏輯如下:

* 要先判斷被刪除的節點的情況,然后分別處理,被刪除節點可能是:是葉節點,只有一個子節點,有兩個子節點

* 如果被刪除節點有兩個子節點:找到要刪除節點的后繼節點,然后讓后繼節點替換該節點。由於二叉搜索樹要滿足的特性是一個節點的左子節點的key值要比該節點小,右子節點的key值要比該節點大,所以,一個節點的后繼節點就是比該節點key值大的最小的那個節點。也就是該節點的右子節點的左子節點,再左子節點,直到最后一個不是null的左子節點,就是該節點的后繼節點。找到后繼節點后,就將后繼節點替換當前節點,涉及到各個節點引用的改變,有四個地方的引用改變,也就是代碼中的abcd四個步驟。

如果要刪除的節點有兩個子節點,看圖:

//刪除方法
    //1.要刪除的節點是葉節點 2.只有一個字節點  3.有兩個子節點
    //步驟:1.先找到要刪除的節點
    public boolean delete(int key){
        //1.先查找到要刪除的節點
        Node current = root;
        Node parent = root;
        //標識被刪除的節點是否是左子節點
        boolean isLeftChild = true;
        while(key != current.key){
            parent = current;
            if(key < current.key){
                current = current.leftChild;
                isLeftChild = true;
            }else{
                current = current.rightChild;
                isLeftChild = false;
            }
            
            //沒有找到要刪除的節點
            if(null == current){
                return false;
            }
        }
        
        //2.如果該節點沒有子節點
        if(null == current.leftChild && null == current.rightChild){
            //判斷該節點是否是根節點
            if(current == root){
                root = null;
            }else if(isLeftChild) {
                parent.leftChild = null;
            }else{
                parent.rightChild = null;
            }
            //3.如果該節點只有一個子節點:左子節點
        }else if(null == current.rightChild){
            //被刪除的節點如果是根節點    
            if(current == root){
                root = current.leftChild;
            }else if(isLeftChild){
                parent.leftChild = current.leftChild;
            }else{
                parent.rightChild = current.leftChild;
            }
            //4.如果該節點只有一個子節點:右子節點
        }else if(null == current.leftChild){
            if(current == root){
                root = current.rightChild;
            }else if(isLeftChild){
                parent.leftChild = current.rightChild;
            }else{
                parent.rightChild = current.rightChild;
            }
            //5.如果要刪除的節點有兩個子節點:需要找到該節點的后繼節點代替該節點,后繼節點就是key值比當前節點大的那個最小的值
        }else{
            //先找到后繼節點successor
            Node successor = getSuccessor(current);
            if(current == root){
                root = successor;
            }else if(isLeftChild){
                //c 將后繼節點賦值給要刪除節點的父節點的左子節點 
                parent.leftChild = successor;
            }else{
                //c 將后繼節點賦值給要刪除節點的父節點的右子節點
                parent.rightChild = successor;
            }
            //d 將要刪除節點的左子節點賦值給后繼節點的左子節點
            successor.leftChild = current.leftChild;
        }
        
        return true;
        
    }
    
    
    //查找后繼節點,並替換部分引用 a,b
    private Node getSuccessor(Node delNode){
        Node successorParent = delNode;
        Node successor = delNode;
        Node current = delNode.rightChild;
        while (current != null) {
            successorParent = successor;
            successor = current;
            current = current.leftChild;
        }
        
        if(successor != delNode.rightChild){
            successorParent.leftChild = successor.rightChild; //a 把后繼節點的右子節點賦值給后繼節點的父節點的左子節點
            successor.rightChild = delNode.rightChild; //b 把要刪除節點的右子節點賦值給后繼節點的右子節點 
        }
        
        return successor;
    }
    

8.用數組表示二叉樹

其實,除了以上那種方式表示二叉樹外,還可以用數組表示二叉樹。用數組時,節點存儲在數組中,節點再數組中的位置對應於它在樹中的位置,通過下圖可以理解它的存儲方式。

9.重復關鍵字 

由於二叉搜索樹的特性是:右子節點的key值大於等於父節點,所以在insert操作中,關鍵字key值相同的節點插入到與它相同的節點的右子節點處。在查詢操作時,獲取到的是多個相同節點的第一個節點。

 

注:以上圖片摘自《Java數據結構和算法》這本書


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM