10.樹結構的基礎

目錄

1.樹的介紹
2.二叉樹
3.滿二叉樹
4.完全二叉樹
5.二叉樹的遍歷算法
6.二叉樹的順序存儲結構
7.二叉樹的鏈式存儲結構
8.線索二叉樹

1.樹的介紹

樹結構是一種非線性存儲結構,存儲的是具有“一對多”關係的數據元素的集合。

在這裏插入圖片描述
圖中就是使用樹結構存儲的集合 {A,B,C,D,E,F,G,H,I,J,K,L,M} 的示意圖。對於數據 A 來說,和數據 B、C、D 有關係;對於數據 B 來說,和 E、F 有關係。這就是“一對多”的關係。整個存儲形狀類似於實際生活中倒着的樹,所以稱這種存儲結構爲“樹型”存儲結構。

樹的常用術語:

節點:使用樹結構存儲的每一個數據元素都被稱爲“結點”。例如上圖中數據元素 A 就是一個結點;
父節點、子結點和兄弟結點:若一個節點含有子節點,則這個節點稱爲其子節點的父節點。例如上圖中A是B、C、D的父節點。反過來B、C、D就成爲A的子節點。並且對於B、C、D 來說,它們都有相同的父節點A,它們又互爲兄弟節點。
根節點:每一個非空樹都有且只有一個被稱爲根的節點,如果一個節點沒有父節點,那麼這個節點就是整棵樹的根節點。例如上圖中A節點就是整棵樹的根節點。
葉子節點:如果一個節點沒有任何子節點,那麼該節點稱爲葉子節點。例如上圖中的K、L、F、G、M、I、J 都是這棵樹的葉子節點。
子樹:在上圖中,A是整棵樹的根節點,而B節點,又可以看成是E、F、K、L組成的樹的根節點。所以稱 B、E、F、K、L 組成的樹爲整棵樹的子樹。同樣,E、K、L 構成的也是一棵子樹,根節點爲 E。另外單個結點也是一棵樹,只不過根結點就是它本身。
:對於一個節點,擁有的子樹數稱爲該節點的度,例如上圖中根節點 A 下有3個子樹,所以節點 A 的度爲 3。對於一整顆樹而言,一棵樹的度是樹內各節點的度的最大值,如上圖中各個節點的度的最大值爲 3,所以整棵樹的度的值是 3。
節點的層次:從一棵樹的樹根開始,樹根所在層爲第一層,根的孩子節點所在的層爲第二層,依次類推。
樹的深度:一棵樹的深度(高度)是樹中節點所在的最大的層次。圖 1(A)樹的深度爲 4。

2.二叉樹

二叉樹介紹:

每個節點最多含有兩個子樹的樹稱爲二叉樹。
在這裏插入圖片描述

二叉樹性質:

1.二叉樹中,第 i 層最多有 2i-1 個節點。
2.如果二叉樹的深度爲 K,那麼此二叉樹最多有 2K-1 個節點。
3.二叉樹中,葉子節點數爲 n1,度爲 2 的節點數爲 n2,則 n1=n2+1。

3.滿二叉樹

滿二叉樹介紹:

如果二叉樹中除了葉子節點,每個節點的度都爲 2,則此二叉樹稱爲滿二叉樹。
在這裏插入圖片描述

滿二叉樹性質:

滿二叉樹除了滿足普通二叉樹的性質,還具有以下性質:
1.滿二叉樹中第 i 層的節點數爲 2n-1 個。
2.深度爲 k 的滿二叉樹必有 2k-1 個節點 ,葉子數爲 2k-1
3.滿二叉樹中不存在度爲 1 的節點,每一個分支點中都兩棵深度相同的子樹,且葉子節點都在最底層。
4.具有 n 個節點的滿二叉樹的深度爲 log2(n+1)。

4.完全二叉樹

完全二叉樹介紹:

如果二叉樹中除去最後一層節點爲滿二叉樹,且最後一層的節點依次從左到右分佈,則此二叉樹被稱爲完全二叉樹。滿二叉樹也是完全二叉樹。
在這裏插入圖片描述

完全二叉樹性質:

完全二叉樹除了具有普通二叉樹的性質,它自身也具有一些獨特的性質,比如說,n 個結點的完全二叉樹的深度爲 ⌊log2n⌋+1。(⌊log2n⌋ 表示取小於 log2n 的最大整數。例如,⌊log24⌋ = 2,而 ⌊log25⌋ 結果也是 2。)

5.二叉樹的遍歷算法

層次遍歷介紹:

在上述樹的術語中,樹是有層次的。通過對樹中各層的節點從左到右依次遍歷(訪問),即可實現對這棵二叉樹的遍歷,此種方式稱爲層次遍歷。如下圖所示:
在這裏插入圖片描述
遍歷結果爲:1 2 3 4 5 6 7

先序遍歷介紹:

每遇到一個節點,先訪問,然後再遍歷其左、右子樹。即先根節點->遍歷左子樹->遍歷右子樹。(注意,這裏的根節點指的是每個節點是其子樹的根節點,而不是整棵樹的最頂上那個根節點。)
在這裏插入圖片描述
以該圖爲例,其先序遍歷算法訪問節點的先後次序爲:1 2 4 5 3 6 7

中序遍歷介紹:

遍歷左子樹->根節點->遍歷右子樹。以上圖爲例,中序遍歷算法訪問節點的次序爲:4 2 5 1 6 3 7

後序遍歷介紹:

遍歷左子樹->遍歷右子樹->根節點。以上圖爲例,後序遍歷訪問節點的次序爲:4 5 2 6 7 3 1

先序、中序、後續遍歷總結:

通過判斷根節點的訪問順序,確定其是前序、中序還是後序遍歷。

6.二叉樹的順序存儲結構

二叉樹的存儲結構有兩種,分別爲順序存儲和鏈式存儲。

順序存儲二叉樹介紹:

二叉樹的順序存儲,指的是使用順序表(數組)存儲二叉樹。需要注意的是,順序存儲只適用於完全二叉樹。換句話說,只有完全二叉樹纔可以使用順序表存儲。因此,如果我們想順序存儲普通二叉樹,需要提前將普通二叉樹轉化爲完全二叉樹。

普通二叉樹採用順序存儲:

順序存儲只適用於完全二叉樹,因此,如果我們想順序存儲普通二叉樹,需要提前將普通二叉樹轉化爲完全二叉樹。
普通二叉樹轉完全二叉樹的方法很簡單,只需給二叉樹額外添加一些無效數據節點,將其"拼湊"成完全二叉樹即可。
在這裏插入圖片描述

順序存儲的方法:

完全二叉樹的順序存儲,僅需從根節點開始,按照層次依次從左至右將樹中節點存儲到數組即可。
在這裏插入圖片描述
例如,存儲上圖所示的完全二叉樹,其存儲狀態在數組中如下圖所示:
在這裏插入圖片描述
同樣,存儲由普通二叉樹轉化來的完全二叉樹也是如此。例如,上述示例普通二叉樹的數組存儲狀態如下圖所示:
在這裏插入圖片描述

順序存儲二叉樹特點:

1.第i個元素的左子節點爲 2 * i + 1
2.第i個元素的右子節點爲 2 * i + 2
3.第i個元素的父節點爲 (i-1) / 2
i 表示二叉樹中第幾個節點,對應數組中從0開始編號。

順序存儲二叉樹的遍歷實現:

package tree;

/**
 * 順序存儲二叉樹
 */
public class ArrayBinaryTree {

    public static void main(String[] args) {
        int[] array = { 1, 2, 3, 4, 5, 6, 7 };
        ArrayBinaryTree arrBinaryTree = new ArrayBinaryTree(array);
        System.out.println("順序存儲二叉樹先序遍歷:");
        arrBinaryTree.preOrderTraverse(); // 1,2,4,5,3,6,7
        System.out.println("\n順序存儲二叉樹中序遍歷:");
        arrBinaryTree.inOrderTraverse(); // 4 2 5 1 6 3 7
        System.out.println("\n順序存儲二叉樹後序遍歷:");
        arrBinaryTree.postOrderTraverse(); // 4 5 2 6 7 3 1
    }

    private int[] array;

    public ArrayBinaryTree(int[] array){
        this.array = array;
    }

    /**
     * 重載preOrderTraverse
     */
    public void preOrderTraverse(){
        this.preOrderTraverse(0);
    }

    /**
     * 遞歸方式先序遍歷
     * @param index
     */
    private void preOrderTraverse(int index){
        if(array == null || array.length == 0) {
            throw new RuntimeException("空數組");
        }
        if (index >= array.length){
            return;
        }
        //輸出根節點
        System.out.print(array[index]+" ");
        //向左遞歸遍歷
        preOrderTraverse(2*index+1);
        //向右遞歸遍歷
        preOrderTraverse(2*index+2);
    }

    /**
     * 重載inOrderTraverse
     */
    public void inOrderTraverse(){
        this.inOrderTraverse(0);
    }

    /**
     * 遞歸方式中序遍歷
     * @param index
     */
    private void inOrderTraverse(int index){
        if(array == null || array.length == 0) {
            throw new RuntimeException("空數組");
        }
        if (index >= array.length){
            return;
        }
        //向左遞歸遍歷
        inOrderTraverse(2*index+1);
        //輸出根節點
        System.out.print(array[index]+" ");
        //向右遞歸遍歷
        inOrderTraverse(2*index+2);
    }

    /**
     * 重載postOrderTraverse
     */
    public void postOrderTraverse(){
        this.postOrderTraverse(0);
    }

    /**
     * 遞歸方式後序遍歷
     * @param index
     */
    private void postOrderTraverse(int index){
        if(array == null || array.length == 0) {
            throw new RuntimeException("空數組");
        }
        if (index >= array.length){
            return;
        }
        //向左遞歸遍歷
        postOrderTraverse(2*index+1);
        //向右遞歸遍歷
        postOrderTraverse(2*index+2);
        //輸出根節點
        System.out.print(array[index]+" ");
    }

}

7.二叉樹的鏈式存儲結構

在上述二叉樹的順序存儲,容易發現,其實二叉樹並不適合用數組存儲,因爲並不是每個二叉樹都是完全二叉樹,普通二叉樹使用順序表存儲或多或多會存在空間浪費的現象。

鏈式存儲二叉樹介紹:

在這裏插入圖片描述
如上圖,作爲一顆普通二叉樹,若將其採用鏈式存儲,則只需從樹的根節點開始,將各個節點及其左右孩子使用鏈表存儲即可。由此上圖對應的鏈式存儲結構如下圖所示:
在這裏插入圖片描述

鏈式存儲二叉樹的節點數據結構:

由上述示例圖可知,採用鏈式存儲二叉樹時,其節點結構由 3 部分構成:
1.指向左孩子節點的指針(Lchild);
2.節點存儲的數據(data);
3.指向右孩子節點的指針(Rchild);
在這裏插入圖片描述
表示該節點結構的 JAVA 語言代碼爲:

public class BiTNode{
   public int data;//數據域
   public BiTNode lchild, rchild;//左右孩子引用(指針)
}

順序存儲二叉樹的遍歷實現:

package tree;

/**
 * 二叉樹節點類
 */
class BiTNode{
    public int data;
    public BiTNode lchild, rchild;

    public BiTNode(int data){
        this.data = data;
    }
}

/**
 * 鏈式存儲二叉樹
 */
public class BinaryTree {

    public static void main(String[] args) {
        //創建節點,構造樹
        BiTNode node1 = new BiTNode(1);
        BiTNode node2 = new BiTNode(2);
        BiTNode node3 = new BiTNode(3);
        BiTNode node4 = new BiTNode(4);
        BiTNode node5 = new BiTNode(5);
        BiTNode node6 = new BiTNode(6);
        BiTNode node7 = new BiTNode(7);
        node1.lchild = node2;
        node1.rchild = node3;
        node2.lchild = node4;
        node2.rchild = node5;
        node3.lchild = node6;
        node3.rchild = node7;
        BinaryTree binaryTree = new BinaryTree(node1);
        System.out.println("鏈式存儲二叉樹先序遍歷:");
        binaryTree.preOrderTraverse(); // 1,2,4,5,3,6,7
        System.out.println("\n鏈式存儲二叉樹中序遍歷:");
        binaryTree.inOrderTraverse(); // 4 2 5 1 6 3 7
        System.out.println("\n鏈式存儲二叉樹後序遍歷:");
        binaryTree.postOrderTraverse(); // 4 5 2 6 7 3 1
    }

    private BiTNode root;

    public BinaryTree(BiTNode root){
        this.root = root;
    }

    /**
     * 重載preOrderTraverse
     */
    public void preOrderTraverse(){
        this.preOrderTraverse(this.root);
    }

    /**
     * 遞歸實現先序遍歷
     * @param node
     */
    public void preOrderTraverse(BiTNode node){
        if (node==null){
            return;
        }
        //輸出根節點
        System.out.print(node.data+" ");
        //向左遞歸遍歷
        preOrderTraverse(node.lchild);
        //向右遞歸遍歷
        preOrderTraverse(node.rchild);
    }

    /**
     * 重載inOrderTraverse
     */
    public void inOrderTraverse(){
        this.inOrderTraverse(this.root);
    }

    /**
     * 遞歸實現中序遍歷
     * @param node
     */
    public void inOrderTraverse(BiTNode node){
        if (node==null){
            return;
        }
        //向左遞歸遍歷
        inOrderTraverse(node.lchild);
        //輸出根節點
        System.out.print(node.data+" ");
        //向右遞歸遍歷
        inOrderTraverse(node.rchild);
    }

    /**
     * 重載postOrderTraverse
     */
    public void postOrderTraverse(){
        this.postOrderTraverse(this.root);
    }

    /**
     * 遞歸實現後序遍歷
     * @param node
     */
    public void postOrderTraverse(BiTNode node){
        if (node==null){
            return;
        }
        //向左遞歸遍歷
        postOrderTraverse(node.lchild);
        //向右遞歸遍歷
        postOrderTraverse(node.rchild);
        //輸出根節點
        System.out.print(node.data+" ");
    }

}

8.線索二叉樹

線索二叉樹前言:

二叉樹本身是一種非線性結構,採用任何一種遍歷二叉樹的方法,都可以得到樹中所有結點的一個線性序列。在這個序列中,除第一個節點外,每個節點都有自己的直接前趨;除最後一個節點外,每個節點都有一個直接後繼。
在這裏插入圖片描述
例如,上圖採用先序遍歷的方法得到的節點序列爲:1 2 4 5 3 6 7,在這個序列中,節點 2 的直接前趨結點爲 1,直接後繼節點爲 4。

線索二叉樹介紹:

當對上圖二叉樹進行中序遍歷時:4 2 5 1 6 3 7,容易發現一個問題,4 5 6 7這幾個葉子節點的左右指針,並沒有利用上;度爲1的節點也有一個空指針域。所以如果用二叉樹中空閒的內存空間記錄某些節點的前趨和後繼元素的位置(不是全部)。這樣在遍歷二叉樹時,就可以利用保存的節點信息,提高遍歷的效率。使用這種方法構建的二叉樹,即爲“線索二叉樹”。在有 n 個結點的二叉樹中必定存在 n+1 個空指針域。

線索二叉樹節點數據結構:

線索二叉樹中,如果結點有左子樹,則 lchild 指針域指向左孩子,否則 lchild 指針域指向該節點的前趨節點;同樣,如果節點有右子樹,則 rchild 指針域指向右孩子,否則 rchild 指針域指向該節點的後繼節點。
所以爲了避免指針域指向的節點的意義混淆,需要改變節點本身的結構,增加兩個標誌域:
在這裏插入圖片描述
leftType和 rightType爲標誌域:當值爲0時,表示對應left、right指針指向的是孩子節點;當值爲1時,表示對應left、right指針指向的是前驅、後繼節點。
 
表示該節點結構的 JAVA 語言代碼爲:

class Node{
   public int data;
   public Node left, right;
   //指向的子節點的類型:0-left指向左子樹 1-left前驅節點
   public int leftType;
   //指向的子節點的類型:0-right指向右子樹 1-right後繼節點
   public int rightType;
}

線索二叉樹遍歷代碼實現:

下圖二叉樹的中序遍歷結果爲:4 2 5 1 6 3 7,進行中序遍歷的線索化後,如圖所示:紅色箭頭指向後繼節點,藍色箭頭指向前驅節點。
在這裏插入圖片描述

package tree;

class Node{
    public int data;
    public Node left, right;
    //指向的子節點的類型:0-left指向左子樹 1-left前驅節點
    public int leftType;
    //指向的子節點的類型:0-right指向右子樹 1-right後繼節點
    public int rightType;

    public Node(int data){
        this.data = data;
    }
}

public class ThreadedBinaryTree {

    public static void main(String[] args) {
        //創建節點,構造樹
        Node node1 = new Node(1);
        Node node2 = new Node(2);
        Node node3 = new Node(3);
        Node node4 = new Node(4);
        Node node5 = new Node(5);
        Node node6 = new Node(6);
        Node node7 = new Node(7);
        node1.left = node2;
        node1.right = node3;
        node2.left = node4;
        node2.right = node5;
        node3.left = node6;
        node3.right = node7;
        ThreadedBinaryTree threadedBinaryTree = new ThreadedBinaryTree(node1);
        //將二叉樹進行線索化重新構建
        System.out.println("將二叉樹構建爲線索化二叉樹:");
        threadedBinaryTree.inOrderThreadedNodes();
        System.out.println("4的後繼節點:"+node4.right.data);
        System.out.println("5的前驅節點:"+node5.left.data+",後繼節點:"+node5.right.data);
        System.out.println("6的前驅節點:"+node6.left.data+",後繼節點:"+node6.right.data);
        System.out.println("7的前驅節點:"+node7.left.data);
        //遍歷
        System.out.println("線索化二叉樹中序遍歷:");
        threadedBinaryTree.inOrderTraverse();
    }

    //根節點
    private Node root;
    //輔助前驅節點
    private Node preNode;

    public ThreadedBinaryTree(Node root){
        this.root = root;
    }

    /**
     * 重載inOrderThreadedNodes
     */
    public void inOrderThreadedNodes(){
        this.inOrderThreadedNodes(this.root);
    }

    /**
     * 構建中序線索化二叉樹
     * @param node
     */
    public void inOrderThreadedNodes(Node node){
        if (node==null){
            return;
        }
        //線索化左子樹
        inOrderThreadedNodes(node.left);
        //線索化當前節點
        if (node.left==null){//處理前驅節點
            node.left = preNode;
            node.leftType = 1;
        }
        if (preNode!=null && preNode.right==null){//通過輔助前驅節點來處理後繼節點的指向
            preNode.right = node;
            preNode.rightType = 1;
        }
        //處理完前驅後繼節點後,當前節點成爲前驅節點
        preNode = node;
        //線索化右子樹
        inOrderThreadedNodes(node.right);
    }

    /**
     * 中序遍歷線索化二叉樹
     */
    public void inOrderTraverse(){
        //從當前節點開始中序遍歷
        Node node = root;
        while (node!=null){
            //當前節點node作爲它左右子樹的根節點,應該先輸出其左子樹,查找找到第一個有後繼節點的,一定是node的最底層的子樹節點
            while (node.leftType ==0){
                node = node.left;
            }
            System.out.print(node.data+" ");//輸出該節點node
            //有指向後繼節點的,可以直接遍歷輸出,當沒有後繼節點了退出循環
            while (node.rightType==1){
                node = node.right;
                System.out.print(node.data+" ");
            }
            //當前節點和當前節點的左子樹在前面遍歷後繼結點時都已經輸出了,中序遍歷左子樹和當前節點都輸出後便開始訪問右子樹
            node = node.right;
        }
    }

}

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