數據結構與算法學習十七:順序儲存二叉樹、線索化二叉樹

一、順序儲存二叉樹

1.1 概念

從數據存儲來看,數組存儲方式和樹的存儲方式可以相互轉換,即數組可以轉換成樹,樹也可以轉換成數組,
看下面的示意圖。
在這裏插入圖片描述

1.2 特點

  • 順序二叉樹通常只考慮 完全二叉樹:
  1. 第n個元素的左子節點爲 2 * n + 1
  2. 第n個元素的右子節點爲 2 * n + 2
  3. 第n個元素的父節點爲 (n-1) / 2
  • n : 表示二叉樹中的第幾個元素(按0開始編號如圖所示,同時對應的也是數組的順序)
    在這裏插入圖片描述

複習一下完全二叉樹:如果該二叉樹的所有葉子節點都在最後一層或者倒數第二層,而且最後一層的葉子節點在左邊連續,倒數第二層的葉子節點在右邊連續,我們稱爲 完全二叉樹

1.3 關係

樹的存儲方式可以與數組的方式可以互換,我看出來的一個規律,二叉樹的層次遍歷就是數組的儲存方式。

如果要將二叉樹轉爲數組存儲,既可以層次遍歷然後儲存到數組中即可。

1.4 案例

  1. 上圖的二叉樹的結點,要求以數組的方式來存放 array : [1, 2, 3, 4, 5, 6, 7]
  2. 要求在遍歷數組 array 時,仍然可以以 前序遍歷,中序遍歷和後序遍歷 的方式完成結點的遍歷。
  3. 手寫三種遍歷,與代碼進行比較
    前序遍歷:1 2 4 5 3 6 7
    中序遍歷:4 2 5 1 6 3 7
    後序遍歷:4 5 2 6 7 3 1

1.5 代碼實現

1.5.1 ArrayBinaryTree 二叉樹類

package com.feng.ch12_tree.t2_arraybinarytree;

/*
 * 編寫一個 ArrayBinaryTree, 實現順序存儲二叉樹遍歷
 * 數組 和 二叉樹的 對應規律
 * */
class ArrayBinaryTree {

    private int[] array; // 存儲數據結點的數組

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

    // 重載perOrder()
    public void preOrder() {
        this.preOrder(0);
    }
    public void infixOrder() {
        this.infixOrder(0);
    }
    public void postOrder() {
        this.postOrder(0);
    }



    /*
     * 編寫一個方法,完成順序儲存二叉樹的一個前序遍歷
     * @param index 數組的下標
     * */
    public void preOrder(int index) {
        // 如果數組爲空,或者 array。length = 0
        if (array == null || array.length == 0) {
            System.out.println("數組爲空,不能按照二叉樹的前序遍歷");
        }
        // 輸出當前這個元素
        System.out.print(array[index] + " ");
        // 向左遞歸遍歷
        if ((index * 2 + 1) < array.length) {
            preOrder(2 * index + 1);
        }

        // 向右遞歸遍歷
        if ((index * 2 + 2) < array.length) {
            preOrder(2 * index + 2);
        }
    }

    /*
     * 方法,完成順序儲存二叉樹的一箇中序遍歷
     * @param index 數組的下標
     * */
    public void infixOrder(int index){
        // 如果數組爲空,或者 array。length = 0
        if (array == null || array.length == 0) {
            System.out.println("數組爲空,不能按照二叉樹的前序遍歷");
        }
        // 向左遞歸遍歷
        if ((index * 2 + 1) < array.length) {
            infixOrder(2 * index + 1);
        }
        // 輸出當前這個元素
        System.out.print(array[index] + " ");
        // 向右遞歸遍歷
        if ((index * 2 + 2) < array.length) {
            infixOrder(2 * index + 2);
        }

    }

    /*
     * 方法,完成順序儲存二叉樹的一個後序遍歷
     * @param index 數組的下標
     * */
    public void postOrder(int index){
        // 如果數組爲空,或者 array。length = 0
        if (array == null || array.length == 0) {
            System.out.println("數組爲空,不能按照二叉樹的前序遍歷");
        }
        // 向左遞歸遍歷
        if ((index * 2 + 1) < array.length) {
            postOrder(2 * index + 1);
        }
        // 向右遞歸遍歷
        if ((index * 2 + 2) < array.length) {
            postOrder(2 * index + 2);
        }
        // 輸出當前這個元素
        System.out.print(array[index] + " ");
    }
}

1.5.2 T2_ArrayBinaryTreeMain 測試類

package com.feng.ch12_tree.t2_arraybinarytree;

/*
* 順序儲存二叉樹
*
* 從數據存儲來看,數組存儲方式和樹的存儲方式可以相互轉換,即數組可以轉換成樹,樹也可以轉換成數組
* 對應的規律爲:
* 1、第n個元素的左子節點爲  2 * n + 1
* 2、第n個元素的右子節點爲  2 * n + 2
* 3、第n個元素的父節點爲  (n-1) / 2
* 4、注意 上面的 n : 表示二叉樹中的第幾個元素(按0開始編號)比如 根節點 的索引 n = 0;
* 也是對應數組裏的元素,下標正好也是從0開始的。
*
* 這裏將使用數組 來 儲存二叉樹,進行前序、後序、中序的排列。
*
* 擴展、思考:
* 二叉樹轉化爲數組儲存,其實就是二叉樹的 層次遍歷 後的結果。
* */
public class T2_ArrayBinaryTreeMain {

    public static void main(String[] args) {
//        int array[] = {1, 3, 4, 6, 8, 9, 15};
        int array[] = {1, 3, 4, 6, 8, 9, 15};

        ArrayBinaryTree arrayBinaryTree = new ArrayBinaryTree(array);
        System.out.println("前序序遍歷:");
        arrayBinaryTree.preOrder(); // 1 3 6 8   4 9 15
        System.out.println();

        System.out.println("中序遍歷:");
        arrayBinaryTree.infixOrder(); // 6 3 8 1 9 4 15
        System.out.println();

        System.out.println("後序遍歷:");
        arrayBinaryTree.postOrder(); // 6 8 3 9 15 4 1
    }
}

1.5.3 測試結果

在這裏插入圖片描述
在這裏插入圖片描述

1.6 ** 實際應用 **

八大排序算法中的堆排序,就會使用到順序存儲二叉樹, 關於堆排序,我們放在<<樹結構實際應用>> 章節講解。

二、線索化二叉樹(學的不太好,沒深入)

2.1 先看一個問題

將數列 {1, 3, 6, 8, 10, 14 } 構建成一顆二叉樹. n+1=7, 7個空指針域
在這裏插入圖片描述
問題分析:

  1. 當我們對上面的二叉樹進行中序遍歷時,數列爲 {8, 3, 10, 1, 14, 6 }
  2. 但是 6, 8, 10, 14 這幾個節點的 左右指針,並沒有完全的利用上.
  3. 如果我們希望充分的利用 各個節點的左右指針, 讓各個節點可以指向自己的前後節點,怎麼辦?
  4. 解決方案-線索二叉樹

2.1 線索二叉樹基本介紹

線索二叉樹基本介紹

  1. n個結點的二叉鏈表中含有n+1 【公式 2n-(n-1)=n+1】 個空指針域。利用二叉鏈表中的空指針域,存放指向該結點在某種遍歷次序下的前驅和後繼結點的指針(這種附加的指針稱爲"線索")

  2. 這種加上了線索的二叉鏈表稱爲線索鏈表,相應的二叉樹稱爲線索二叉樹(Threaded BinaryTree)。根據線索性質的不同,線索二叉樹可分爲前序線索二叉樹、中序線索二叉樹和後序線索二叉樹三種

  3. 一個結點的前一個結點,稱爲 前驅結點

  4. 一個結點的後一個結點,稱爲 後繼結點

2.2 案例

應用案例說明:將下面的二叉樹,進行中序線索二叉樹。中序遍歷的數列爲 {8, 3, 10, 1, 14, 6}
在這裏插入圖片描述
思路分析:中序遍歷的結果:{8, 3, 10, 1, 14, 6}
在這裏插入圖片描述
說明: 當線索化二叉樹後,Node節點的 屬性 left 和 right ,有如下情況:

  1. left 指向的是左子樹,也可能是指向的前驅節點. 比如 ① 節點 left 指向的左子樹, 而 ⑩ 節點的 left 指向的就是 前驅節點.
  2. right指向的是右子樹,也可能是指向後繼節點,比如 ① 節點right 指向的是右子樹,而⑩ 節點的right 指向的是 後繼節點.

2.3 代碼實現

2.3.1 HeroNode 結點類

package com.feng.ch12_tree.t3_threadedbinarytree;

/*
 * 創建 HeroNode 結點
 * */
class HeroNode {
    private int no;
    private String name;
    private HeroNode left;
    private HeroNode right;

    /*
     * 說明
     * 1. 如果leftType == 0 表示指向的是左子樹, 如果 1 則表示指向前驅結點
     * 2. 如果rightType == 0 表示指向是右子樹, 如果 1表示指向後繼結點
     * */
    private int leftType;
    private int rightType;

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

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

    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;
    }

    public int getNo() {
        return no;
    }

    public void setNo(int no) {
        this.no = no;
    }

    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;
    }
}

2.3.2 ThreadedBinaryTree 線索化二叉樹

package com.feng.ch12_tree.t3_threadedbinarytree;

/*
 * 定義 ThreadedBinaryTree 實現了線索化功能的二叉樹
 * */
class ThreadedBinaryTree {
    private HeroNode root;

    // 爲了實現線索化,需要創建要給指向當前結點的前驅結點的指針
    // 在遞歸進行線索化時,pre 總是要保留前一個結點
    private HeroNode pre = null;

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

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

    /*
     * 編寫對二叉樹 進行中序線索化的方法
     *
     * @param node 就是當前需要線索化的結點
     * */
    public void threadedNodes(HeroNode node) {
        // node == null。不能線索化
        if (node == null) {
            return;
        }
        // (1) 先線索化左子樹
        threadedNodes(node.getLeft());

        // (2) 再線索化當前結點
        // 2.1 先處理當前結點的前驅結點
        // 以8結點來理解: 8結點的.left = null , 8結點的.leftType = 1
        if (node.getLeft() == null) {
            // 讓當前結點的左指針指向前驅結點
            node.setLeft(pre);
            // 修改當前結點的左指針的類型,指向前驅結點
            node.setLeftType(1);
        }
        // 2.2 處理後繼節點
        if (pre != null && pre.getRight() == null) {
            // 前驅結點的右指針指向當前結點
            pre.setRight(node);
            // 修改前驅結點的右指針類型
            pre.setRightType(1);
        }
        // !!! 每處理一個結點後,讓當前結點是下一個結點的前驅結點
        pre = node;

        // (3) 先線索化右子樹
        threadedNodes(node.getRight());
    }

    /*
     * 中序遍歷 線索化二叉樹的方法
     * */
    public void threadedInfixList() {
        // 定義一個變量,儲存當前遍歷的節點,從root開始
        HeroNode node = root;
        if (root == null) {
            System.out.println("鏈表爲空,無法遍歷");
        }
        while (node != null) {
            // 循環的找到 leftType == 1 的結點,第一個找到就是 8 結點
            // 後面隨着遍歷而變化,因爲當 leftType== 1 時。說明該結點是按照線索化
            // 處理後的有效節點
            while (node.getLeftType() == 0) {
                node = node.getLeft();
            }

            // 打印當前這個結點
            System.out.println(node);

            // 如果當前結點的右指針指向的是後繼結點,就一直輸出
            while (node.getRightType() == 1) {
                // 獲取到當前結點的後繼結點
                node = node.getRight();
                System.out.println(node);
            }
            // 替換這個遍歷的節點
            node = node.getRight();
        }
    }

    /*
     * 前序遍歷 線索化二叉樹的方法
     * */
    public void threadedPreList() {
        // 定義一個變量,儲存當前遍歷的節點,從root開始
        HeroNode node = root;
        if (root == null) {
            System.out.println("鏈表爲空,無法遍歷");
        }

        while (node != null) {
            // 打印當前這個結點
            System.out.println(node);

            // 循環的找到 leftType == 1 的結點,第一個找到就是 8 結點
            // 後面隨着遍歷而變化,因爲當 leftType == 1 時。說明該結點是按照線索化
            // 處理後的有效節點
            while (node.getLeftType() == 0) {
                node = node.getLeft();
                System.out.println(node);
            }

            // 如果當前結點的右指針指向的是後繼結點,就一直輸出
            while (node.getRightType() == 1) {
                // 獲取到當前結點的後繼結點
//                System.out.println(node);
                node = node.getRight();
            }
            // 替換這個遍歷的節點
            node = node.getRight();
        }
    }

    /*
     * 後序遍歷 線索化二叉樹的方法
     * */
    public void threadedPostList() {
        // 定義一個變量,儲存當前遍歷的節點,從root開始
        HeroNode node = root;
        if (root == null) {
            System.out.println("鏈表爲空,無法遍歷");
        }

        while (node != null) {
            // 循環的找到 leftType == 1 的結點,第一個找到就是 8 結點
            // 後面隨着遍歷而變化,因爲當 leftType == 1 時。說明該結點是按照線索化
            // 處理後的有效節點
            while (node.getLeftType() == 0) {
                node = node.getLeft();
            }

            // 如果當前結點的右指針指向的是後繼結點,就一直輸出
            while (node.getRightType() == 1) {
                // 獲取到當前結點的後繼結點
                System.out.println(node);
                node = node.getRight();
            }

            // 打印當前這個結點
            System.out.println(node);
            // 替換這個遍歷的節點
            node = node.getRight();
        }
    }
}

2.3.3 T3_ThreadedBinaryTreeMain 測試類

package com.feng.ch12_tree.t3_threadedbinarytree;

/*
 * 線索二叉樹
 *
 * 1、n個結點的二叉鏈表中含有n+1  【公式 2n-(n-1)=n+1】 個空指針域。利用二叉鏈表中的空指針域,
 * 存放指向該結點在某種遍歷次序下的前驅和後繼結點的指針(這種附加的指針稱爲"線索")
 * 2、這種加上了線索的二叉鏈表稱爲線索鏈表,相應的二叉樹稱爲線索二叉樹(Threaded BinaryTree)。
 * 根據線索性質的不同,線索二叉樹可分爲前序線索二叉樹、中序線索二叉樹和後序線索二叉樹三種
 * 3、一個結點的前一個(左子)結點,稱爲前驅結點;一個結點的後一個(右子)結點,稱爲後繼結點
 *
 * 一旦線索化後,原來的遍歷方式不能使用,從新編寫 遍歷方法,無需使用遞歸
 *
 * */
public class T3_ThreadedBinaryTreeMain {

    public static void main(String[] args) {
        // 測試 中序線索二叉樹的功能
        HeroNode root = new HeroNode(1, "tom");
        HeroNode node02 = new HeroNode(3, "jack");
        HeroNode node03 = new HeroNode(6, "smith");
        HeroNode node04 = new HeroNode(8, "mary");
        HeroNode node05 = new HeroNode(10, "king");
        HeroNode node06 = new HeroNode(14, "dim");

        // 二叉樹,後面我們要遞歸創建,現在簡單處理,使用手動創建
        root.setLeft(node02);
        root.setRight(node03);
        node02.setLeft(node04);
        node02.setRight(node05);
        node03.setLeft(node06);

        // 測試線索化
        ThreadedBinaryTree tree = new ThreadedBinaryTree();
        tree.setRoot(root);
        tree.threadedNodes();

        // 測試 : 以10號爲結點
        HeroNode leftNode05 = node05.getLeft();

        HeroNode rightNode05 = node05.getRight();
        System.out.println("10號結點的前驅結點是=" + leftNode05);
        System.out.println("10號結點的後繼結點是=" + rightNode05);

        /*
         * 測試遍歷
         * 這裏的遍歷 不能使用 T1_BinaryTreeMain.java 類中遍歷方法了。爲什麼的話 去看代碼即可。重新寫
         * */
        System.out.println("使用線索化的方法 中序遍歷 線索化二叉樹 ");
        tree.threadedInfixList(); //8, 3, 10, 1, 14, 6
        System.out.println();

        System.out.println("使用線索化的方法 前序遍歷 線索化二叉樹 ");
        tree.threadedPreList(); //1 3 8 10 6 14
        System.out.println();

        /*
         * 這裏不對 有問題。
         * */
        System.out.println("使用線索化的方法 後序遍歷 線索化二叉樹 ");
        tree.threadedPostList(); //8 10 3 14 6 1
        System.out.println();

    }

}


2.3.4 測試結果

在這裏插入圖片描述
後序線索化有點問題。。。。

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