1. 程式人生 > 實用技巧 >淺入淺出二叉樹

淺入淺出二叉樹


樹的概述

樹是一種重要的非線性資料結構,直觀地看,它是資料元素(在樹中稱為結點)按分支關係組織起來的結構,很象自然界中的樹那樣。形同下圖。

樹有如下基本概念:

  • 根結點

    根結點是樹的一個組成部分,也叫樹根。每一顆樹都有且僅有一個根結點。它是同一棵樹中除本身外所有結點的祖先,沒有父結點。按上圖的樹結構來看,根節點就是 1。

  • 父結點

    也叫雙親結點,一個結點如果有上一級,則稱這個上一級是它的父結點,如果沒有上一級,則該結點無父結點。按上圖的樹結構來看,可以說是父節點有 1、2、3、5、6、22。

  • 子結點

    一個結點如果有下一級,則稱這個下一級是它的子結點,如果沒有下一級,則該結點無子結點。按上圖的樹結構來看,其實除了根結點以外,各個結點都是它們對應父結點的子結點。

  • 路徑

    從根結點訪問其他結點所需要經過的結點。比如從 1 要走到 31,則路徑是 1、3、31。

  • 結點的度

    一個結點擁有多少個子結點,就認為它的度是多少。比如根結點 1,它的度就是 5。

  • 結點的權

    結點的權值,圖中每個結點都有對應的數字,這些數字就是對應結點的權。

  • 葉子結點

    沒有子結點的結點,按上圖的樹結構來看,葉子結點有 21、221、222、223、31、51、52、61。

  • 子樹

    還是上圖,試著把結點 2 單獨拿出來看,會發現它和它的子結點也能構成樹結構。這顆樹在整顆大的樹裡邊,所以稱為子樹。

  • 結點的層次從根開始定義起,根為第一層,根的孩子是二層,依次累計。上圖的樹結構層次就是 4。

  • 樹的高度

    樹的最大層數,上圖的樹結構最大層數就是從根結點開始,到最底層的葉子結點,高度是 4。

  • 森林

    多個樹組成的集合,想象現在有好多顆樹,每一顆樹結構不一,它們共同構成森林。


二叉樹的概述

任何子結點的數量都不超過 2,就是一顆二叉樹。比如之前舉例的圖,明顯就不是二叉樹。二叉樹的子結點分左結點和右結點,不能隨意顛倒位置。

二叉樹也有分類:

  • 滿二叉樹

    所有葉子結點都在最後一層,而且結點總數為 2^n - 1,n 是樹的高度。

  • 完全二叉樹

    所有葉子結點都在最後一層或倒數第二層,且最後一層葉子結點在左邊延續,倒數第二層的葉子結點在右邊連續。即最後一層的葉子結點總是從左往右,倒數第二層總是從右到左。滿二叉樹也是一顆完全二叉樹。


鏈式儲存的二叉樹

顧名思義,用連結串列的方式去實現二叉樹結構。用程式碼去實現,首先我們要建立一個結點類。

public class TreeNode {
    // 節點的權
    private int value;
    // 左子結點
    private TreeNode leftNode;
    // 右子結點
    private TreeNode rightNode;

    public TreeNode(int value) {
        this.value = value;
    }
	// 剩下的都是 set/get 方法了
    ...
}

其次,和連結串列有頭結點一樣,二叉樹也需要有根結點輔助操作,作為建立二叉樹的基礎。

public class BinaryTree {

    private TreeNode root;

    public TreeNode getRoot() {
        return root;
    }

    public void setRoot(TreeNode root) {
        this.root = root;
    }
}

萬事具備,向根結點新增左右子結點即可。

public class TestBinaryTree {

    public static void main(String[] args) {
        // 建立二叉樹
        BinaryTree binaryTree = new BinaryTree();
        // 建立根結點
        TreeNode root = new TreeNode(1);
        // 設定根結點
        binaryTree.setRoot(root);
        // 建立一個左結點
        TreeNode rootL = new TreeNode(2);
        // 把新建立的結點設定為根結點的左子結點
        root.setLeftNode(rootL);
        // 建立一個右結點
        TreeNode rootR = new TreeNode(3);
        // 把新建立的結點設定為根結點的右子結點
        root.setRightNode(rootR);
        // 為第二層的左結點建立兩個子結點
        rootL.setLeftNode(new TreeNode(4));
        rootL.setRightNode(new TreeNode(5));
        // 為第二層的右結點建立兩個子結點
        rootR.setLeftNode(new TreeNode(6));
        rootR.setRightNode(new TreeNode(7));
    }
}

遍歷二叉樹

二叉樹的遍歷方式有三種,分別是:前序遍歷、中序遍歷、後序遍歷。

以上圖為例,記住一點,所謂的前序、中序、後序都是參考當前結點的位置。前序遍歷,即是先取當前結點的權,然後是它的左子結點,最後是右子結點。從根結點開始,遍歷過程中每一個結點都要遵守這個規矩。

因此上圖前序遍歷得到的結果是:1、2、4、5、3、6、7;中序遍歷得到的結果是:4、2、5、1、6、3、7;後序遍歷得到的結果是:4、5、2、6、3、7、1。程式碼實現用到遞迴的思想。

public class TestBinaryTree {

    public static void main(String[] args) {
        // 之前建立二叉樹的程式碼,這裡就省略不寫了
        ...
        // 前序遍歷樹
        binaryTree.frontShow();
        System.out.println("-----------------");
        // 中序遍歷樹
        binaryTree.midShow();
        System.out.println("-----------------");
        // 後序遍歷樹
        binaryTree.afterShow();
    }
}
public class BinaryTree {

    private TreeNode root;

    public TreeNode getRoot() {
        return root;
    }

    public void setRoot(TreeNode root) {
        this.root = root;
    }

    // 前序遍歷
    public void frontShow() {
        if (root != null) {
        	root.frontShow();
        }
    }

    // 中序遍歷
    public void midShow() {
        if (root != null) {
        	root.midShow();
        }
    }

    // 後序遍歷
    public void afterShow() {
        if (root != null) {
        	root.afterShow();
        }
    }
}
public class TreeNode {

    // 節點的權
    private int value;
    // 左子結點
    private TreeNode leftNode;
    // 右子結點
    private TreeNode rightNode;

    public TreeNode(int value) {
        this.value = value;
    }

    // set/get 方法
	...

    // 前序遍歷
    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 class TestBinaryTree {

    public static void main(String[] args) {
		// 建立二叉樹
        ...
        // 前序查詢
        TreeNode result = binaryTree.frontSearch(5);
        System.out.println(result);
    }
}

public class BinaryTree {

    private TreeNode root;

    public TreeNode getRoot() {
        return root;
    }

    public void setRoot(TreeNode root) {
        this.root = root;
    }

    /**
     * 前序查詢
     * @return 目標結點
     */
    public TreeNode frontSearch(int value) {

        return root.frontSearch(value);
    }
}
public class TreeNode {

    // 節點的權
    private int value;
    // 左子結點
    private TreeNode leftNode;
    // 右子結點
    private TreeNode rightNode;

    public TreeNode(int value) {
        this.value = value;
    }

    /**
     * 前序查詢
     * @return 目標結點
     */
    public TreeNode frontSearch(int value) {
        TreeNode target = null;
        // 返回本結點
        if (this.value == value) {
            return this;
        }
        // 向左子結點方向查詢
        if (leftNode != null) {
            target = leftNode.frontSearch(value);
        }
        // 如果不為空,證明找到結點,返回
        if (target != null) {
            return target;
        }
        // 向右子結點方向查詢
        if (rightNode != null) {
            target = rightNode.frontSearch(value);
        }
        return target;
    }
}

刪除二叉樹的結點

對於一顆普通的二叉樹而言,刪除一個結點就等同於把對應的整顆子樹一併刪掉。之後講到二叉排序樹時,就不是這樣操作了。

刪除時要區分是根結點還是其他結點。如果是根結點的話,直接置為 null 就好了。但如果不是,則依次比較左右兩個子結點,符合就直接置為 null。如果都不符合,那就遞迴呼叫左右結點的 delete 方法。

public class TestBinaryTree {

    public static void main(String[] args) {
		// 建立一顆子樹
        ...
        // 刪除一個結點
        binaryTree.delete(5);
        binaryTree.frontShow();
    }
}
public class BinaryTree {

    private TreeNode root;

	...

    /**
     * 根據權值刪除結點
     * @param value 依據權值
     */
    public void delete(int value) {
        // 要刪除的是根結點
        if (root.getValue() == value) {
            root = null;
        } else {
            // 要刪除的是其他結點
            root.delete(value);
        }
    }
}
public class TreeNode {

    // 節點的權
    private int value;
    // 左子結點
    private TreeNode leftNode;
    // 右子結點
    private TreeNode rightNode;

	...

    /**
     * 根據權值刪除結點
     * @param value 依據權值
     */
    public void delete(int value) {
        TreeNode parent = this;
        // 判斷左子結點
        if (parent.leftNode != null && parent.leftNode.value == value) {
            parent.leftNode = null;
            return;
        }
        // 判斷右子結點
        if (parent.rightNode != null && parent.rightNode.value == value) {
            parent.rightNode = null;
            return;
        }
        parent = leftNode;
        if (parent != null) {
            parent.delete(value);
        }
        parent = rightNode;
        if (parent != null) {
            parent.delete(value);
        }
    }
}

順序儲存的二叉樹

二叉樹還可以用陣列實現,或者說,任意一個數組都可以轉化為二叉樹。就上圖二叉樹而言,它對應的陣列實現就是 [1,2,3,4,5,6,7]。

並不是每顆二叉樹都長得這麼規矩,有可能會出現缺胳膊少腿的情況。通常情況下,順序儲存的二叉樹只考慮完全二叉樹(滿二叉樹也是完全二叉樹),否則沒有意義。

順序儲存的二叉樹還有其對應的性質公式,常用的有如下三個:

  • 陣列中第 n 個元素的左子結點下標為:2*n + 1
  • 陣列中第 n 個元素的左子結點下標為:2*n + 2
  • 陣列中第 n 個元素的父節點下標為:(n-1)/ 2

順序儲存的二叉樹的遍歷

我們把一個數組當成二叉樹作前序遍歷,剩下的中序和後序遍歷也大同小異了。

public class TestBinaryTree {

    public static void main(String[] args) {
		
        int[] data = new int[]{1, 2, 3, 4, 5, 6, 7};
        ArrayBinaryTree arrayBinaryTree = new ArrayBinaryTree(data);
        // 前序遍歷
        arrayBinaryTree.frontShow();

    }
}
public class ArrayBinaryTree {

    private int[] data;

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

    public int[] getData() {
        return data;
    }

    public void setData(int[] data) {
        this.data = data;
    }

    public void frontShow() {
        frontShow(0);
    }

    public void frontShow(int index) {
        if (data == null || data.length == 0) {
            return;
        }
        // 先遍歷當前結點的內容
        System.out.println(data[index]);
        // 遍歷左子結點
        if (2 * index + 1 < data.length) {
            frontShow(2 * index + 1);
        }
        // 遍歷右子結點
        if (2 * index + 2 < data.length) {
            frontShow(2 * index + 2);
        }
    }
}