數據結構與算法——二叉樹


為什么需要樹這種數據結構?

數組存儲方式的分析

  • 優點:
    • 通過 下標 方式訪問元素,速度快
    • 對於 有序數組,還可以使用二分查找提高檢索速度
  • 缺點:如果無序數組要檢索具體某個值,或插入值(按一定順序)會整體移動,並且數組的大小固定,如果該數組已經滿了但又想插入數據,那就必須對該數組進行擴容,效率較低,如下的示意圖

鏈表存儲方式的分析

  • 優點:在一定程度上對數組存儲方式有優化

    例如:插入一個數值節點,只需要將插入節點,鏈接到鏈表中即可,同理,刪除效率也很好

  • 缺點:檢索效率較低

    需要從頭結點開始遍歷查找。

簡單說:

  • 數組訪問快,增刪慢
  • 鏈表增刪快,訪問慢

所以就出現了 這種數據結構。如下圖就是二叉樹模型

樹 存儲數據方式分析

提供數據 存儲讀取 效率。

例如:利用 二叉排序樹(Binary Sort Tree),既可以保證數據的檢索速度,同時也可以保證數據的插入刪除修改 的速度

如圖所示:

  • 插入時,小的數在 左節點、大的數在 右節點
  • 查找時:根據插入事的特性,基本上就類似折半查找了,每次都過濾一半的節點
  • 刪除時:只需要移動相鄰的節點的引用

樹 的常用術語

  • 節點:每一個圓圈表示一個節點,也稱節點對象

  • 根節點:最上面,最頂部的那個節點,也就是一棵樹的入口

  • 父節點:有子節點的節點

  • 子節點:看圖

  • 兄弟節點:具有相同父節點的節點互稱為兄弟節點

  • 葉子節點:沒有子節點的節點

  • 非終端節點分支節點:度不為零的節點

  • 節點的度:一個節點含有的子樹的個數稱為該節點的度

  • 樹的度:一棵樹中,最大的節點度稱為樹的度

  • 節點的權:可以簡單的理解為節點值

    有時候也用 路徑 來表示

  • 路徑:從 root 節點找到該節點的路線

  • :看圖

  • 子樹:有子節點的父子兩層就可以稱為是一個子樹

  • 樹的高度:最大層數

  • 森林:多棵子樹構成森林

二叉樹的概念

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

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

定義:

二叉樹(binary tree)是指樹中節點的度不大於2的有序樹,它是一種最簡單且最重要的樹。二叉樹的遞歸定義為:二叉樹是一棵空樹,或者是一棵由一個根節點和兩棵互不相交的,分別稱作根的左子樹和右子樹組成的非空樹;左子樹和右子樹又同樣都是二叉樹。

  • 二叉樹的子節點分為 左節點右節點

  • 如果該二叉樹的所有 葉子節點 都在 最后一層,並且 節點總數 = 2^n -1 (n 為層數),則我們稱為 滿二叉樹

  • 如果該二叉樹的所有葉子節點都在最 后一層或倒數第二層,而且 最后一層的葉子節點在左邊連續倒數第二層的葉子節點在右邊連續,我們稱為 完全二叉樹

    詳細點的解析:

    一棵深度為k且有

    img

    個結點的二叉樹稱為滿二叉樹。

    根據二叉樹的性質2, 滿二叉樹每一層的結點個數都達到了最大值, 即滿二叉樹的第i層上有

    img

    個結點 (i≥1) 。

    如果對滿二叉樹的結點進行編號, 約定編號從根結點起, 自上而下, 自左而右。則深度為k的, 有n個結點的二叉樹, 當且僅當其每一個結點都與深度為k的滿二叉樹中編號從1至n的結點一一對應時, 稱之為完全二叉樹。 [2]

    滿二叉樹和完全二叉樹的定義可以看出, 滿二叉樹是完全二叉樹的特殊形態, 即如果一棵二叉樹是滿二叉樹, 則它必定是完全二叉樹。

    完全二叉樹的特點:葉子結點只能出現在最下層和次下層,且最下層的葉子結點集中在樹的左部。需要注意的是,滿二叉樹肯定是完全二叉樹,而完全二叉樹不一定是滿二叉樹。

二叉樹的遍歷

有三種:

  • 前序遍歷先輸出父節點,再遍歷左子樹(遞歸)和右子樹(遞歸)
  • 中序遍歷:先遍歷左子樹(遞歸),再輸出父節點,再遍歷右子樹(遞歸)
  • 后序遍歷:先遍歷左子樹(遞歸),再遍歷右子樹(遞歸),最后輸出父節點

看上述粗體部分:前中后說的就是父節點的輸出時機。

注意理清楚描述中的遞歸,這關乎着你如何找到下一個輸出節點

關於遞歸,請看 數據結構與算法——初談遞歸

二叉樹遍歷思路分析

  • 前序遍歷:

    1. 先輸出當前節點(初始節點是 root 節點)
    2. 如果左子節點不為空,則遞歸繼續前序遍歷
    3. 如果右子節點不為空,則遞歸繼續前序遍歷

    上圖的輸出順序為:1、2、3、4

  • 中序遍歷:

    1. 如果當前節點的左子節點不為空,則遞歸中序遍歷
    2. 輸出當前節點
    3. 如果當前節點的右子節點不為空,則遞歸中序

    上圖的輸出順序為:2、1、3、4

  • 后序遍歷:

    1. 如果左子節點不為空,則遞歸繼續前序遍歷
    2. 如果右子節點不為空,則遞歸繼續前序遍歷
    3. 輸出當前節點

    上圖的輸出順序為:2、4、3、1

如果不理解,就結合下面的代碼進行理解。

二叉樹遍歷代碼實現

注意這里 this 的含義。

/**
 * 二叉樹測試
 */
public class BinaryTreeTest {

    // 先編寫二叉樹節點
    class HeroNode {
        public int id;
        public String name;
        public HeroNode left;
        public HeroNode right;

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

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

        /**
         * 前序遍歷
         */
        public void preOrder() {
            // 1. 先輸出當前節點
            System.out.println(this);
            // 2. 如果左子節點不為空,則遞歸繼續前序遍歷
            if (this.left != null) {
                this.left.preOrder();
            }
            // 3. 如果右子節點不為空,則遞歸繼續前序遍歷
            if (this.right != null) {
                this.right.preOrder();
            }
        }

        /**
         * 中序遍歷
         */
        public void infixOrder() {
            // 1. 如果左子節點不為空,則遞歸繼續前序遍歷
            if (this.left != null) {
                this.left.infixOrder();
            }
            // 2. 先輸出當前節點
            System.out.println(this);

            // 3. 如果右子節點不為空,則遞歸繼續前序遍歷
            if (this.right != null) {
                this.right.infixOrder();
            }
        }

        /**
         * 后序遍歷
         */
        public void postOrder() {
            // 1. 如果左子節點不為空,則遞歸繼續前序遍歷
            if (this.left != null) {
                this.left.postOrder();
            }
            // 2. 如果右子節點不為空,則遞歸繼續前序遍歷
            if (this.right != null) {
                this.right.postOrder();
            }
            // 3. 先輸出當前節點
            System.out.println(this);
        }
    }

    // 編寫 二叉樹 類
    class BinaryTree {
        public HeroNode root;//樹根

        /**
         * 前序遍歷
         */
        public void preOrder() {
            //判斷二叉樹是否為空
            if (root == null) {
                System.out.println("二叉樹為空");
                return;
            }
            root.preOrder();
        }

        /**
         * 中序遍歷
         */
        public void infixOrder() {
            if (root == null) {
                System.out.println("二叉樹為空");
                return;
            }
            root.infixOrder();
        }

        /**
         * 后續遍歷
         */
        public void postOrder() {
            if (root == null) {
                System.out.println("二叉樹為空");
                return;
            }
            root.postOrder();
        }
    }

    /**
     * 前、中、后 遍歷測試
     */
    @Test
    public void fun1() {
        // 手動創建節點與構建二叉樹
        HeroNode n1 = new HeroNode(1, "宋江");
        HeroNode n2 = new HeroNode(2, "無用");
        HeroNode n3 = new HeroNode(3, "盧俊");
        HeroNode n4 = new HeroNode(4, "林沖");
        n1.left = n2;
        n1.right = n3;
        n3.right = n4;
        BinaryTree binaryTree = new BinaryTree();
        binaryTree.root = n1;

        System.out.println("\n 前序遍歷:");
        binaryTree.preOrder();
        System.out.println("\n 中序遍歷:");
        binaryTree.infixOrder();
        System.out.println("\n 后序遍歷:");
        binaryTree.postOrder();
    }

}

測試輸出

 前序遍歷:
HeroNode{id=1, name='宋江'}
HeroNode{id=2, name='無用'}
HeroNode{id=3, name='盧俊'}
HeroNode{id=4, name='林沖'}

 中序遍歷:
HeroNode{id=2, name='無用'}
HeroNode{id=1, name='宋江'}
HeroNode{id=3, name='盧俊'}
HeroNode{id=4, name='林沖'}

 后序遍歷:
HeroNode{id=2, name='無用'}
HeroNode{id=4, name='林沖'}
HeroNode{id=3, name='盧俊'}
HeroNode{id=1, name='宋江'}

思考一下:

如上圖,給 盧俊義 增加一個節點 關勝,寫出他的前、中、后序的打印順序:

注意:上面這個新增的節點,並不是按照順序增加的,這里考的知識點是 前、中、后序的遍歷規則

  • 前序:1、2、3、5、4

  • 中序:2、1、5、3,4

  • 后序:2、5、4、3、1

那么下面通過程序來檢測答案是否正確:

    /**
     * 考題:給盧俊新增一個 left 節點,然后打印前、中、后 遍歷順序
     */
    @Test
    public void fun2() {
        // 創建節點與構建二叉樹
        HeroNode n1 = new HeroNode(1, "宋江");
        HeroNode n2 = new HeroNode(2, "無用");
        HeroNode n3 = new HeroNode(3, "盧俊");
        HeroNode n4 = new HeroNode(4, "林沖");
        HeroNode n5 = new HeroNode(5, "關勝");
        n1.left = n2;
        n1.right = n3;
        n3.right = n4;
        n3.left = n5;
        BinaryTree binaryTree = new BinaryTree();
        binaryTree.root = n1;

        System.out.println("\n 前序遍歷:");
        binaryTree.preOrder();
        System.out.println("\n 中序遍歷:");
        binaryTree.infixOrder();
        System.out.println("\n 后序遍歷:");
        binaryTree.postOrder();
    }

輸出信息

 前序遍歷:
HeroNode{id=1, name='宋江'}
HeroNode{id=2, name='無用'}
HeroNode{id=3, name='盧俊'}
HeroNode{id=5, name='關勝'}
HeroNode{id=4, name='林沖'}

 中序遍歷:
HeroNode{id=2, name='無用'}
HeroNode{id=1, name='宋江'}
HeroNode{id=5, name='關勝'}
HeroNode{id=3, name='盧俊'}
HeroNode{id=4, name='林沖'}

 后序遍歷:
HeroNode{id=2, name='無用'}
HeroNode{id=5, name='關勝'}
HeroNode{id=4, name='林沖'}
HeroNode{id=3, name='盧俊'}
HeroNode{id=1, name='宋江'}

可以看到,后序是最容易弄錯的規則,所以在后續上,一定要多多 debug,多多思考 ,看下他的調用軌跡。

二叉樹的查找

要求:

  1. 編寫前、中、后序查找方法(和上面的遍歷類似,只是添加了一些東西,注意觀察,細看代碼,思想是一樣的)
  2. 並分別使用三種查找方式,查找 id=5 的節點
  3. 並分析各種查找方式,分別比較了多少次

由於二叉樹的查找是遍歷查找,所以就簡單了,前面遍歷規則已經寫過了,改寫成查找即可

/**
 * 二叉樹測試
 */
public class BinaryTreeTest {

    // 先編寫二叉樹節點
    class HeroNode {
        public int id;
        public String name;
        public HeroNode left;
        public HeroNode right;

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

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

        /**
         * 前序遍歷
         */
        public void preOrder() {
            // 1. 先輸出當前節點
            System.out.println(this);
            // 2. 如果左子節點不為空,則遞歸繼續前序遍歷
            if (this.left != null) {
                this.left.preOrder();
            }
            // 3. 如果右子節點不為空,則遞歸繼續前序遍歷
            if (this.right != null) {
                this.right.preOrder();
            }
        }

        /**
         * 中序遍歷
         */
        public void infixOrder() {
            // 1. 如果左子節點不為空,則遞歸繼續前序遍歷
            if (this.left != null) {
                this.left.infixOrder();
            }
            // 2. 先輸出當前節點
            System.out.println(this);

            // 3. 如果右子節點不為空,則遞歸繼續前序遍歷
            if (this.right != null) {
                this.right.infixOrder();
            }
        }

        /**
         * 后序遍歷
         */
        public void postOrder() {
            // 1. 如果左子節點不為空,則遞歸繼續前序遍歷
            if (this.left != null) {
                this.left.postOrder();
            }
            // 2. 如果右子節點不為空,則遞歸繼續前序遍歷
            if (this.right != null) {
                this.right.postOrder();
            }
            // 3. 先輸出當前節點
            System.out.println(this);
        }

        /**
         * 前序查找
         */
        public HeroNode preOrderSearch(int id) {
            System.out.println("  進入前序遍歷");  // 用來統計查找了幾次
            // 1. 判斷當前節點是否相等,如果相等,則返回
            if (this.id == id) {
                return this;
            }
            // 2. 如果左子節點不為空,則遞歸繼續前序遍歷
            if (this.left != null) {
                HeroNode result = this.left.preOrderSearch(id);
                if (result != null) {
                    return result;
                }
            }
            // 3. 如果右子節點不為空,則遞歸繼續前序遍歷
            if (this.right != null) {
                HeroNode result = this.right.preOrderSearch(id);
                if (result != null) {
                    return result;
                }
            }
            return null;
        }

        /**
         * 中序查找
         */
        public HeroNode infixOrderSearch(int id) {
//            System.out.println("  進入中序遍歷");  // 用來統計查找了幾次,不能在這里打印,這里打印是進入了方法幾次,看的是比較了幾次
            // 1. 如果左子節點不為空,則遞歸繼續前序遍歷
            if (this.left != null) {
                HeroNode result = this.left.infixOrderSearch(id);
                if (result != null) {
                    return result;
                }
            }
            System.out.println("  進入中序遍歷");// 用來統計查找了幾次
            // 2. 如果相等,則返回
            if (this.id == id) {
                return this;
            }

            // 3. 如果右子節點不為空,則遞歸繼續前序遍歷
            if (this.right != null) {
                HeroNode result = this.right.infixOrderSearch(id);
                if (result != null) {
                    return result;
                }
            }
            return null;
        }

        /**
         * 后序查找
         */
        public HeroNode postOrderSearch(int id) {
//            System.out.println("  進入后序遍歷");  // 用來統計查找了幾次,不能在這里打印,這里打印是進入了方法幾次,看的是比較了幾次
            // 1. 如果左子節點不為空,則遞歸繼續前序遍歷
            if (this.left != null) {
                HeroNode result = this.left.postOrderSearch(id);
                if (result != null) {
                    return result;
                }
            }
            // 2. 如果右子節點不為空,則遞歸繼續前序遍歷
            if (this.right != null) {
                HeroNode result = this.right.postOrderSearch(id);
                if (result != null) {
                    return result;
                }
            }
            System.out.println("  進入后序遍歷");// 用來統計查找了幾次
            // 3. 如果相等,則返回
            if (this.id == id) {
                return this;
            }
            return null;
        }
    }

    // 編寫 二叉樹 類
    class BinaryTree {
        public HeroNode root;

        /**
         * 前序遍歷
         */
        public void preOrder() {
            if (root == null) {
                System.out.println("二叉樹為空");
                return;
            }
            root.preOrder();
        }

        /**
         * 中序遍歷
         */
        public void infixOrder() {
            if (root == null) {
                System.out.println("二叉樹為空");
                return;
            }
            root.infixOrder();
        }

        /**
         * 后續遍歷
         */
        public void postOrder() {
            if (root == null) {
                System.out.println("二叉樹為空");
                return;
            }
            root.postOrder();
        }

        /**
         * 前序查找
         */
        public HeroNode preOrderSearch(int id) {
            //判斷樹是否為空
            if (root == null) {
                System.out.println("二叉樹為空");
                return null;
            }
            return root.preOrderSearch(id);
        }

        /**
         * 中序查找
         */
        public HeroNode infixOrderSearch(int id) {
            if (root == null) {
                System.out.println("二叉樹為空");
                return null;
            }
            return root.infixOrderSearch(id);
        }

        /**
         * 后序查找
         */
        public HeroNode postOrderSearch(int id) {
            if (root == null) {
                System.out.println("二叉樹為空");
                return null;
            }
            return root.postOrderSearch(id);
        }
    }

    


    /**
     * 查找 id=5 的前、中、后序測試
     */
    @Test
    public void fun3() {
        // 創建節點與構建二叉樹
        HeroNode n1 = new HeroNode(1, "宋江");
        HeroNode n2 = new HeroNode(2, "無用");
        HeroNode n3 = new HeroNode(3, "盧俊");
        HeroNode n4 = new HeroNode(4, "林沖");
        HeroNode n5 = new HeroNode(5, "關勝");
        n1.left = n2;
        n1.right = n3;
        n3.right = n4;
        n3.left = n5;
        BinaryTree binaryTree = new BinaryTree();
        binaryTree.root = n1;

        System.out.println("找到測試:");
        int id = 5;
        System.out.println("\n前序遍歷查找 id=" + id);
        System.out.println(binaryTree.preOrderSearch(id));
        System.out.println("\n中序遍歷查找 id=" + id);
        System.out.println(binaryTree.infixOrderSearch(id));
        System.out.println("\n后序遍歷查找 id=" + id);
        System.out.println(binaryTree.postOrderSearch(id));

        System.out.println("找不到測試:");
        id = 15;
        System.out.println("\n前序遍歷查找 id=" + id);
        System.out.println(binaryTree.preOrderSearch(id));
        System.out.println("\n中序遍歷查找 id=" + id);
        System.out.println(binaryTree.infixOrderSearch(id));
        System.out.println("\n后序遍歷查找 id=" + id);
        System.out.println(binaryTree.postOrderSearch(id));
    }
}

測試輸出

找到測試:

前序遍歷查找 id=5   # 共查找 4 次
  進入前序遍歷
  進入前序遍歷
  進入前序遍歷
  進入前序遍歷
HeroNode{id=5, name='關勝'}

中序遍歷查找 id=5  # 共查找 3 次
  進入中序遍歷
  進入中序遍歷
  進入中序遍歷
HeroNode{id=5, name='關勝'}

后序遍歷查找 id=5  # 共查找 2 次
  進入后序遍歷
  進入后序遍歷
HeroNode{id=5, name='關勝'}
找不到測試:

前序遍歷查找 id=15
  進入前序遍歷
  進入前序遍歷
  進入前序遍歷
  進入前序遍歷
  進入前序遍歷
null

中序遍歷查找 id=15
  進入中序遍歷
  進入中序遍歷
  進入中序遍歷
  進入中序遍歷
  進入中序遍歷
null

后序遍歷查找 id=15
  進入后序遍歷
  進入后序遍歷
  進入后序遍歷
  進入后序遍歷
  進入后序遍歷
null

可以看出:

  • 找到的次數和 查找的順序 有關,而查找順序就是於遍歷方式有關
  • 找不到的次數則是相當於都遍歷完成,所以是相等的次數

二叉樹的刪除

要求:

  1. 如果刪除的節點是 葉子節點,則刪除該節點
  2. 如果刪除的節點是非葉子節點,則刪除該子樹

測試:刪除 5 號葉子節點和 3 號子樹。

說明:目前的二叉樹不是規則的,如果不刪除子樹,則需要考慮哪一個節點會被上提作為父節點。這個后續講解排序二叉樹時再來實現。先實現簡單的

思路分析:

  • 由於我們的二叉樹是單向的

  • 所以我們判定一個節點是否可以刪除,是判斷它的 子節點 是否可刪除,否則則沒法回到父節點刪除了,因為要判斷被刪除的節點滿足前面的兩點要求(因為鏈表的關系)

    1. 當前節點的 左子節點 不為空,並且左子節點就是要刪除的節點,則 this.left = null,並且返回(結束遞歸刪除)
    2. 當前節點的 右子節點 不為空,並且右子節點就是要刪除的節點,則 this.right = null,並且返回(結束遞歸刪除)

    如果前面都沒有刪除,則繼續遞歸,直到找到並刪除,或者是找不到對應的節點,輸出沒有該節點即可。上面的要求是 2 點,實際上是,找到符合條件的節點則直接刪除(因為不考慮是否有子樹)

// BinaryTree 新增刪除的方法

 /**
 * 刪除節點
 *
 * @param id
 * @return
 */
public HeroNode delete(int id) {
  if (root == null) {
    System.out.println("樹為空");
    return null;
  }
  HeroNode target = null;//保存要刪除的目標節點
  // 如果 root 節點就是要刪除的節點,則直接置空
  if (root.id == id) {
    target = root;
    root = null;
  } else {
    target = this.root.delete(id);
  }

  return target;
}
// HeroNode 中新增刪除的方法

/**
* 刪除節點,思路,先看左右,看完再遞歸,具體看代碼
* @param id
* @return 如果刪除成功,則返回刪除的節點
*/
public HeroNode delete(int id) {
  // 判斷左子節點是否是要刪除的節點
  HeroNode target = null;
  if (this.left != null && this.left.id == id) {
    target = this.left;
    this.left = null;
    return target;
  }

  if (this.right != null && this.right.id == id) {
    target = this.right;
    this.right = null;
    return target;
  }

  // 嘗試左遞歸
  if (this.left != null) {
    target = this.left.delete(id);
    if (target != null) {
      return target;
    }
  }

  // 嘗試右遞歸
  if (this.right != null) {
    target = this.right.delete(id);
    if (target != null) {
      return target;
    }
  }
  return null;
}

刪除方法測試用例

    /**
     * 構建當前這個樹
     *
     * @return
     */
    private BinaryTree buildBinaryTree() {
        HeroNode n1 = new HeroNode(1, "宋江");
        HeroNode n2 = new HeroNode(2, "無用");
        HeroNode n3 = new HeroNode(3, "盧俊");
        HeroNode n4 = new HeroNode(4, "林沖");
        HeroNode n5 = new HeroNode(5, "關勝");
        n1.left = n2;
        n1.right = n3;
        n3.right = n4;
        n3.left = n5;
        BinaryTree binaryTree = new BinaryTree();
        binaryTree.root = n1;
        return binaryTree;
    }

    /**
     * 不考慮子節點的刪除
     */
    @Test
    public void delete() {
        System.out.println("\n刪除 3 號節點");
        delete3();
        System.out.println("\n刪除 5 號節點");
        delete5();
        System.out.println("\n刪除一個不存在的節點");
        deleteFail();
        System.out.println("\n刪除 root 節點");
        deleteRoot();
    }

    @Test
    public void delete3() {
        // 創建節點與構建二叉樹
        BinaryTree binaryTree = buildBinaryTree();
        binaryTree.preOrder();

        // 刪除 3 號節點
        HeroNode target = binaryTree.delete(3);
        String msg = (target == null ? "刪除失敗,未找到" : "刪除成功:" + target.toString());
        System.out.println(msg);
        binaryTree.preOrder();
    }


    @Test
    public void delete5() {
        // 創建節點與構建二叉樹
        BinaryTree binaryTree = buildBinaryTree();
        binaryTree.preOrder();

        // 刪除 5 號節點
        HeroNode target = binaryTree.delete(5);
        String msg = (target == null ? "刪除失敗,未找到" : "刪除成功:" + target.toString());
        System.out.println(msg);
        binaryTree.preOrder();
    }

    /**
     * 刪除一個不存在的節點
     */
    @Test
    public void deleteFail() {
        // 創建節點與構建二叉樹
        BinaryTree binaryTree = buildBinaryTree();
        binaryTree.preOrder();

        // 刪除 5 號節點
        HeroNode target = binaryTree.delete(9);
        String msg = (target == null ? "刪除失敗,未找到" : "刪除成功:" + target.toString());
        System.out.println(msg);
        binaryTree.preOrder();
    }

    /**
     * 刪除 root 節點
     */
    @Test
    public void deleteRoot() {
        // 創建節點與構建二叉樹
        BinaryTree binaryTree = buildBinaryTree();
        binaryTree.preOrder();

        // 刪除 1 號節點
        HeroNode target = binaryTree.delete(1);
        String msg = (target == null ? "刪除失敗,未找到" : "刪除成功:" + target.toString());
        System.out.println(msg);
        binaryTree.preOrder();
    }

分別輸出信息如下

刪除 3 號節點
HeroNode{id=1, name='宋江'}
HeroNode{id=2, name='無用'}
HeroNode{id=3, name='盧俊'}
HeroNode{id=5, name='關勝'}
HeroNode{id=4, name='林沖'}
刪除成功:HeroNode{id=3, name='盧俊'}
HeroNode{id=1, name='宋江'}
HeroNode{id=2, name='無用'}

刪除 5 號節點
HeroNode{id=1, name='宋江'}
HeroNode{id=2, name='無用'}
HeroNode{id=3, name='盧俊'}
HeroNode{id=5, name='關勝'}
HeroNode{id=4, name='林沖'}
刪除成功:HeroNode{id=5, name='關勝'}
HeroNode{id=1, name='宋江'}
HeroNode{id=2, name='無用'}
HeroNode{id=3, name='盧俊'}
HeroNode{id=4, name='林沖'}

刪除一個不存在的節點
HeroNode{id=1, name='宋江'}
HeroNode{id=2, name='無用'}
HeroNode{id=3, name='盧俊'}
HeroNode{id=5, name='關勝'}
HeroNode{id=4, name='林沖'}
刪除失敗,未找到
HeroNode{id=1, name='宋江'}
HeroNode{id=2, name='無用'}
HeroNode{id=3, name='盧俊'}
HeroNode{id=5, name='關勝'}
HeroNode{id=4, name='林沖'}

刪除 root 節點
HeroNode{id=1, name='宋江'}
HeroNode{id=2, name='無用'}
HeroNode{id=3, name='盧俊'}
HeroNode{id=5, name='關勝'}
HeroNode{id=4, name='林沖'}
刪除成功:HeroNode{id=1, name='宋江'}
二叉樹為空

一、順序存儲二叉樹

基本說明-概念

從數據存儲來看,數組存儲 方式和 的存儲方式可以 相互轉換即使數組可以轉換成樹,樹也可以轉換成數組。如下示意圖

上圖閱讀說明:

  • 圓圈頂部的數字對應了數組中的索引
  • 圓圈內部的值對應的數數組元素的值

現在有兩個要求:

  1. 上圖的二叉樹的節點,要求以數組的方式來存儲 arr=[1,2,3,4,5,6,7]
  2. 要求在遍歷數組 arr 時,仍然可以以 前序、中序、后序的方式遍歷

特點(思路)

想要 實現上面的兩個要求,需要知道順序存儲二叉樹的特點

  1. 順序二叉樹 通常只考慮 完全二叉樹(完全二叉樹上面有解釋)
  2. 第 n 個元素的 左子節點2*n+1
  3. 第 n 個元素的 右子節點2*n+2
  4. 第 n 個元素的 父節點(n-1)/2

:n 表示二叉樹中的第幾個元素(按 0 開始編號)

比如:

  • 元素 2 的左子節點為:2 * 1 + 1 = 3,對比上圖去查看,的確是 3
  • 元素 2 的右子節點為:2 * 1 + 2 = 4,也 就是元素 5
  • 元素 3 的左子節點為:2 * 2 + 1 = 5,也就是元素 6
  • 元素 3 的父節點為: (2-1)/2= 1/2 = 0,也就是根節點 1

搞懂特點規律,下面進行代碼實現。

前序遍歷

使用如上的知識點,進行前序遍歷,需求:將數組 arr=[1,2,3,4,5,6,7],以二叉樹前序遍歷的方式進行遍歷,遍歷結果為 1、2、4、5、3、6

前序遍歷概念(上面有講):

  1. 先輸出當前節點(初始節點是 root 節點)
  2. 如果左子節點不為空,則遞歸繼續前序遍歷
  3. 如果右子節點不為空,則遞歸繼續前序遍歷
/**
 * 順序存儲二叉樹
 */
public class ArrBinaryTreeTest {
    /**
     * 前序遍歷測試
     */
    @Test
    public void preOrder() {
        int[] arr = new int[]{1, 2, 3, 4, 5, 6, 7};
        ArrBinaryTree tree = new ArrBinaryTree(arr);
        tree.preOrder(0); // 1,2,4,5,3,6,7
    }
}

class ArrBinaryTree {
    int[] arr;

    public ArrBinaryTree(int[] arr) {
        this.arr = arr;
    }

    /**
     * 前序遍歷
     *
     * @param index 就是知識點中的 n,從哪一個節點開始遍歷
     */
    public void preOrder(int index) {
        /*
            1. 順序二叉樹 通常只考慮 **完成二叉樹**
            2. 第 n 個元素的 **左子節點** 為 `2*n+1`
            3. 第 n 個元素的 **右子節點** 為 `2*n+2`
            4. 第 n 個元素的 **父節點** 為 `(n-1)/2`
         */
        if (arr == null || arr.length == 0) {
            System.out.println("數組為空,不能前序遍歷二叉樹");
            return;
        }
        // 1. 先輸出當前節點(初始節點是 root 節點)
        System.out.println(arr[index]);
        // 2. 如果左子節點不為空,則遞歸繼續前序遍歷
        int left = 2 * index + 1;
        if (left < arr.length) {
            preOrder(left);
        }
        // 3. 如果右子節點不為空,則遞歸繼續前序遍歷
        int right = 2 * index + 2;
        if (right < arr.length) {
            preOrder(right);
        }
    }
}

測試輸出

1
2
4
5
3
6
7

中序、后序遍歷

    /**
     * 中序遍歷:先遍歷左子樹,再輸出父節點,再遍歷右子樹
     *
     * @param index
     */
    public void infixOrder(int index) {
        if (arr == null || arr.length == 0) {
            System.out.println("數組為空,不能前序遍歷二叉樹");
            return;
        }
        int left = 2 * index + 1;
        if (left < arr.length) {
            infixOrder(left);
        }
        
        System.out.println(arr[index]);
        
        int right = 2 * index + 2;
        if (right < arr.length) {
            infixOrder(right);
        }
    }

    /**
     * 后序遍歷:先遍歷左子樹,再遍歷右子樹,最后輸出父節點
     *
     * @param index
     */
    public void postOrder(int index) {
        if (arr == null || arr.length == 0) {
            System.out.println("數組為空,不能前序遍歷二叉樹");
            return;
        }
        
        int left = 2 * index + 1;
        if (left < arr.length) {
            postOrder(left);
        }
        
        int right = 2 * index + 2;
        if (right < arr.length) {
            postOrder(right);
        }
        
        System.out.println(arr[index]);
    }

測試代碼

    /**
     * 中序遍歷測試
     */
    @Test
    public void infixOrder() {
        int[] arr = new int[]{1, 2, 3, 4, 5, 6, 7};
        ArrBinaryTree tree = new ArrBinaryTree(arr);
        tree.infixOrder(0); // 4,2,5,1,6,3,7
    }

    /**
     * 后序遍歷測試
     */
    @Test
    public void postOrder() {
        int[] arr = new int[]{1, 2, 3, 4, 5, 6, 7};
        ArrBinaryTree tree = new ArrBinaryTree(arr);
        tree.postOrder(0); // 4,5,2,6,7,3,1
    }

應用案例

學會了順序存儲二叉樹,那么它可以用來做什么呢?

八大排序算法中的 堆排序,就會使用到順序存儲二叉樹。

二、線索化二叉樹

為什么要線索化二叉樹?

看如下問題:將數列 {1,3,6,8,10,14} 構成一顆二叉樹

可以看到上圖的二叉樹為一顆 完全二叉樹。對他進行分析,可以發現如下的一些問題:

  1. 當對上面的二叉樹進行中序遍歷時,數列為 8,3,10,1,14,6
  2. 但是 6,8,10,14 這幾個節點的左右指針,並沒有完全用上

如果希望充分利用各個節點的左右指針,讓各個節點可以 指向自己的前后節點,這個時候就可以使用 線索化二叉樹

介紹

n 個節點的二叉樹鏈表中含有 n + 1 個空指針域,他的推導公式為 2n-(n-1) = n + 1

利用二叉樹鏈表中的空指針域,存放指向該節點在 某種遍歷次序下(前序,中序,后序)的 前驅節點后繼節點的指針,這種附加的指針稱為「線索」

  • 前驅:一個節點的前一個節點
  • 后繼:一個節點的后一個節點

如下圖,在中序遍歷中,下圖的中序遍歷為 8,3,10,1,14,6,那么 8 的后繼節點就為 3,8 沒有前驅節點,10 的前驅節點是 3,10 的后繼節點是 1 (主要是看遍歷出來的數列的順序來判定)

這種加上了線索的二叉樹鏈表稱為 線索鏈表(一般的二叉樹本來就是用鏈表實現的),相應的二叉樹稱為 線索二叉樹(Threaded BinaryTree)。根據線索性質的不同,線索二叉樹可分為:前、中、后序線索二叉樹

思路分析

將上圖的二叉樹,進行 中序線索二叉樹,中序遍歷的數列為 8,3,10,1,14,6

那么以上圖為例,線索化二叉樹后的樣子如下圖

  • 8 的后繼節點為 3
  • 3 由於 左右節點都有元素,不能線索化
  • 10 的前驅節點為 3,后繼節點為 1
  • 1 不能線索化
  • 14 的前驅節點為 1,后繼節點為 6
  • 6 有左節點,不能線索化

注意:當線索化二叉樹后,那么一個 Node 節點的 left 和 right 屬性,就有如下情況

  1. left 指向的是 左子樹,也可能是指向 前驅節點

    例如:節點 1 left 節點指向的是左子樹,節點 10 的 left 指向的就是前驅節點

  2. right 指向的是 右子樹,也可能是指向 后繼節點

    例如:節點 3 的 right 指向的是右子樹,節點 10 的 right 指向的是后繼節點

代碼實現

下面的代碼,有幾個地方需要注意:

  • HeroNode 就是一個 簡單的二叉樹節點,不同的是多了兩個 type 屬性:

    • leftType:左節點的類型:0:左子樹,1:前驅節點
    • rightType:右節點的類型:0:右子樹,1:后繼節點

    為什么需要?上面原理講解了,left 或則 right 會有兩種身份,需要一個額外 的屬性來指明

  • threadeNodes:線索化二叉樹

    是將一棵二叉樹,進行線索化標記。只是將可以線索化的節點進行賦值。

/**
 * 線索化二叉樹
 */
public class ThreadedBinaryTreeTest {
    class HeroNode {
        public int id;
        public String name;
        public HeroNode left;
        public HeroNode right;
        /**
         * 左節點的類型:0:左子樹,1:前驅節點
         */
        public int leftType;
        /**
         * 右節點的類型:0:右子樹,1:后繼節點
         */
        public int rightType;

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

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

    class ThreadedBinaryTree {
        public HeroNode root;
        public HeroNode pre; // 保留上一個節點

        /**
         * 線索化二叉樹:以 中序的方式線索化
         */
        public void threadeNodes() {
            // 從 root 開始遍歷,然后 線索化
            this.threadeNodes(root);
        }

        private void threadeNodes(HeroNode node) {
            if (node == null) {
                return;
            }
            // 中序遍歷順序:先左、自己、再右
            threadeNodes(node.left);
            // 難點就是在這里,如何線索化自己
            // 當自己的 left 節點為空,則設置為前驅節點
            if (node.left == null) {
                node.left = pre;
                node.leftType = 1;
            }

            // 因為要設置后繼節點,只有回到自己(node)的后繼節點的時候,才能把自己設置為前一個的后繼節點    !!這里自己好好意會一下
            // 當前一個節點的 right 為空時,則需要自己(node)是后繼節點
            if (pre != null && pre.right == null) {
                pre.right = node;
                pre.rightType = 1;
            }

            // 數列: 1,3,6,8,10,14
            // 中序: 8,3,10,1,14,6
            // 這里最好結合上面圖示的二叉樹來看,容易理解
            // 因為中序遍歷,先遍歷左邊,所以 8 是第一個輸出的節點
            // 當 node = 8 時,pre 還沒有被賦值過,則為空。這是正確的,因為 8 就是第一個節點
            // 當 8 處理完成之后,處理 3 時
            // 當 node = 3 時,pre 被賦值為 8 了。
            pre = node; //關鍵!!!!
            
            threadeNodes(node.right);
        }
    }

    @Test
    public void threadeNodesTest() {
        HeroNode n1 = new HeroNode(1, "宋江");
        HeroNode n3 = new HeroNode(3, "無用");
        HeroNode n6 = new HeroNode(6, "盧俊");
        HeroNode n8 = new HeroNode(8, "林沖2");
        HeroNode n10 = new HeroNode(10, "林沖3");
        HeroNode n14 = new HeroNode(14, "林沖4");
        n1.left = n3;
        n1.right = n6;
        n3.left = n8;
        n3.right = n10;
        n6.left= n14;

        ThreadedBinaryTree tree = new ThreadedBinaryTree();
        tree.root = n1;

        tree.threadeNodes();

        // 驗證:
        HeroNode left = n10.left;
        HeroNode right = n10.right;
        System.out.println("10 號節點的前驅節點:" + left.id);
        System.out.println("10 號節點的后繼節點:" + right.id);
    }
}

測試輸出

10 號節點的前驅節點:3
10 號節點的后繼節點:1

如果看代碼注釋看不明白的話 ,現在來解釋:

  • 線索化的時候,就是要按照 中序遍歷 的順序,去找可以線索化的節點

    中序遍歷順序:先左、自己、再右

    我們主要的代碼是在 自己這一塊

  • 確定前一個節點 pre

    這個 pre 很難理解,對照下圖進行理解

    // 數列: 1,3,6,8,10,14
    // 中序: 8,3,10,1,14,6
    
    // 因為中序遍歷,先遍歷左邊,所以 8 是第一個輸出的節點
    // 當 node = 8 時,pre 還沒有被賦值過,則為空。這是正確的,因為 8 就是第一個節點
    // 當 8 處理完成之后,處理 3 時
    // 當 node = 3 時,pre 被賦值為 8 了。
    
  • 設置前驅節點

    難點的講解在於 pre,解決了這里就簡單了

    如果當 node = 8 時,pre 還是 null,因為 8 就是中序的第一個節點。因此 8 沒有前驅

    如果當 node = 3 時,pre = 8,那么 3 是不符合線索化要求的,因為 8 是 3 的 left

  • 設置后繼節點

    接上面的邏輯。

    如果當 node = 8 時,本來 該給 8 設置他的后繼節點,但是此時根本就獲取不到節點 3,因為節點是單向的

    這里就得利用前一個節點 pre

    當 node=3 時,pre = 8,這時就可以為節點 8 處理它的后繼節點了,因為根據中序的順序,左、自己、后。那么自己(node)一定是前一個的后繼。只要前一個的 right 為 null,就符合線索化了

上述最難的 3 個點說明,請對照上圖看,先看一遍代碼,再看說明。然后去 debug 你就了解了。

遍歷線索化二叉樹

結合圖示來看思路說明最直觀

對於原來的中序遍歷來說,無法使用了,因為左右節點再也不為空了。這里直接利用線索化節點提供的線索,找到他的后繼節點遍歷,思路如下:

  1. 首先找到它的第一個節點,並打印它

    中序遍歷,先左,所以一直往左找,直到 left 為 null 時,則是第一個節點

  2. 然后看它的 right節點是否為線索化節點,是的話則打印它

    因為:如果 right 是一個線索化節點,也就是 right 是當前節點的 后繼節點,可以直接打印。

    這里判斷是否為線索化節點就得用到新添加的那兩個屬性了,

    • leftType:左節點的類型:0:左子樹,1:前驅節點
    • rightType:右節點的類型:0:右子樹,1:后繼節點
  3. right 如果是一個普通節點,那么就直接處理它的右側節點

    因為:按照中序遍歷順序,左、自己、右,這里就理所當然是右了

看描述索然無味,結合下面的代碼來看,就比較清楚了

       /**
         * 遍歷線索化二叉樹
         */
        public void threadedList() {
            // 前面線索化使用的是中序,這里也同樣要用中序的方式
            // 但是不適合使用之前那種遞歸了
            HeroNode node = root;
            while (node != null) {
                // 中序:左、自己、右
                // 數列: 1,3,6,8,10,14
                // 中序: 8,3,10,1,14,6
                // 那么先找到左邊的第一個線索化節點,也就是 8. 對照圖示理解,比較容易
                while (node.leftType == 0) {
                    node = node.left;
                }
                // 找到這個線索化節點之后,打印它
                System.out.println(node);

                // 如果該節點右子節點也是線索化節點,則打印它
                while (node.rightType == 1) {
                    node = node.right;
                    System.out.println(node);
                }
                //否則
                // 到達這里,就說明遇到的不是一個 線索化節點了
                // 而且,按中序的順序來看:這里應該處理右側了
                node = node.right;
            }
        }

測試

    /**
     * 線索化遍歷測試
     */
    @Test
    public void threadedListTest() {
        // 1,3,6,8,10,14
        HeroNode n1 = new HeroNode(1, "宋江");
        HeroNode n3 = new HeroNode(3, "無用");
        HeroNode n6 = new HeroNode(6, "盧俊");
        HeroNode n8 = new HeroNode(8, "林沖2");
        HeroNode n10 = new HeroNode(10, "林沖3");
        HeroNode n14 = new HeroNode(14, "林沖4");
        n1.left = n3;
        n1.right = n6;
        n3.left = n8;
        n3.right = n10;
        n6.left = n14;

        ThreadedBinaryTree tree = new ThreadedBinaryTree();
        tree.root = n1;

        tree.threadeNodes();
        tree.threadedList(); // 8,3,10,1,14,6
    }

輸出信息

HeroNode{id=8, name='林沖2'}
HeroNode{id=3, name='無用'}
HeroNode{id=10, name='林沖3'}
HeroNode{id=1, name='宋江'}
HeroNode{id=14, name='林沖4'}
HeroNode{id=6, name='盧俊'}

前序線索化

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

        /**
         * 前序線索化二叉樹
         */
        public void preOrderThreadeNodes(HeroNode node) {
            // 前序:自己、左(遞歸)、右(遞歸)
            // 數列: 1,3,6,8,10,14
            // 前序: 1,3,8,10,6,14

            if (node == null) {
                return;
            }

            System.out.println(node);
            // 當自己的 left 節點為空,則可以線索化
            if (node.left == null) {
                node.left = pre;
                node.leftType = 1;
            }
            // 當前一個節點 right 為空,則可以把自己設置為前一個節點的后繼節點
            if (pre != null && pre.right == null) {
                pre.right = node;
                pre.rightType = 1;
            }

            // 因為是前序,因此 pre 保存的是自己
            // 到下一個節點的時候,下一個節點如果是線索化節點 ,才能將自己作為它的前驅節點
            pre = node;

            // 那么繼續往左,查找符合可以線索化的節點
            // 因為先處理的自己,如果 left == null,就已經線索化了
            // 再往左的時候,就不能直接進入了
            // 需要判定,如果不是線索化節點,再進入
            // 比如:當前節點 8,前驅 left 被設置為了 3
            // 這里節點 8 的 left 就為 1 了,就不能繼續遞歸,否則又回到了節點 3 上
            // 導致死循環了。
            if (node.leftType == 0) {
                preOrderThreadeNodes(node.left);
            }
            if (node.rightType == 0) {
                preOrderThreadeNodes(node.right);
            }
        }

這里代碼相對於中序線索化來說,難點在於:什么時候該繼續往左查找,什么時候該繼續往右查找。

測試

    /**
     * 前序線索化
     */
    @Test
    public void preOrderThreadedNodesTest() {
        // 1,3,6,8,10,14
        HeroNode n1 = new HeroNode(1, "宋江");
        HeroNode n3 = new HeroNode(3, "無用");
        HeroNode n6 = new HeroNode(6, "盧俊");
        HeroNode n8 = new HeroNode(8, "林沖2");
        HeroNode n10 = new HeroNode(10, "林沖3");
        HeroNode n14 = new HeroNode(14, "林沖4");
        n1.left = n3;
        n1.right = n6;
        n3.left = n8;
        n3.right = n10;
        n6.left = n14;

        ThreadedBinaryTree tree = new ThreadedBinaryTree();
        tree.root = n1;

        tree.preOrderThreadeNodes();

        // 驗證: 前序順序: 1,3,8,10,6,14
        HeroNode left = n10.left;
        HeroNode right = n10.right;
        System.out.println("10 號節點的前驅節點:" + left.id); // 8
        System.out.println("10 號節點的后繼節點:" + right.id); // 6

        left = n6.left;
        right = n6.right;
        System.out.println("6 號節點的前驅節點:" + left.id); // 14, 普通節點
        System.out.println("6 號節點的后繼節點:" + right.id); // 14,線索化節點
    }

輸出

HeroNode{id=1, name='宋江'}
HeroNode{id=3, name='無用'}
HeroNode{id=8, name='林沖2'}
HeroNode{id=10, name='林沖3'}
HeroNode{id=6, name='盧俊'}
HeroNode{id=14, name='林沖4'}
10 號節點的前驅節點:8
10 號節點的后繼節點:6
6 號節點的前驅節點:14   注意:這里不是前驅,而是正常的一個left節點
6 號節點的后繼節點:14

可以看到,我們線索化二叉樹的時候,是按照前序的順序 1,3,8,10,6,14 的順序遍歷查找處理的。處理之后的 6 號節點兩個都是一樣的,但是 left 是正常的節點 14,right 是線索化節點 14,不明白可以結合下圖想一下

前序線索化遍歷

前序線索化遍歷,還是要記住它的特點是:自己、左(遞歸)、右(遞歸),那么遍歷思路如下:

  1. 先打印自己
  2. 再左遞歸打印
  3. 直到遇到一個節點有 right 且是后繼節點,則直接跳轉到該后繼節點,繼續打印
  4. 如果遇到的是一個普通節點,則打印該普通節點,完成一輪循環,進入到下一輪,從第 1 步開始
/**
         * 前序線索化二叉樹遍歷
         */
        public void preOrderThreadeList() {
            HeroNode node = root;
          // 最后一個節點無后繼節點,就會退出了
          // 前序:自己、左(遞歸)、右(遞歸)
            while (node != null) {
                // 先打印自己
                System.out.println(node);

                while (node.leftType == 0) {
                    node = node.left;
                    System.out.println(node);
                }
                while (node.rightType == 1) {
                    node = node.right;
                    System.out.println(node);
                }
                node = node.right;
            }
        }

測試代碼

    @Test
    public void preOrderThreadeListTest() {
        ThreadedBinaryTree tree = buildTree();
        tree.preOrderThreadeNodes();
        System.out.println("前序線索化遍歷");
        tree.preOrderThreadeList(); // 1,3,8,10,6,14
    }

測試輸出

HeroNode{id=1, name='宋江'}
HeroNode{id=3, name='無用'}
HeroNode{id=8, name='林沖2'}
HeroNode{id=10, name='林沖3'}
HeroNode{id=6, name='盧俊'}
HeroNode{id=14, name='林沖4'}
前序線索化遍歷
HeroNode{id=1, name='宋江'}
HeroNode{id=3, name='無用'}
HeroNode{id=8, name='林沖2'}
HeroNode{id=10, name='林沖3'}
HeroNode{id=6, name='盧俊'}
HeroNode{id=14, name='林沖4'}

總結

還有一個后序線索化,這里不寫了,從前序、中序獲取到幾個重要的點:

  • 線索化時:

    1. 根據不同的「序」,如何進行遍歷的同時,處理線索化節點

      對於中序來說:

      1. 先遞歸到最左節點
      2. 開始線索化
      3. 再遞歸到最右節點

      它的順序:先左(遞歸)、自己(node)、再右(遞歸)

      對於前序來說:

      1. 開始線索化
      2. 一直往左遞歸
      3. 一直往右遞歸

      它的順序:自己(node)、左(遞歸)、右(遞歸)

    2. 根據不同的「序」,考慮如何跳過或進入下一個節點,因為要考慮前驅和后繼

      「序」:前、中、后序

      1. 中序:由於它的順序,第一個線索化節點,就是他的順序的第一個節點,不用管接下來遇到的節點是否已經線索化過了,這是由於它天然的順序,已經線索化過的節點,不會在下一次處理
      2. 前序:由於它的順序,第一個順序輸出的節點,並不是第一個線索化節點。所以它需要對他的 左右節點進行類型判定,是普通節點的話,再按:自己、左、右的順序進行左、右進行遞歸,因為下一次出現的節點有可能是已經線索化過的節點,如果不進行判定,就會導致又回到了已經遍歷過的節點。就會導致死循環了。這里可以結合上圖思考一下。
  • 遍歷線索化時:基本上和線索化時的「序」一起去考慮,何時該進行輸出?什么時候遇到后繼節點時,跳轉到后繼節點處理。最重要的一點是:遍歷時,不用考慮前驅節點,之后考慮何時通過后繼節點進行跳轉輸出(看遍歷代碼就懂)。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM