整理:數據結構與算法之二叉樹

1、概述

通過前面的學習,我們知道,

有序數組可以利用二分查找法快速的查找特定的值,時間複雜度爲O(log2N),但是插入數據時很慢,時間複雜度爲O(N)

鏈表的插入和刪除速度都很快,時間複雜度爲O(1),但是查找特定值很慢,時間複雜度爲O(N)

那麼,有沒有一種數據結構既能像有序數組那樣快速的查找數據,又能像鏈表那樣快速的插入數據呢?

樹就能滿足這種要求,不過依然是以算法的複雜度爲代價。

在編程的世界裏,有一個真理叫“複雜度守恆定律”(當然,這是我杜撰的),一個程序當它降低了一個方面的複雜度,必然會在其他方面增加複雜度。這就跟談戀愛一樣,也沒有無緣無故的愛,沒有無緣無故的恨,當你跟程序談戀愛時,沒有無緣無故的易用性,也沒有無緣無故的複雜度。

我們先從廣義上來討論一下樹的概念。

下面是一個普通的非二叉樹:

 

在程序中,節點一般用來表示實體,也就是數據結構裏存儲的那些數據項,在java這樣的面向對象的編程語言中,常用節點來表示對象。
節點間的邊表示關聯節點間的路徑,沿着路徑,從一個節點到另一個節點很容易,也很快,在樹中,從一個節點到另一個節點的唯一方法就是順着邊前進。java語言中,常用引用來表示邊(C/C++中一般使用指針)。

樹的頂層總是隻有一個節點,它通過邊連接到第二層的多個節點,然後第二層也可以通過邊連接到第三層,以此類推。所以樹的頂部小,底部大,呈倒金字塔型,這和現實世界中的樹是相反的。

如果樹的每個節點最多有兩個子節點,則稱爲二叉樹。如果節點的子節點可以多餘兩個,稱爲多路樹

有很多關於樹的術語,在這裏不做過多的文字解釋,下面給出一個圖例,通過它可以直觀地理解樹的路徑、根、父節點、子節點、葉節點、子樹、層等概念。

 

需要注意的是,從樹的根到任意節點有且只有一條路徑可以到達,下圖所示就不是一棵樹,它違背了這一原則:

 

2、二叉搜索樹

我們從一種特殊的、使用很廣泛的二叉樹入手:二叉搜索樹

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

下圖就是一個二叉搜索樹的示例:

 

關於樹,還有一個平衡樹非平衡樹的概念。非平衡就是說樹的大部分節點在根的一邊,如下圖所示:

 

樹的不平衡是由數據項插入的順序造成的。如果關鍵字是隨機插入的,樹會更趨向於平衡,如果插入順序是升序或者降序,則所有的值都是右子節點或左子節點,這樣生成的樹就會不平衡了,非平衡樹的效率會嚴重退化。

接下來我們就用java語言實現一個二叉搜索樹,並給出查找、插入、遍歷、刪除節點的方法。

首先要有一個封裝節點的類,這個類包含節點的數據以及它的左子節點和右子節點的引用。

 

//樹節點的封裝類
public class Node {
    int age;
    String name;
    Node leftChild;  //左子節點的引用
    Node rightChild; //右子節點的引用
    
    public Node(int age,String name){
        this.age = age;
        this.name = name;
    }
    
    //打印該節點的信息
    public void displayNode(){
        System.out.println("name:"+name+",age:"+age);
    }
}

以上agename兩個屬性用來代表該節點存儲的信息,更好的方法是將這些屬性封裝成一個對象,例如:

 

Person{
    private int age;
    private String name;

    public void setAge(int age){
        this.age = age;
    }

    public int getAge(){
        return this.age;
    }

    public void setName(String name){
        this.name = name;
    }

    public String getName(){
        return this.name;
    }

}

這樣做才更符合“面向對象”的編程思想。不過現在我們的重點是數據結構而非編程思想,所以在程序中簡化了。

由於樹的結構和算法相對複雜,我們先逐步分析一下查找、插入等操作的思路,然後再寫出整個的java類。

2.1 查找

我們已經知道,二叉搜索樹的特點是左子節點小於父節點,右子節點大於或等於父節點。查找某個節點時,先從根節點入手,如果該元素值小於根節點,則轉向左子節點,否則轉向右子節點,以此類推,直到找到該節點,或者到最後一個葉子節點依然沒有找到,則證明樹中沒有該節點。

比如我們要在樹中查找57,執行的搜索路線如下圖所示:

 

2.2 插入

插入一個新節點首先要確定插入的位置,這個過程類似於查找一個不存在的節點。如下圖所示:

 

找到要插入的位置之後,將父節點的左子節點或者右子節點指向新節點即可

2.3 遍歷

遍歷的意思是根據一種特定順序訪問樹的每一個節點。

有三種簡單的方法遍歷樹:

  • 前序遍歷
  • 中序遍歷
  • 後序遍歷

二叉搜索樹最常用的方法是中序遍歷,中序遍歷二叉搜索樹會使所有的節點按關鍵字升序被訪問到

遍歷樹最簡單的方法是遞歸。用該方法時,只需要做三件事(初始化時這個節點是根):

  • 調用自身來遍歷節點的左子樹
  • 訪問這個節點
  • 調用自身來遍歷節點的右子樹

遍歷可以應用於任何二叉樹,而不只是二叉搜索樹。遍歷的節點並不關心節點的關鍵字值,它只看這個節點是否有子節點
下圖展示了中序遍歷的過程:

 

對於每個節點來說,都是先訪問它的左子節點,然後訪問自己,然後在訪問右子節點。

如果是前序遍歷呢?就是先訪問父節點,然後左子節點,最後右子節點;同理,後序遍歷就是先訪問左子節點,在訪問右子節點,最後訪問父節點。所謂的前序、中序、後序是針對父節點的訪問順序而言的。

A:根節點、B:左節點、C:右節點,

前序順序是ABC(根節點排最先,然後同級先左後右);

中序順序是BAC(先左後根最後右);

後序順序是BCA(先左後右最後根)

2.4 查找最值

在二叉搜索樹中,查找最大值、最小是是很容易實現的,從根循環訪問左子節點,直到該節點沒有左子節點爲止,該節點就是最小值;從根循環訪問右子節點,直到該節點沒有右子節點爲止,該節點就是最大值。

下圖就展示了查找最小值的過程:

 

2.5 刪除節點

樹的刪除節點操作是最複雜的一項操作。該操作需要考慮三種情況考慮:

  • 該節點沒有子節點
  • 該節點有一個子節點
  • 該節點有兩個子節點

第一種沒有子節點的情況很簡單,只需將父節點指向它的引用設置爲null即可:

 

第二種情況也不是很難,這個節點有兩個連接需要處理:父節點指向它的引用和它指向子節點的引用。無論要刪除的節點下面有多複雜的子樹,只需要將它的子樹上移:

 

還有一種特殊情況需要考慮,就是要刪除的是根節點,這時就需要把它唯一的子節點設置成根節點。

下面來看最複雜的第三種情況:要刪除的節點有兩個子節點。顯然,這時候不能簡單地將子節點上移,因爲該節點有兩個節點,右子節點上移之後,該右子節點的左子節點和右子節點又怎麼安排呢?

 

這是應該想起,二叉搜索樹是按照關鍵升序排列,對每一個關鍵字來說,比它關鍵字值高的節點是它的中序後繼,簡稱後繼。刪除有兩個子節點的節點,應該用它的中序後繼來替代該節點。

 

上圖中,我們先列出中序遍歷的順序:

5 15 20 25 30 35 40

可以看到,25的後繼是35,所以應該用30來替代25的位置。實際上就是找到比欲刪除節點的關鍵字值大的集合中的最小值。從樹的結構上來說,就是從欲刪除節點的右子節點開始,依次跳到下一層的左子節點,直到該左子節點沒有左子節點爲止。下圖就是找後繼節點的示例:

 

從上圖中可以看到,後繼結點有兩種情況:一種是欲刪除節點的右子節點沒有左子節點,那麼它本身就是後繼節點,此時,只需要將以此後繼節點爲根的子樹移到欲刪除節點的位置:

 

另一種情況是欲刪除節點的右子節點有左子節點,這種情況就比較複雜,下面來逐步分析。首先應該意識到,後繼節點是肯定沒有左子節點的,但是可能會有右子節點。

 

上圖中,75爲欲刪除節點,77爲它的後繼節點,樹變化的步驟如下:

  • 把87的左子節點設置爲79;
  • 把77的右子節點設爲以87爲根的子樹;
  • 把50的右子節點設置爲以77爲根的子樹;
  • 把77的左子節點設置爲62

到此爲止,刪除操作終於分析完畢,包含了所有可能出現的情況。可見,二叉樹的刪除是一件非常棘手的工作,那麼我們就該反思了,刪除是必須要做的任務嗎?有沒有一種方法避開這種煩人的操作?有困難要上,沒有困難創造困難也要上的二貨精神是不能提倡的。

節點邏輯刪除:

在刪除操作不是很多的情況下,可以在節點類中增加一個布爾字段,來作爲該節點是否已刪除的標誌。在進行其他操作,比如查找時,之前對該節點是否已刪除進行判斷。這種思路有點逃避責任,但是在很多時候還是很管用的。本例中爲了更好的深入理解二叉樹,會採用原始的、複雜的刪除方法。

3、實例

下面我們就根據上面的分析,寫出一個完整的二叉搜索樹類,該類中,如果有重複值,插入到右子節點,查找時也只返回第一個找到的節點。

 

import java.util.ArrayList;
import java.util.List;

//二叉搜索樹的封裝類
public class BinaryTree {
    private Node root;  //根節點
    
    public BinaryTree(){
        root = null;
    }
    
    //按關鍵字查找節點
    public Node find(int key){
        Node cur = root;  //從根節點開始查找
        
        if(cur == null){  //如果樹爲空,直接返回null
            return null;
        }
        
        while(cur.age != key){
            if(key < cur.age){ 
                cur = cur.leftChild;  //如果關鍵字比當前節點小,轉向左子節點
            }else{
                cur = cur.leftChild;  //如果關鍵字比當前節點大,轉向右子節點
            }
            
            if(cur == null){  //沒有找到結果,搜索結束
                return null;
            }
        }
        return cur;
    }
    
    //插入新節點
    public void insert(Node node){
        if(root == null){
            root = node;  //如果樹爲空,則新插入的節點爲根節點
        }else{
            Node cur = root;  
            
            while(true){  
                if(node.age < cur.age){
                    if(cur.leftChild == null){  //找到了要插入節點的父節點
                        cur.leftChild = node;
                        return;
                    }
                    cur = cur.leftChild;
                }else{
                    if(cur.rightChild == null){  //找到了要插入節點的父節點
                        cur.rightChild = node;
                        return;
                    }
                    cur = cur.rightChild;
                }
            }
        }
    }
    
    //刪除指定節點
    public boolean delete(Node node){
        if(root == null){
            return false;  //如果爲空樹,直接返回false
        }
        
        boolean isLeftChild = true;  //記錄目標節點是否爲父節點的左子節點
        Node cur= root;  //要刪除的節點
        Node parent = null; //要刪除節點的父節點
        
        while(cur.age != node.age){  //確定要刪除節點和它的父節點
            parent = cur;
            if(node.age < cur.age){  //目標節點小於當前節點,跳轉左子節點
                cur = cur.leftChild;
            }else{//目標節點大於當前節點,跳轉右子節點
                isLeftChild = false;
                cur = cur.rightChild;
            }
            if(cur == null){
                return false;  //沒有找到要刪除的節點
            }
        }
    
        if(cur.leftChild == null && cur.rightChild == null){  //目標節點爲葉子節點(無子節點)
            if(cur == root){  //要刪除的爲根節點
                root = null;
            }else if(isLeftChild){ 
                //要刪除的不是根節點,則該節點肯定有父節點,該節點刪除後,需要將父節點指向它的引用置空
                parent.leftChild = null;
            }else{
                parent.rightChild = null;
            }
        }else if(cur.leftChild == null){  //只有一個右子節點
            if(cur == root){
                root = cur.rightChild;
            }else if(isLeftChild){
                parent.leftChild = cur.rightChild;
            }else{
                parent.rightChild = cur.rightChild;
            }
        }else if(cur.rightChild == null){  //只有一個左子節點
            if(cur == root){
                root = cur.leftChild;
            }else if(isLeftChild){
                parent.leftChild = cur.leftChild;
            }else{
                parent.rightChild = cur.leftChild;
            }
        }else{  //有兩個子節點
            //第一步要找到欲刪除節點的後繼節點
            Node successor = cur.rightChild;  
            Node successorParent = null;
            while(successor.leftChild != null){
                successorParent = successor;
                successor = successor.leftChild;
            }
            //欲刪除節點的右子節點就是它的後繼,證明該後繼無左子節點,則將以後繼節點爲根的子樹上移即可
            if(successorParent == null){  
                if(cur == root){  //要刪除的爲根節點,則將後繼設置爲根,且根的左子節點設置爲欲刪除節點的做左子節點
                    root = successor;
                    root.leftChild = cur.leftChild;
                }else if(isLeftChild){ 
                    parent.leftChild = successor;
                    successor.leftChild = cur.leftChild;
                }else{
                    parent.rightChild = successor;
                    successor.leftChild = cur.leftChild;
                }
            }else{ //欲刪除節點的後繼不是它的右子節點
                successorParent.leftChild = successor.rightChild;
                successor.rightChild = cur.rightChild;
                if(cur == root){  
                    root = successor;
                    root.leftChild = cur.leftChild;
                }else if(isLeftChild){ 
                    parent.leftChild = successor;
                    successor.leftChild = cur.leftChild;
                }else{
                    parent.rightChild = successor;
                    successor.leftChild = cur.leftChild;
                }
            }
        }
        
        return true;
    }
    
    public static final int PREORDER = 1;   //前序遍歷
    public static final int INORDER = 2;    //中序遍歷
    public static final int POSTORDER = 3;  //中序遍歷
    
    //遍歷
    public void traverse(int type){
        switch(type){
        case 1:
            System.out.print("前序遍歷:\t");
            preorder(root);
            System.out.println();
            break;
        case 2:
            System.out.print("中序遍歷:\t");
            inorder(root);
            System.out.println();
            break;
        case 3:
            System.out.print("後序遍歷:\t");
            postorder(root);
            System.out.println();
            break;
        }
    }
    
    //前序遍歷
    public void preorder(Node currentRoot){
        if(currentRoot != null){
            System.out.print(currentRoot.age+"\t");
            preorder(currentRoot.leftChild);
            preorder(currentRoot.rightChild);
        }
    }
    
    //中序遍歷,這三種遍歷都用了迭代的思想
    public void inorder(Node currentRoot){
        if(currentRoot != null){
            inorder(currentRoot.leftChild);  //先對當前節點的左子樹對進行中序遍歷
            System.out.print(currentRoot.age+"\t"); //然後訪問當前節點
            inorder(currentRoot.rightChild);  //最後對當前節點的右子樹對進行中序遍歷
        }
    }
    
    //後序遍歷
    public void postorder(Node currentRoot){
        if(currentRoot != null){
            postorder(currentRoot.leftChild);
            postorder(currentRoot.rightChild);
            System.out.print(currentRoot.age+"\t");
        }
    }
    
    //私有方法,用迭代方法來獲取左子樹和右子樹的最大深度,返回兩者最大值
    private int getDepth(Node currentNode,int initDeep){
        int deep = initDeep;  //當前節點已到達的深度
        int leftDeep = initDeep;
        int rightDeep = initDeep;
        if(currentNode.leftChild != null){  //計算當前節點左子樹的最大深度
            leftDeep = getDepth(currentNode.leftChild, deep+1);
        }
        if(currentNode.rightChild != null){  //計算當前節點右子樹的最大深度
            rightDeep = getDepth(currentNode.rightChild, deep+1);
        }
        
        return Math.max(leftDeep, rightDeep);
    }
    
    //獲取樹的深度
    public int getTreeDepth(){
        if(root == null){
            return 0;
        }
        return getDepth(root,1);
    }
    
    //返回關鍵值最大的節點
    public Node getMax(){
        if(isEmpty()){
            return null;
        }
        Node cur = root;
        while(cur.rightChild != null){
            cur = cur.rightChild;
        }
        return cur;
    }
    
    //返回關鍵值最小的節點
    public Node getMin(){
        if(isEmpty()){
            return null;
        }
        Node cur = root;
        while(cur.leftChild != null){
            cur = cur.leftChild;
        }
        return cur;
    }
    
    //以樹的形式打印出該樹
    public void displayTree(){
        int depth = getTreeDepth();
        ArrayList<Node> currentLayerNodes = new ArrayList<Node> ();
        currentLayerNodes.add(root);  //存儲該層所有節點
        int layerIndex = 1;
        while(layerIndex <= depth){
            int NodeBlankNum = (int)Math.pow(2, depth-layerIndex)-1;  //在節點之前和之後應該打印幾個空位
            for(int i = 0;i<currentLayerNodes.size();i++){
                Node node = currentLayerNodes.get(i);
                printBlank(NodeBlankNum);   //打印節點之前的空位
                
                if(node == null){
                    System.out.print("*\t");  //如果該節點爲null,用空位代替
                }else{
                    System.out.print("*  "+node.age+"\t");  //打印該節點
                }
                
                printBlank(NodeBlankNum);  //打印節點之後的空位
                System.out.print("*\t");   //補齊空位
            }
            System.out.println();
            layerIndex++;
            currentLayerNodes = getAllNodeOfThisLayer(currentLayerNodes);  //獲取下一層所有的節點
        }
    }
    
    //獲取指定節點集合的所有子節點
    private ArrayList getAllNodeOfThisLayer(List parentNodes){
        ArrayList list = new ArrayList<Node>();
        Node parentNode;
        for(int i=0;i<parentNodes.size();i++){
            parentNode = (Node)parentNodes.get(i);
            if(parentNode != null){  
                if(parentNode.leftChild != null){  //如果上層的父節點存在左子節點,加入集合
                    list.add(parentNode.leftChild);
                }else{
                    list.add(null);  //如果上層的父節點不存在左子節點,用null代替,一樣加入集合
                }
                if(parentNode.rightChild != null){
                    list.add(parentNode.rightChild);
                }else{
                    list.add(null);
                }
            }else{  //如果上層父節點不存在,用兩個null佔位,代表左右子節點
                list.add(null);
                list.add(null);
            }
        }
        return list;
    }
    
    //打印指定個數的空位
    private void printBlank(int num){
        for(int i=0;i<num;i++){
            System.out.print("*\t");
        }
    }
    
    //判空
    public boolean isEmpty(){
        return (root == null);
    }
    
    //判斷是否爲葉子節點
    public boolean isLeaf(Node node){
        return (node.leftChild != null || node.rightChild != null);
    }
    
    //獲取根節點
    public Node getRoot(){
        return root;
    }
    
}

displayTree方法按照樹的形狀打印該樹。對一顆深度爲3的二叉樹的打印效果如下圖所示:



作者:冰河winner
鏈接:https://www.jianshu.com/p/bb0c12b34f2a
來源:簡書
著作權歸作者所有。商業轉載請聯繫作者獲得授權,非商業轉載請註明出處。

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