資料結構和演算法:二叉樹

小高飛發表於2020-10-03

二叉樹

二叉樹(Binary tree)是樹形結構的一個重要型別。許多實際問題抽象出來的資料結構往往是二叉樹形式,即使是一般的樹也能簡單地轉換為二叉樹,而且二叉樹的儲存結構及其演算法都較為簡單,因此二叉樹顯得特別重要。二叉樹特點是每個節點最多隻能有兩棵子樹,即樹的度最大為2,且有左右之分

二叉樹是n個有限元素的集合,該集合或者為空、或者由一個稱為根(root)的元素及兩個不相交的、被分別稱為左子樹和右子樹的二叉樹組成,是有序樹。當集合為空時,稱該二叉樹為空二叉樹。

特殊型別

滿二叉樹:二叉樹內只有度為2和0的節點,且度為0的節點在同一層,即除了樹的最後一層的節點沒有子節點,其他節點都有2個子節點,則這個二叉樹就是滿二叉樹。滿二叉樹的節點數量為2^k-1(k為樹的深度)

完全二叉樹:完全二叉樹的節點順序是由上到下,由左到右的。即葉子節點所在的層級差別不能大於1,且右節點不為空時,左節點也不能為空。滿二叉樹一定就是完全二叉樹,但完全二叉樹不一定是滿二叉樹

資料結構和演算法:二叉樹

程式碼例項

使用二叉樹來儲存水滸英雄好漢,模擬水滸英雄排名。

首先需要先建立二叉樹裡的節點,用於儲存水滸英雄的編號、名字和節點的左、右子節點。

//水滸英雄節點
class HeroNode{
    private int id;
    private String name;
    private HeroNode left;//節點的左子節點
    private HeroNode right;//節點的右子節點
    
    //3種遍歷樹的方法,在《資料結構與演算法:樹》的一章有過講解
    //前序遍歷(中-左-右)
    public void preOrder(){
        //先輸出該節點(中)
        System.out.println(this);
        //往左子節點遞迴遍歷子節點(左)
        if (this.left != null){
            this.left.preOrder();
        }
        //往右子節點遞迴遍歷子節點(右)
        if (this.right != null){
            this.right.preOrder();
        }
    }

    //中序遍歷(左-中-右)
    public void midOrder(){
        if (this.left != null){
            this.left.midOrder();
        }
        System.out.println(this);
        if (this.right != null){
            this.right.midOrder();
        }
    }

    //後序遍歷(左-右-中)
    public void postOrder(){
        if (this.left != null){
            this.left.postOrder();
        }
        if (this.right != null){
            this.right.postOrder();
        }
        System.out.println(this);
    }

    public HeroNode(int id, String name) {
        this.id = id;
        this.name = name;
    }

    public int getId() {
        return id;
    }

    public void setId(int id) {
        this.id = id;
    }

    public String getName() {
        return name;
    }

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

    public HeroNode getLeft() {
        return left;
    }

    public void setLeft(HeroNode left) {
        this.left = left;
    }

    public HeroNode getRight() {
        return right;
    }

    public void setRight(HeroNode right) {
        this.right = right;
    }

    @Override
    public String toString() {
        return "HeroNode{" +
                "id=" + id +
                ", name='" + name + '\'' +
                '}';
    }

}

有了節點,接下來就需要建立二叉樹類了,而二叉樹的起點是根節點,我們需要靠根節點才可以實現增刪改查的操作,所以二叉樹最重要的屬性就是根節點。

//二叉樹
class BinaryTree{
    private HeroNode root;//根節點

    //例項化根節點,即例項化二叉樹
    public void setRoot(HeroNode root) {
        this.root = root;
    }
    
      //因為是通過二叉樹去操作節點,所以要為節點的遍歷方法進行封裝呼叫
    public void preOrder(){
        if (this.root != null){
            root.preOrder();
        }else {
            System.out.println("二叉樹為空,無法遍歷");
        }
    }

    public void midOrder(){
        if (this.root != null){
            root.midOrder();
        }else {
            System.out.println("二叉樹為空,無法遍歷");
        }
    }

    public void postOrder(){
        if (this.root != null){
            root.postOrder();
        }else {
            System.out.println("二叉樹為空,無法遍歷");
        }
    }
}

接下來我們就可以建立一個二叉樹,因為我們沒有為二叉樹設定任何節點順序規則,所以這裡我們先手動為二叉樹新增節點,等到後面章節再講解根據規則來自動新增節點和刪除節點操作。

public class BinaryTreeDemo {
    public static void main(String[] args) {
        BinaryTree binaryTree = new BinaryTree();

        HeroNode root = new HeroNode(1, "宋江");
        HeroNode node1 = new HeroNode(2, "盧俊義");
        HeroNode node2 = new HeroNode(3, "吳用");
        HeroNode node3 = new HeroNode(4, "公孫勝");
        HeroNode node4 = new HeroNode(5, "關勝");

        //為各個節點設定關係
        root.setLeft(node1);
        root.setRight(node2);
        node2.setLeft(node4);
        node2.setRight(node3);
        //例項化二叉樹根節點
        binaryTree.setRoot(root);
        
        //使用3種遍歷方式遍歷二叉樹
        System.out.println("前序遍歷:");
        binaryTree.preOrder();
        System.out.println("中序遍歷:");
        binaryTree.midOrder();
        System.out.println("後序遍歷:");
        binaryTree.postOrder();
    }
}

如果我們要根據id查詢單個水滸英雄資訊時,只需將遍歷方法修改一下即可,下面以前序遍歷查詢為例:

//前序遍歷查詢(修改節點方法只需新增個修改引數,並把返回程式碼改成修改程式碼)
public HeroNode preOrderSearch(int id){
    //如果當前節點就是查詢的節點時,返回該節點(中)
    if (id == this.getId()){
        return this;
    }
    HeroNode result = null;
    //往左子節點遞迴遍歷查詢
    if (this.left != null){
        result = this.left.preOrderSearch(id);
    }
    //如果result不為空則表明已經查詢到該節點,直接返回result
    if (result != null){
        return result;
    }
    //往右子節點遞迴遍歷查詢
    if (this.right != null){
        result = this.right.preOrderSearch(id);
    }
    return result;
}

 

順序儲存二叉樹

二叉樹的儲存方式有倆種,一種是鏈式儲存(上面程式碼所演示的),另一種是順序儲存。順序儲存指的是用陣列來儲存二叉樹,即根據層級,從第一層到最後一層由左到右的順序儲存在陣列中。

順序儲存二叉樹的節點總數量:陣列的長度

下標為n的節點的左節點下標:n*2+1

下標為n的節點的右節點下標:n*2+2

下標為n的節點的父節點下標:(n-1)/2

資料結構和演算法:二叉樹

注意:順序儲存方式只能儲存完全二叉樹,如果要儲存非完全二叉樹,需將它轉換成完全二叉樹,即補值為0的節點,直到該二叉樹變成完全二叉樹

資料結構和演算法:二叉樹

程式碼例項

//順序儲存二叉樹
class ArrayBinaryTree{
    //順序儲存的陣列
    private int[] array;

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

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

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

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

    //前序遍歷
    public void preOrder(int index){
        if (array == null || array.length == 0){
            System.out.println("二叉樹陣列為空,無法遍歷");
            return;
        }
        //輸出該節點(中)
        System.out.println(array[index]);
        //往左遞迴遍歷(左)
        if (index*2+1 < array.length){//判斷左節是否為空
            preOrder(index*2+1);
        }
        //往右遞迴遍歷(右)
        if (index*2+2 < array.length){//判斷右節點是否為空
            preOrder(index*2+2);
        }
    }

    //中序遍歷
    public void midOrder(int index){
        if (array == null || array.length == 0){
            System.out.println("二叉樹陣列為空,無法遍歷");
            return;
        }
        if (index*2+1 < array.length){
            midOrder(index*2+1);
        }
        System.out.println(array[index]);
        if (index*2+2 < array.length){
            midOrder(index*2+2);
        }
    }

    //後序遍歷
    public void postOrder(int index){
        if (array == null || array.length == 0){
            System.out.println("二叉樹陣列為空,無法遍歷");
            return;
        }
        if (index*2+1 < array.length){
            postOrder(index*2+1);
        }
        if (index*2+2 < array.length){
            postOrder(index*2+2);
        }
        System.out.println(array[index]);
    }
}

 

線索化二叉樹

在上圖的二叉樹遍歷中,當我們遍歷到葉子節點時,需要一層一層的返回到下一個輸出的節點,效率並不高,那我們能不能直接讓葉子節點指向在對應遍歷方法中它的下一個節點呢?這時我們就需要線索化二叉樹了。

對於n個結點的二叉樹,在二叉鏈儲存結構中有n+1個空鏈域,利用這些空鏈域存放在某種遍歷次序下該結點的前驅結點和後繼結點的指標,這些指標稱為線索,加上線索的二叉樹稱為線索二叉樹。

這種加上了線索的二叉連結串列稱為線索連結串列,相應的二叉樹稱為線索二叉樹(Threaded BinaryTree)。根據線索性質的不同,線索二叉樹可分為前序線索二叉樹中序線索二叉樹後序線索二叉樹三種。

資料結構和演算法:二叉樹

程式碼例項

//中序線索化二叉樹
class ThreadedBinaryTree{
    private HeroNode2 root;
    //在中序線索化時,儲存前一個節點,預設為空
    private HeroNode2 pre;

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

    public void threadedNodes(){
        threadedNodes(root);
    }

    //中序線索化二叉樹
    public void threadedNodes(HeroNode2 node){
        //如果傳入的引數節點為空,即已經超出二叉樹範圍,則直接返回
        if (node == null){
            return;
        }

        /**
         * 按照中序遍歷(左中右)的順序,依次為各個節點線索化
         */
        //先向節點的左節點遞迴,依次線索化左節點
        threadedNodes(node.getLeft());

        //為節點線索化
        /*
        如果該節點的左節點為空,則將該節點的左節點設定為中序線索化的前一個節點pre,
        並將該節點的左節點型別leftType設定為前驅節點1
        注意: 在第一次線索化時,左節點會設定為空,但因為是中序線索化,所以不影響
         */
        if (node.getLeft() == null){
            node.setLeft(pre);
            node.setLeftType(1);
        }
        /*
        如果前一個節點pre的右節點為空,則將中序線索化的前一個節點pre的右節點設定為當前節點,
        並將pre節點的右節點型別設為後繼節點1
        邏輯:pre只有當它的右節點為空時才會進入線索化,而這時遞迴已經返回到中序遍歷時pre節點的
        下一個遍歷節點node,所以node就是在中序線索化中pre的下一個節點,即後繼節點
         */
        if (pre != null && pre.getRight() == null){
            pre.setRight(node);
            pre.setRightType(1);
        }
        //線索化後要將當前節點設定會上一個節點
        pre = node;

        //再向節點的右節點遞迴,依次線索化右節點
        threadedNodes(node.getRight());
    }

    //遍歷中序線索化二叉樹(即輸出中序遍歷的結果)
    public void threadedList(){
        HeroNode2 node = root;

        //當node為空時,即中序線索化二叉樹已經遍歷完成
        while (node != null){
            //如果左節點的型別是子樹0的話,則繼續往節點的左節點走
            while (node.getLeftType() == 0){
                node = node.getLeft();
            }
            //當退出迴圈時,即node沒有左子節點了,則輸出node節點
            System.out.println(node);

            //如果右節點的型別是後繼節點1時,則直接跳到後繼節點並輸出
            while (node.getRightType() == 1){
                node = node.getRight();
                System.out.println(node);
            }
            //當退出迴圈時,即node還有右子節點,則繼續往節點的右子節點走
            node = node.getRight();
        }
    }
}

//線索化二叉樹節點
/**
 * 因為二叉樹線索化後,
 * 左節點的指向有兩種情況:指向左子樹或指向前驅節點;右節點的指向也有兩種情況:指向右子樹或指向後繼節點;
 * 所以要分別為左、右節點建立一個屬性,該屬性可以說明左、右節點的指向情況。
 */
class HeroNode2{
    private int id;
    private String name;
    private HeroNode2 left;
    private HeroNode2 right;
    //左指向的型別,當為0時,指向的是左子樹,當為1時,指向的是前驅節點
    private int leftType;
    //右指向的型別,當為0時,指向的是右子樹,當為1時,指向的是後繼節點
    private int rightType;

    public HeroNode2(int id, String name) {
        this.id = id;
        this.name = name;
    }

    public int getId() {
        return id;
    }

    public void setId(int id) {
        this.id = id;
    }

    public String getName() {
        return name;
    }

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

    public HeroNode2 getLeft() {
        return left;
    }

    public void setLeft(HeroNode2 left) {
        this.left = left;
    }

    public HeroNode2 getRight() {
        return right;
    }

    public void setRight(HeroNode2 right) {
        this.right = right;
    }

    public int getLeftType() {
        return leftType;
    }

    public void setLeftType(int leftType) {
        this.leftType = leftType;
    }

    public int getRightType() {
        return rightType;
    }

    public void setRightType(int rightType) {
        this.rightType = rightType;
    }

    @Override
    public String toString() {
        return "HeroNode2{" +
                "id=" + id +
                ", name='" + name + '\'' +
                '}';
    }
}

 

 

 

相關文章