【從今天開始好好學數據結構04】程序員你心中就沒點「樹」嗎?

前面咱們講的都是線性表結構,棧、隊列等等。今天咱們講一種非線性表結構,樹。樹這種數據結構比線性表的數據結構要複雜得多,內容也比較多,首先咱們先從樹(Tree)開始講起。
@面試

樹(Tree)

樹型結構是一種非線性結構,它的數據元素之間呈現分支、分層的特色。算法

1.樹的定義

樹(Tree)是由n(n≥0)個結點構成的有限集合T,當n=0時T稱爲空樹;不然,在任一非空樹T中:
(1)有且僅有一個特定的結點,它沒有前驅結點,稱其爲根(Root)結點;
(2)剩下的結點可分爲m(m≥0)個互不相交的子集T1,T2,…,Tm,其中每一個子集自己又是一棵樹,並稱其爲根的子樹(Subtree)。數組

注意:樹的定義具備遞歸性,即「樹中還有樹」。樹的遞歸定義揭示出了樹的固有特性數據結構

2.什麼是樹結構

什麼是「樹」?再好的定義,都沒有圖解來的直觀。因此我在圖中畫了幾棵「樹」。你來看看,這些「樹」都有什麼特徵?
在這裏插入圖片描述
你有沒有發現,「樹」這種數據結構真的很像咱們現實生活中的「樹」函數

3.爲何使用樹結構

在有序數組中,能夠快速找到特定的值,可是想在有序數組中插入一個新的數據項,就必須首先找出新數據項插入的位置,而後將比新數據項大的數據項向後移動一位,來給新的數據項騰出空間,刪除同理,這樣移動很費時。顯而易見,若是要作不少的插入和刪除操做和刪除操做,就不應選用有序數組。另外一方面,鏈表中能夠快速添加和刪除某個數據項,可是在鏈表中查找數據項可不容易,必須從頭開始訪問鏈表的每個數據項,直到找到該數據項爲止,這個過程很慢。 樹這種數據結構,既能像鏈表那樣快速的插入和刪除,又能想有序數組那樣快速查找學習

4.樹的經常使用術語

結點——包含一個數據元素和若干指向其子樹的分支
度——結點擁有的子樹個數
樹的度——該樹中結點的最大度數
葉子——度爲零的結點
分支結點(非終端結點)——度不爲零的結點
孩子和雙親——結點的子樹的根稱爲該結點的孩子,相應地,該結點稱爲孩子的雙親
兄弟——同一個雙親的孩子
祖先和子孫——從根到該結點所經分支上的全部結點。相應地,以某一結點爲根的子樹中的任一結點稱爲該結點的子孫。
結點的層次——結點的層次從根開始定義,根結點的層次爲1,其孩子結點的層次爲2,……
堂兄弟——雙親在同一層的結點
樹的深度——樹中結點的最大層次
有序樹和無序樹——若是將樹中每一個結點的各子樹當作是從左到右有次序的(即位置不能互換),則稱該樹爲有序樹;不然稱爲無序樹。
森林——m(m≥0)棵互不相交的樹的有限集合測試

在這裏插入圖片描述
到這裏,樹就講的差很少了,接下來說講二叉樹(Binary Tree)this

二叉樹(Binary Tree)

樹結構多種多樣,不過咱們最經常使用仍是二叉樹,咱們平時最經常使用的樹就是二叉樹。二叉樹的每一個節點最多有兩個子節點,分別是左子節點和右子節點。二叉樹中,有兩種比較特殊的樹,分別是滿二叉樹和徹底二叉樹。滿二叉樹又是徹底二叉樹的一種特殊狀況。3d

1.二叉樹的定義和特色

二叉樹的定義:
二叉樹(Binary Tree)是n(n≥0)個結點的有限集合BT,它或者是空集,或者由一個根結點和兩棵分別稱爲左子樹和右子樹的互不相交的二叉樹組成 。
————————————
二叉樹的特色:
每一個結點至多有二棵子樹(即不存在度大於2的結點);二叉樹的子樹有左、右之分,且其次序不能任意顛倒。

2.幾種特殊形式的二叉樹

一、滿二叉樹
定義:深度爲k且有2k-1個結點的二叉樹,稱爲滿二叉樹。
特色:每一層上的結點數都是最大結點數
二、徹底二叉樹
定義:
深度爲k,有n個結點的二叉樹當且僅當其每個結點都與深度爲k的滿二叉樹中編號從1至n的結點一一對應時,稱爲徹底二叉樹
特色:
特色一 : 葉子結點只可能在層次最大的兩層上出現;
特色二 : 對任一結點,若其右分支下子孫的最大層次爲l,則其左分支下子孫的最大層次必爲l 或l+1

建議看圖對應文字綜合理解

在這裏插入圖片描述
代碼建立二叉樹

首先,建立一個節點Node類

package demo5;
/*
 * 節(結)點類 
 */
public class Node {
    //節點的權
    int value;
    //左兒子(左節點)
    Node leftNode;
    //右兒子(右節點)
    Node rightNode;
    //構造函數,初始化的時候就給二叉樹賦上權值
    public Node(int value) {
        this.value=value;
    }
    
    //設置左兒子(左節點)
    public void setLeftNode(Node leftNode) {
        this.leftNode = leftNode;
    }
    //設置右兒子(右節點)
    public void setRightNode(Node rightNode) {
        this.rightNode = rightNode;
    }

接着建立一個二叉樹BinaryTree 類

package demo5;
/*
 * 二叉樹Class
 */
public class BinaryTree {
    //根節點root
    Node root;
    
    //設置根節點
    public void setRoot(Node root) {
        this.root = root;
    }
    
    //獲取根節點
    public Node getRoot() {
        return root;
    }
}

最後建立TestBinaryTree 類(該類主要是main方法用來測試)來建立一個二叉樹

package demo5;
public class TestBinaryTree {

    public static void main(String[] args) {
        //建立一顆樹
        BinaryTree binTree = new BinaryTree();
        //建立一個根節點
        Node root = new Node(1);
        //把根節點賦給樹
        binTree.setRoot(root);
        //建立一個左節點
        Node rootL = new Node(2);
        //把新建立的節點設置爲根節點的子節點
        root.setLeftNode(rootL);
        //建立一個右節點
        Node rootR = new Node(3);
        //把新建立的節點設置爲根節點的子節點
        root.setRightNode(rootR);
        //爲第二層的左節點建立兩個子節點
        rootL.setLeftNode(new Node(4));
        rootL.setRightNode(new Node(5));
        //爲第二層的右節點建立兩個子節點
        rootR.setLeftNode(new Node(6));
        rootR.setRightNode(new Node(7));
    }

}

下面將會講的遍歷、查找節點、刪除節點都將圍繞這三個類開展

不難看出建立好的二叉樹以下(畫的很差,還望各位見諒):
在這裏插入圖片描述

3.二叉樹的兩種存儲方式

二叉樹既能夠用鏈式存儲,也能夠用數組順序存儲。數組順序存儲的方式比較適合徹底二叉樹,其餘類型的二叉樹用數組存儲會比較浪費存儲空間,因此鏈式存儲更合適。

咱們先來看比較簡單、直觀的鏈式存儲法
在這裏插入圖片描述
接着是基於數組的順序存儲法(該例子是一棵徹底二叉樹)
在這裏插入圖片描述
上面例子是一棵徹底二叉樹,因此僅僅「浪費」了一個下標爲0的存儲位置。若是是非徹底二叉樹,則會浪費比較多的數組存儲空間,以下。
在這裏插入圖片描述
在這裏插入圖片描述
還記得堆和堆排序嗎,堆其實就是一種徹底二叉樹,最經常使用的存儲方式就是數組。

4.二叉樹的遍歷

前面我講了二叉樹的基本定義和存儲方法,如今咱們來看二叉樹中很是重要的操做,二叉樹的遍歷。這也是很是常見的面試題。

經典遍歷的方法有三種,前序遍歷中序遍歷後序遍歷

前序遍歷是指,對於樹中的任意節點來講,先打印這個節點,而後再打印它的左子樹,最後打印它的右子樹。

中序遍歷是指,對於樹中的任意節點來講,先打印它的左子樹,而後再打印它自己,最後打印它的右子樹。

後序遍歷是指,對於樹中的任意節點來講,先打印它的左子樹,而後再打印它的右子樹,最後打印這個節點自己。

在這裏插入圖片描述
我想,睿智的你已經想到了二叉樹的前、中、後序遍歷就是一個遞歸的過程。好比,前序遍歷,其實就是先打印根節點,而後再遞歸地打印左子樹,最後遞歸地打印右子樹。

在以前建立好的二叉樹代碼之上,咱們來使用這三種方法遍歷一下~

依舊是在Node節點類上添加方法:能夠看出遍歷方法都是用的遞歸思想

package demo5;
/*
 * 節(結)點類 
 */
public class Node {
//===================================開始 遍歷========================================
    //前序遍歷
    public void frontShow() {
        //先遍歷當前節點的內容
        System.out.println(value);
        //左節點
        if(leftNode!=null) {
            leftNode.frontShow();
        }
        //右節點
        if(rightNode!=null) {
            rightNode.frontShow();
        }
    }

    //中序遍歷
    public void midShow() {
        //左子節點
        if(leftNode!=null) {
            leftNode.midShow();
        }
        //當前節點
        System.out.println(value);
        //右子節點
        if(rightNode!=null) {
            rightNode.midShow();
        }
    }

    //後序遍歷
    public void afterShow() {
        //左子節點
        if(leftNode!=null) {
            leftNode.afterShow();
        }
        //右子節點
        if(rightNode!=null) {
            rightNode.afterShow();
        }
        //當前節點
        System.out.println(value);
    }

}

而後依舊是在二叉樹BinaryTree 類上添加方法,而且添加的方法調用Node類中的遍歷方法

package demo5;
/*
 * 二叉樹Class
 */
public class BinaryTree {

    public void frontShow() {
        if(root!=null) {
            //調用節點類Node中的前序遍歷frontShow()方法
            root.frontShow();
        }
    }

    public void midShow() {
        if(root!=null) {
            //調用節點類Node中的中序遍歷midShow()方法
            root.midShow();
        }
    }

    public void afterShow() {
        if(root!=null) {
            //調用節點類Node中的後序遍歷afterShow()方法
            root.afterShow();
        }
    }

}

依舊是在TestBinaryTree類中測試

package demo5;

public class TestBinaryTree {

    public static void main(String[] args) {
        //前序遍歷樹
        binTree.frontShow();
        System.out.println("===============");
        //中序遍歷
        binTree.midShow();
        System.out.println("===============");
        //後序遍歷
        binTree.afterShow();
        System.out.println("===============");
        //前序查找
        Node result = binTree.frontSearch(5);
        System.out.println(result);
        
}

若是遞歸理解的不是很透,我能夠分享一個學習的小方法:我建議各位能夠這樣斷點調試,一步一步調,思惟跟上,仔細推敲每一步的運行相信我,你會從新認識到遞歸!(像下面這樣貼個圖再一步一步斷點思惟更加清晰)
在這裏插入圖片描述
貼一下我斷點對遞歸的分析,但願對你有必定的幫助~
在這裏插入圖片描述
二叉樹遍歷的遞歸實現思路天然、簡單,易於理解,但執行效率較低。爲了提升程序的執行效率,能夠顯式的設置棧,寫出相應的非遞歸遍歷算法。非遞歸的遍歷算法能夠根據遞歸算法的執行過程寫出。至於代碼能夠嘗試去寫一寫,這也是一種提高!具體的非遞歸算法主要流程圖貼在下面了:
在這裏插入圖片描述
二叉樹遍歷算法分析:

二叉樹遍歷算法中的基本操做是訪問根結點,不論按哪一種次序遍歷,都要訪問全部的結點,對含n個結點的二叉樹,其時間複雜度均爲O(n)。所需輔助空間爲遍歷過程當中所需的棧空間,最多等於二叉樹的深度k乘以每一個結點所需空間數,最壞狀況下樹的深度爲結點的個數n,所以,其空間複雜度也爲O(n)。

5.二叉樹中節點的查找與刪除

剛纔講到二叉樹的三種金典遍歷放法,那麼節點的查找一樣是能夠效仿的,分別叫作前序查找、中序查找以及後序查找,下面代碼只之前序查找爲例,三者查找方法思路相似~

至於刪除節點,有三種狀況:

一、若是刪除的是根節點,那麼二叉樹就徹底被刪了
二、若是刪除的是雙親節點,那麼該雙親節點以及他下面的全部子節點所構成的子樹將被刪除
三、若是刪除的是葉子節點,那麼就直接刪除該葉子節點

那麼,我把完整的三個類給貼出來(包含建立、遍歷、查找、刪除)

依舊是Node節點類

package demo5;
/*
 * 節(結)點類 
 */
public class Node {
    //節點的權
    int value;
    //左兒子
    Node leftNode;
    //右兒子
    Node rightNode;
    //構造函數,初始化的時候就給二叉樹賦上權值
    public Node(int value) {
        this.value=value;
    }
    
    //設置左兒子
    public void setLeftNode(Node leftNode) {
        this.leftNode = leftNode;
    }
    //設置右兒子
    public void setRightNode(Node rightNode) {
        this.rightNode = rightNode;
    }
    
    //前序遍歷
    public void frontShow() {
        //先遍歷當前節點的內容
        System.out.println(value);
        //左節點
        if(leftNode!=null) {
            leftNode.frontShow();
        }
        //右節點
        if(rightNode!=null) {
            rightNode.frontShow();
        }
    }

    //中序遍歷
    public void midShow() {
        //左子節點
        if(leftNode!=null) {
            leftNode.midShow();
        }
        //當前節點
        System.out.println(value);
        //右子節點
        if(rightNode!=null) {
            rightNode.midShow();
        }
    }

    //後序遍歷
    public void afterShow() {
        //左子節點
        if(leftNode!=null) {
            leftNode.afterShow();
        }
        //右子節點
        if(rightNode!=null) {
            rightNode.afterShow();
        }
        //當前節點
        System.out.println(value);
    }

    //前序查找
    public Node frontSearch(int i) {
        Node target=null;
        //對比當前節點的值
        if(this.value==i) {
            return this;
        //當前節點的值不是要查找的節點
        }else {
            //查找左兒子
            if(leftNode!=null) {
                //有可能能夠查到,也能夠查不到,查不到的話,target仍是一個null
                target = leftNode.frontSearch(i);
            }
            //若是不爲空,說明在左兒子中已經找到
            if(target!=null) {
                return target;
            }
            //查找右兒子
            if(rightNode!=null) {
                target=rightNode.frontSearch(i);
            }
        }
        return target;
    }
    
    //刪除一個子樹
    public void delete(int i) {
        Node parent = this;
        //判斷左兒子
        if(parent.leftNode!=null&&parent.leftNode.value==i) {
            parent.leftNode=null;
            return;
        }
        //判斷右兒子
        if(parent.rightNode!=null&&parent.rightNode.value==i) {
            parent.rightNode=null;
            return;
        }
        
        //遞歸檢查並刪除左兒子
        parent=leftNode;
        if(parent!=null) {
            parent.delete(i);
        }
        
        //遞歸檢查並刪除右兒子
        parent=rightNode;
        if(parent!=null) {
            parent.delete(i);
        }
    }

}

依舊是BinaryTree 二叉樹類

package demo5;
/*
 * 二叉樹Class
 */
public class BinaryTree {
    //根節點root
    Node root;
    
    //設置根節點
    public void setRoot(Node root) {
        this.root = root;
    }
    
    //獲取根節點
    public Node getRoot() {
        return root;
    }

    public void frontShow() {
        if(root!=null) {
            //調用節點類Node中的前序遍歷frontShow()方法
            root.frontShow();
        }
    }

    public void midShow() {
        if(root!=null) {
            //調用節點類Node中的中序遍歷midShow()方法
            root.midShow();
        }
    }

    public void afterShow() {
        if(root!=null) {
            //調用節點類Node中的後序遍歷afterShow()方法
            root.afterShow();
        }
    }
    //查找節點i
    public Node frontSearch(int i) {
        return root.frontSearch(i);
    }
    //刪除節點i
    public void delete(int i) {
        if(root.value==i) {
            root=null;
        }else {
            root.delete(i);
        }
    }
    
}

依舊是TestBinaryTree測試類

package demo5;

public class TestBinaryTree {

    public static void main(String[] args) {
        //建立一顆樹
        BinaryTree binTree = new BinaryTree();
        //建立一個根節點
        Node root = new Node(1);
        //把根節點賦給樹
        binTree.setRoot(root);
        //建立一個左節點
        Node rootL = new Node(2);
        //把新建立的節點設置爲根節點的子節點
        root.setLeftNode(rootL);
        //建立一個右節點
        Node rootR = new Node(3);
        //把新建立的節點設置爲根節點的子節點
        root.setRightNode(rootR);
        //爲第二層的左節點建立兩個子節點
        rootL.setLeftNode(new Node(4));
        rootL.setRightNode(new Node(5));
        //爲第二層的右節點建立兩個子節點
        rootR.setLeftNode(new Node(6));
        rootR.setRightNode(new Node(7));
        //前序遍歷樹
        binTree.frontShow();
        System.out.println("===============");
        //中序遍歷
        binTree.midShow();
        System.out.println("===============");
        //後序遍歷
        binTree.afterShow();
        System.out.println("===============");
        //前序查找
        Node result = binTree.frontSearch(5);
        System.out.println(result);
        
        System.out.println("===============");
        //刪除一個子樹
        binTree.delete(4);
        binTree.frontShow();
        
    }

}

到這裏,總結一下,咱們學了一種非線性表數據結構,樹。關於樹,有幾個比較經常使用的概念你須要掌握,那就是:根節點、葉子節點、父節點、子節點、兄弟節點,還有節點的高度、深度、層數,以及樹的高度等。咱們平時最經常使用的樹就是二叉樹。二叉樹的每一個節點最多有兩個子節點,分別是左子節點和右子節點。二叉樹中,有兩種比較特殊的樹,分別是滿二叉樹和徹底二叉樹。滿二叉樹又是徹底二叉樹的一種特殊狀況。二叉樹既能夠用鏈式存儲,也能夠用數組順序存儲。數組順序存儲的方式比較適合徹底二叉樹,其餘類型的二叉樹用數組存儲會比較浪費存儲空間。除此以外,二叉樹裏很是重要的操做就是前、中、後序遍歷操做,遍歷的時間複雜度是O(n),你須要理解並能用遞歸代碼來實現。

若是本文章對你有幫助,哪怕是一點點,請點個讚唄,謝謝~

歡迎各位關注個人公衆號,一塊兒探討技術,嚮往技術,追求技術...說好了來了就是盆友喔...

在這裏插入圖片描述

相關文章
相關標籤/搜索