算法 | 遍歷二分搜索樹

二分搜索樹

又是來自我的好朋友 EvilSay 的投稿,以下是原文:

1、基本定義

  • 二分搜索樹的每個子節點最多有兩個葉子節點
  • 二分搜索樹的每個節點最多有一個根節點
  • 存儲的元素必須具有可比較性
  • 二分搜索樹每個子節點的值

    • 大於其左子節的所有節點的值
    • 小於其右子節點的所有節點的值
  • 二分搜索樹不一定是滿的

2、二分搜索樹 Java 實現

/**
 * @Author: EvilSay
 * @Date: 2019/8/6 19:00
 */
public class BSTMain <E extends Comparable<E>> {
    private class Node {
        public E e;
        private Node left, right;

        public Node(E e) {
            this.e = e;
            left = null;
            right = null;
        }
    }

    //根節點
    private Node root;
    private int size;

    public BSTMain() {
        root = null;
        size = 0;
    }

    public int size() {
        return size;
    }

    public boolean isEmpty() {
        return size == 0;
    }
    public void add(E e){

        root = add(this.root, e);

    }

    // 向node爲根的二分搜索樹中插入元素E,遞歸算法
    // 返回插入新節點後二分搜索樹的根
    private Node add(Node node, E e){

        if (node == null){

            size ++;
            return new Node(e);
        }

        if (e.compareTo(node.e) < 0)
            node.left = add(node.left, e);
        else if (e.compareTo(node.e) > 0)
            node.right = add(node.right,e);
        return node;
    }

    // 看二分搜索樹中是否包含元素e
    public boolean contains(E e){
        return contains(root,e);
    }

    // 看以node爲根的二分搜索樹中是否包含元素e,遞歸算法
    private boolean contains(Node node, E e){
        if (node == null)
            return false;
        if (e.compareTo(node.e) == 0)
            return true;
        else if (e.compareTo(node.e) < 0)
            return contains(node.left, e);
        else
            return contains(node.right,e);
    }

    @Override
    public String toString() {
        StringBuilder res = new StringBuilder();
        generateBSTSString(root,0,res);
        return res.toString();
    }

    // 生成以node爲根節點,深度爲depth的描述二叉樹的字符串
    private void generateBSTSString(Node root, int depth, StringBuilder res) {
        if (root == null){
            res.append(generateDepthString(depth) + "null\n");
            return;
        }
        res.append(generateDepthString(depth) + root.e + "\n");
        generateBSTSString(root.left, depth + 1 ,res);
        generateBSTSString(root.right, depth + 1, res);

    }

    private String generateDepthString(int depth) {
        StringBuilder res = new StringBuilder();
        for (int i = 0; i < depth; i++)
            res.append("--");
        return res.toString();
    }
}

3、圖解二分搜索樹

二分搜索樹

從圖上我們看出二分搜索樹每個節點的值大於其左子節的所有節點的值小於其右子節點的所有節點的值。

4、前序遍歷

前序遍歷也叫先序遍歷,訪問順序是根左右,也就是先訪問根節點,再到左子樹,最後纔到右子樹。所以上圖所示的訪問順序是 5、3、2、4、8、7、9。

二分搜索樹前序遍歷遞歸版與非遞歸版

    //前序遍歷以node爲根的二分搜索樹,遞歸算法
    private void preOrder(Node node){

        if (node == null)
            return;

        System.out.println(node.e);
        preOrder(node.left);
        preOrder(node.right);
    }
    
    //二分搜索樹的前序遍歷遞歸調用
    public void preOrder(){
        preOrder(root);
    }
    
    //二分搜索樹的前序遍歷非遞歸寫法
    public void preOrderNG(){
        Stack<Node> stack = new Stack<>();
        //根節點
        stack.push(root);

        while (!stack.isEmpty()){
            Node cur = stack.pop();

            System.out.println(cur.e);
            //判斷是否還有葉子節點
            if (cur.right != null)
                stack.push(cur.right);
            if (cur.left != null)
                stack.push(cur.left);
        }
    }

理解非遞歸的實現邏輯、推導出前序遞歸的實現

  • 創建一個堆棧,我們把根節點 5 推入棧中,接下來我們就要訪問 5 這個根節點了,所有把 5 從棧中推出 。
  • 推出的元素有 {5},棧中的元素有 [] 。
  • 在推入 5 的子節點就是 3,8,我們先入後出,先推入 8 再推入 3,現在堆棧的元素有 [8,3],棧頂的 3 就是我們下一次要訪問的節點所以把 3 推出 。
  • 推出的元素有 {5,3},棧中的元素有 [8] 。
  • 在推入 3 的子節點就是 2,4 繼續先入後出,先推入 4 再推入 2,現在堆棧的元素有 [8,4,2],棧頂的 2 就是我們下一次要訪問的節點所以把 2 推出 。
  • 推出的元素有 {5,3,2},棧中的元素有 [8,4] 。
  • 訪問棧頂 4,由於 2 和 4 沒有子節點。所以我們直接把棧頂中的 4 推出 。
  • 推出的元素有 {5,3,2,4},棧中的元素有 [8] 。
  • 訪問棧頂 8 把 8 推出棧堆,再把 8 的子節點 7、9 推入棧中,先推入 9 後推入 7 。
  • 推出的元素有 {5,3,2,4,8},棧中的元素有 [9,7] 。
  • 訪問 7,沒有子節點,推出。
  • 推出的元素有 {5,3,2,4,8,7},棧中的元素有 [9] 。
  • 訪問 9,沒有子節點,推出。
  • 推出的元素有 {5,3,2,4,8,7,9},棧中的元素有 [] 。

5、中序遍歷

中序遍歷,訪問順序是左根右,也就是先訪問左子樹,再到根節點,最後纔到右子樹。所以上圖所示的訪問順序是 2、3、4、5、7、8、9。

二分搜索樹中序遍歷遞歸版與非遞歸版

    //遞歸調用
    public void inOrder(){
        inOrder(root);
    }
    //二分搜索樹的中序遍歷遞歸寫法
    private void inOrder(Node root){
        if (root == null)
            return;

        inOrder(root.left);
        System.out.println(root.e);
        inOrder(root.right);
    }
    //二分搜索樹中序遍歷給遞歸寫法
    public void preInOrderNG(){
        // 創建棧,和前序遍歷類似
        Stack<Node> stack = new Stack<>();
        
        Node node = root;
        //添加暫時完畢,開始pop元素
        while(node!=null || stack.size()>0 ){

            while(node!=null){
                stack.push(node);
                node = node.left;
            }
            //一邊pop並且一邊進行判斷,右結點不會null的,右子樹,繼續按照添加方法,將最左結點全部添加進去
            if(stack.size()>0){
                Node pop = stack.pop();
                System.out.print(pop.e+"  ");
                if(pop.right!=null){
                    node = pop.right;
                }
            }
        }

理解非遞歸的實現邏輯、推導出中序遞歸的實現

  • 首先我們把 5 這個節點推入棧中,再把 5 的左子節點 3 推入,再把 3的 左子節點 2 推入,再把 2 的左子節點推入(此時 2 的左子節點爲空,node==null while 循環退出)。
  • 推出的元素有 {},棧中的元素有 [5,3,2]。
  • node 爲空,但我們棧中還有元素,訪問棧頂元素 2,並查看 2 是否有右子節點。沒有則推出棧並結束循環。
  • 推出的元素有 {2},棧中的元素有 [5,3]。
  • 訪問棧頂元素 3,把 3 推出棧中,並把 3 的右子節點 4 推入棧中,結束循環。
  • 推出的元素有 {2,3},棧中的元素有 [5]。
  • 訪問棧頂元素5,把5推出棧中。把5的右子節點8推入棧中,並把8的左子節點7推入棧中,結束循環。
  • 推出的元素有 {2,3,5},棧中的元素有 [8,7]
  • 訪問棧頂元素 7,並查看 2 是否有右子節點。沒有則推出棧並結束循環。
  • 推出的元素有 {2,3,5,7},棧中的元素有 [8]。
  • 訪問棧頂元素 8,把 8 推出棧中。把 8 的右子節點 9 推入棧中
  • 推出的元素有 {2,3,5,7,8},棧中的元素有 [9]。
  • 訪問棧頂元素 9,並查看 2 是否有右子節點。沒有則推出棧並結束循環。
  • 推出的元素有 {2,3,4,5,7,8,9},棧中的元素有 []。

6、後序遍歷

中序遍歷,訪問順序是左右根,也就是先訪問左子樹,再到右子樹,最後纔到根節點。所以上圖所示的訪問順序是 2、4、3、7、9、8、5。

二分搜索樹後序遍歷遞歸版與非遞歸版

//遞歸調用
public void postOrder() {
    postOrder(root);
} 

//二分搜索樹的後序遍歷遞歸方法
private void postOrder(Node node){
    if (node == null)
        return;

    postOrder(node.left);
    postOrder(node.right);
    System.out.println(node.e);
} 

public void postOrderNG(){
    Stack<Node> stack = new Stack<>();
    //利用一個list集合記錄已將被遍歷過的根節點,防止產生死循環
    ArrayList<Node> list = new ArrayList<>();
    Node node = root;
    Node proud; 
    int flag; 

    //首頁檢查完樹的左子樹,再右子數,最後將根節點輸出
    while (node != null || stack.size() > 0){
        //將最左子樹添加完畢
        while (node != null){
            stack.push(node);

            node = node.left;
        } 

        //和中序遍歷相似,爲先輸出左子節點,但是做節點輸出完畢之後,不能直接將根節點彈出,而是必須先將右節點彈出,
        //最後再將根節點彈出來,就會牽扯到一個根節點的訪問狀態的問題,是否已經被遍歷過了
        if (stack.size() > 0){

            Node peek = stack.peek();
            if (peek.right != null){
                boolean con = list.contains(peek);
                if (con){
                    Node pop = stack.pop();
                    System.out.println(pop.e);
                }else{
                    list.add(peek);
                    node = peek.right;
                }
            }else {
                Node pop = stack.pop();
                System.out.println(pop.e);
            }

        }

    }
}

理解非遞歸的實現邏輯、推導出後序遞歸的實現

  • 把 5 這個節點推入棧中,再把 5 的左子節點 3 推入,再把 3 的左子節點 2 推入,再把 2 的左子節點推入(此時 2 的左子節點爲空,node==null while 循環退出)。
  • 推出的元素有 {},棧中的元素有 [5,3,2]。
  • node 爲空但我們棧中還有元素,訪問棧頂元素 2,並查看 2 是否有左子節點。沒有則推出棧並結束循環。
  • 推出的元素有 {2},棧中的元素有 [5,3]。
  • 訪問棧頂元素 3,3 的右子節爲 4,判斷 list 中是否有 3,沒有則把 3 放入 list 中並把 node 賦值爲 4 結束循環。
  • 推出的元素有 {2},棧中的元素有 [5,3]。
  • node 爲 4,把 4 推入棧中,並訪問棧頂元素 4,並查看 4 是否有右子節點。沒有則推出棧並結束循環。
  • 推出的元素有 {2,4},棧中的元素有 [5,3]。
  • 訪問棧頂元素 3,list 中有 3,把 3 的推出棧中並結束循環。
  • 推出的元素有 {2,4,3},棧中的元素有 [5]。
  • 訪問棧頂元素 5,5 的右子節爲 8,判斷 lis t中是否有 8,沒有則把 5 放入 list 中並把 node 賦值爲 8 結束循環。
  • 推出的元素有 {2,4,3},棧中的元素有 [5]。
  • node 爲 8,把 8 推入棧中,並訪問棧頂元 素8,8 有左子節點爲 7。把 7 推入棧中。
  • 推出的元素有 {2,4,3},棧中的元素有 [5,8,7]。
  • 訪問棧頂元素 7,並查看 7 是否有右子節點。沒有則推出棧並結束循環。
  • 推出的元素有 {2,4,3,7},棧中的元素有 [5,8]。
  • 訪問棧頂元素 8,8 的右子節點爲 9。判斷 list 中是否有 9,沒有則把 8 放入 list 中並把 node 並把 node 賦值爲 9 結束循環。
  • 推出的元素有 {2,4,3,7},棧中的元素有 [5,8]。
  • node 爲 9,把 9 推入棧中,並訪問棧頂元素 9,並查看 9 是否有右子節點。沒有則推出棧並結束循環。
  • 推出的元素有 {2,4,3,7,9},棧中的元素有 [5,8]。
  • 訪問棧頂元素 8,list 中有 8,把 8 的推出棧中並結束循環。
  • 推出的元素有 {2,4,3,7,9,8},棧中的元素有 [5]。
  • node 爲空棧中還有元素,訪問棧頂元素 5,list 中有 5,把 5 的推出棧中並結束循環。
  • 推出的元素有 {2,4,3,7,9,8,5},棧中的元素有 []。

推薦閱讀:

1、java | 什麼是動態代理

2、SpringBoot | 啓動原理

3、SpringBoot | 自動配置原理

一個優秀的廢人

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