1. 程式人生 > >二叉樹遞迴非遞迴遍歷,層次遍歷,反轉,輸出路徑等常見操作詳細總結

二叉樹遞迴非遞迴遍歷,層次遍歷,反轉,輸出路徑等常見操作詳細總結

1.序言

在實際工作中,很多業務場景其實也需要一些比較巧妙的演算法來支撐,並不是業務邏輯就全是複製貼上或者說重複的程式碼寫一百遍。越是隨著演算法研究的深入,越是發現數據結構的重要性。或者說,資料結構中就蘊藏著無數精妙演算法的思想,很多演算法的思想在資料結構中體現得非常突出。而作為一種非線性的資料結構,二叉樹是非常重要非常常見也非常牛逼的一種資料結構,裡面包含有遞迴,棧,佇列,dfs等等很多常見的操作。因此,特意寫一篇比較長的文章,記錄一下二叉樹裡面的一些常見操作以及裡面包含的思想。

2.二叉樹節點定義

二叉樹內部是由一個一個的Node組成的。因此我們一般定義一個Node類。這個沒什麼好說的,直接上程式碼。

    static class Node<T> {
        T data;
        Node left = null;
        Node right = null;

        Node(T data) {
            this.data = data;
        }
    }

3.初始化二叉樹

這個也沒太多好說的,直接上程式碼。

    public static Node init() {
        Node n1 = new Node(1);
        Node n2 = new Node(2);
        Node n3 = new Node(3);
        Node n4 = new Node(4);
        Node n5 = new Node(5);
        Node n6 = new Node(6);
        Node n7 = new Node(7);
        Node n8 = new Node(8);
        n1.left = n2;
        n1.right = n3;
        n2.left = n4;
        n2.right = n5;
        n3.left = n6;
        n3.right = n7;
        n4.left = n8;
        return n1;
    }

返回的是樹的根節點。

4.前序中序後續遍歷遞迴

這個其實也沒有太多好說的。但還是稍微說兩句。
前序遍歷是按根節點->左子樹->右子樹的順序訪問二叉樹。
中序遍歷是按左子樹->根節點->右子樹的順序訪問二叉樹。
後序遍歷是按左子樹->根節點->右子樹的順序訪問二叉樹。
還是直接上程式碼

    public static void preOrder(Node root) {
        if (root != null) {
            System.out.print(root.data + " ");
            preOrder(root.left);
            preOrder(root.right);
        }
    }
    public static void midOrder(Node root) {
        if (root != null) {
            midOrder(root.left);
            System.out.print(root.data + " ");
            midOrder(root.right);
        }
    }
    public static void postOrder(Node root) {
        if (root != null) {
            postOrder(root.left);
            postOrder(root.right);
            System.out.print(root.data + " ");
        }

    }

遞迴的方式簡單明瞭,相信也不用過多解釋。

5.前序中序非遞迴

前序中序遍歷非遞迴的方式較為簡單一些。先說說這兩種情況。

    public static void preOrder2(Node root) {
        Stack<Node> stack = new Stack();
        if (root == null) {
            return;
        }
        stack.push(root);
        while (stack.size() > 0) {
            Node tmp = stack.pop();
            System.out.print(tmp.data + " ");
            if (tmp.right != null) {
                stack.push(tmp.right);
            }
            if (tmp.left != null) {
                stack.push(tmp.left);
            }
        }
    }

    public static void midOrder2(Node root) {
        Stack<Node> stack = new Stack<>();
        while (root != null || ! stack.empty()) {
            while(root != null) {
                stack.push(root);
                root = root.left;
            }
            if (! stack.empty()) {
                Node tmp = stack.pop();
                System.out.print(tmp.data + " ");
                root = tmp.right;
            }
        }
    }

核心思想是利用棧這種資料結構的特點來模擬遞迴。
在前序遍歷中,因為每一次遇到新的節點就要訪問,所以直接用一個棧模擬即可。每次pop出一個最後一個壓入棧頂端的節點並且訪問,然後將該節點的左右子節點壓入棧。注意因為棧的特點是後進先出,而前序遍歷是先訪問左節點再訪問右節點,所以壓棧的時候應該是先壓右子節點再壓左子節點,這樣保證pop的時候是先pop出的左子節點。
而在中序遍歷中,可以想象是先沿著左子樹一直遍歷直到某個節點的左子樹為空,在這個過程中所有的節點自然都被壓入棧中。當某個節點的左子樹為空時,將該節點pop訪問,並開始遍歷該節點的右子樹。

6.後續遍歷的非遞迴

後續遍歷的非遞迴方式最複雜,先上程式碼。

    private void postorder2(Node node) {
        if (node == null) return;

        Node cur = node;
        Node pre = node;

        Stack<Node> stack = new Stack<>();
        // cur移動到左子樹最下面
        while (cur != null) {
            stack.push(cur);
            cur = cur.left;
        }
        while (! stack.isEmpty()) {
            // pop棧頂
            cur = stack.pop();
            if (cur.right != null && cur.right != pre) {
                // 根節點再入棧
                stack.push(cur);
                // 處理右子樹
                cur = cur.right;
                while (cur != null) {
                    // 到右子樹最下面
                    stack.push(cur);
                    cur = cur.left;
                }
            } else {
                System.out.print(cur.data + " ");
                pre = cur;
            }
        }
    }

後續遍歷要先將左右子樹都訪問完畢以後再訪問根節點。所以步驟如下:
1.首先肯定是沿著左子樹往下搜尋,並且一直做壓棧操作。
2.當達到左子樹為空以後,此時棧頂元素出棧。如果該元素右子樹不為空,並且該元素的右子樹未被訪問,則先將該元素再壓棧回去(因為該元素此時未被訪問)。
3.將該元素的右子樹也壓棧,並且沿右子樹的的左子樹繼續搜尋。
4.當右子樹為空或者已經被訪問,此時元素可以被訪問,出棧,訪問,並且將當前訪問節點標記。

7.層次遍歷

層次遍歷是從根節點開始,沿著二叉樹的寬度一層一層往下遍歷。由這個特點不難看出,我們可以利用佇列先進先出的特點來模擬層次遍歷。
更具體地說,我們先讓根節點入佇列,並且訪問根節點。然後讓根節點的左節點入隊,再讓右節點入隊。這樣左結點就儲存在隊頭的位置,將首先被訪問。
訪問完根節點以後,左節點出隊,同時訪問左節點。訪問完畢讓左節點的左右節點依次入隊。此時佇列中的頭節點為根節點的右節點。
上述過程迴圈,一直到佇列為空即可。

    public static void levelOrder(Node root) {
        if (root == null) {
            return;
        }
        Queue<Node> queue = new LinkedList<>();
        queue.offer(root);
        while (!queue.isEmpty()) {
            Node tmp = queue.poll();
            System.out.print(tmp.data + " ");
            if (tmp.left != null) {
                queue.offer(tmp.left);
            }
            if (tmp.right != null) {
                queue.offer(tmp.right);
            }
        }
    }