二叉樹的先,中,後序遍歷

Grey Zeng發表於2022-02-27

作者:Grey

原文地址:二叉樹的先,中,後序遍歷

說明

本文主要介紹了二叉樹的先序,中序,後序遍歷。並且分別用如下三種方式實現:

  1. 遞迴方法
  2. 非遞迴(使用棧)
  3. Morris遍歷方法,空間複雜度可以做到O(1)

示例二叉樹

image

資料結構

public static class TreeNode {
    int val;
    TreeNode left;
    TreeNode right;

    TreeNode() {
    }

    TreeNode(int val) {
        this.val = val;
    }

    TreeNode(int val, TreeNode left, TreeNode right) {
        this.val = val;
        this.left = left;
        this.right = right;
    }
}

先序遍歷

先序遍歷流程

先頭,再左,再右。

示例中的二叉樹,先序遍歷的結果為:

1-->2-->4-->7-->11-->8-->12-->3-->5-->6-->9-->13-->10

遞迴方法實現先序遍歷

class Solution {
    public static List<Integer> preorderTraversal(TreeNode root) {
        List<Integer> ans = new ArrayList<>();
        p(root, ans);
        return ans;
    }

    public static void p(TreeNode node, List<Integer> ans) {
        if (node == null) {
            return;
        }
        ans.add(node.val);
        p(node.left, ans);
        p(node.right, ans);
    }
}

使用棧實現先序遍歷

整個流程是分如下幾個步驟:

第一步,申請一個棧,並把頭節點壓入。

第二步,彈出就收集答案。

第三步,第二步中彈出的節點,如果右孩子不為空,則右孩子入棧。

第四步,第二步中彈出的節點,如果左孩子不為空,則左孩子入棧。

第五步,迴圈執行第二步到第四步,直到棧為空。

class Solution {
    public static List<Integer> preorderTraversal(TreeNode root) {
        List<Integer> ans = new ArrayList<>();
        if (root == null) {
            return ans;
        }
        Stack<TreeNode> stack = new Stack<>();
        stack.push(root);
        while (!stack.isEmpty()) {
            TreeNode pop = stack.pop();
            ans.add(pop.val);
            if (pop.right != null) {
                stack.push(pop.right);
            }
            if (pop.left != null) {
                stack.push(pop.left);
            }
        }
        return ans;
    }
}

測評連結:LeetCode 144. Binary Tree Preorder Traversal

中序遍歷

中序遍歷流程

先中,再左,再右。

示例中的二叉樹,中序遍歷的結果為:

2-->11-->7-->4-->12-->8-->1-->5-->3-->9-->13-->6-->10

遞迴方法實現中序遍歷

class Solution {
    public List<Integer> inorderTraversal(TreeNode root) {
        List<Integer> ans = new ArrayList<>();
        p(root, ans);
        return ans;
    }

    public static void p(TreeNode root, List<Integer> ans) {
        if (root != null) {
            p(root.left, ans);
            ans.add(root.val);
            p(root.right, ans);
        }
    }
}

使用棧實現中序遍歷

也是申請一個棧,有如下幾個步驟:

第一步,整條左邊界入棧。

第二步,彈出就收集答案。

第三步,來到右樹上執行同第一步的操作。

第四步,直到棧為空。

class Solution {
    public List<Integer> inorderTraversal(TreeNode head) {
        List<Integer> ans = new ArrayList<>();
        if (head == null) {
            return ans;
        }
        Stack<TreeNode> stack = new Stack<>();
        TreeNode cur = head;
        while (!stack.isEmpty() || cur != null) {
            if (cur != null) {
                stack.push(cur);
                cur = cur.left;
            } else {
                TreeNode pop = stack.pop();
                ans.add(pop.val);
                cur = pop.right;
            }
        }
        return ans;
    }
}

測評連結:LeetCode 94. Binary Tree Inorder Traversal

後序遍歷

後序遍歷流程

先左,後右,再中。

示例中的二叉樹,後序遍歷的結果為:

11-->7-->12-->8-->4-->2-->5-->13-->9-->10-->6-->3-->1

遞迴方法實現後序遍歷

class Solution {
    public List<Integer> postorderTraversal(TreeNode root) {
        List<Integer> ans = new ArrayList<>();
        p(root, ans);
        return ans;
    }

    public static void p(TreeNode root, List<Integer> ans) {
        if (root != null) {
            p(root.left, ans);
            p(root.right, ans);
            ans.add(root.val);
        }
    }
}

使用兩個棧實現後序遍歷

由於我們已經可以通過棧來實現先序遍歷,即:先頭,再左,再右。

而後序遍歷的流程是:先左,再右,再頭。

所以我們可以通過先序遍歷的程式碼簡單加工得到後序遍歷的程式碼。

首先,我們先通過先序遍歷的程式碼,將先序遍歷加工成:先頭,再右,再左。

把這個結果放入一個棧中,假設這個棧叫helper, 然後將helper中的內容依次彈出,便是後序遍歷的結果。

class Solution {
    public List<Integer> postorderTraversal(TreeNode root) {
        List<Integer> ans = new ArrayList<>();
        if (root == null) {
            return ans;
        }
        Stack<TreeNode> stack = new Stack<>();
        Stack<TreeNode> helper = new Stack<>();
        stack.push(root);
        while (!stack.isEmpty()) {
            TreeNode pop = stack.pop();
            helper.push(pop);
            if (pop.left != null) {
                stack.push(pop.left);
            }
            if (pop.right != null) {
                stack.push(pop.right);
            }
        }
        while (!helper.isEmpty()) {
            ans.add(helper.pop().val);
        }
        return ans;
    }
}

測評連結:LeetCode 145. Binary Tree Postorder Traversal

Morris遍歷

以上提到的二叉樹的先,中,後序遍歷演算法,時間複雜度O(N),但是空間複雜度O(h),其中h是樹的高度。Morris遍歷也可以實現二叉樹的先,中,後序遍歷,且時間複雜度O(N), 空間複雜度可以做到O(1)

Morris遍歷流程

Morris遍歷的流程主要分如下幾個步驟:

第一步,從頭節點開始遍歷。

第二步,假設當前遍歷的節點是cur

第三步,如果cur無左樹, cur來到其右樹上,即:cur = cur.right

第四步,如果cur有左樹,找到cur左樹最右節點,假設叫mostRight,則有如下兩種小情況:

情況1,如果mostRight的右指標指向空, 則將mostRight的右指標指向cur,即:mostRight.right = cur, 然後將cur向左移動,即:cur = cur.left

情況2,如果mostRight的右指標指向當前節點cur,則將mostRight的右指標指向空,即:mostRight.right = null,然後將cur向右移動,即:cur = cur.right

第五步:當cur = null,遍歷結束。

程式碼實現如下:

// morris遍歷
public class Code_0047_Morris {
    public static void morris(TreeNode head) {
        if (head == null) {
            return;
        }
        // System.out.println("....morris order....");
        TreeNode cur = head;
        // System.out.print(cur.val + "-->");
        TreeNode mostRight;
        while (cur != null) {
            mostRight = cur.left;
            if (mostRight != null) {
                while (mostRight.right != null && mostRight.right != cur) {
                    mostRight = mostRight.right;
                }
                if (mostRight.right == null) {
                    mostRight.right = cur;
                    cur = cur.left;
                    // System.out.print(cur.val + "-->");
                    continue;
                } else {
                    mostRight.right = null;
                }
            }
            cur = cur.right;
            // if (cur != null) {
            //     System.out.print(cur.val + "-->");
            // }
        }
    }
}

根據如上流程,示例二叉樹的Morris遍歷序列為:

1-->2-->4-->7-->11-->7-->4-->8-->12-->8-->1-->3-->5-->3-->6-->9-->13-->6-->10

Morris遍歷可以實現在O(N)時間複雜度內,用O(1)的空間複雜度實現對樹的遍歷,而且,只要某個節點有右樹,則這個節點一定會被遍歷兩次,我們可以通過Morris遍歷來實現二叉樹的先,中,後序遍歷,做到時間複雜度O(N),空間複雜度O(1)

Morris遍歷實現先序遍歷

根據Morris的遍歷結果,沒有右樹的點只會遍歷一次,有右樹的點會遍歷兩次,針對遍歷一次的點,遍歷到就收集,針對遍歷兩次的點,第一次遍歷到就收集,第二次遍歷到不收集,整個流程跑完,則得到了先序遍歷的結果。

程式碼如下:

class Solution {
    public static List<Integer> preorderTraversal(TreeNode root) {
        List<Integer> ans = new ArrayList<>();
        if (root == null) {
            return ans;
        }
        TreeNode mostRight;
        TreeNode cur = root;
        while (cur != null) {
            mostRight = cur.left;
            if (mostRight != null) {
                while (mostRight.right != null && mostRight.right != cur) {
                    mostRight = mostRight.right;
                }
                if (mostRight.right == null) {
                    // 來到自己兩次的點,在第一次來到自己就收集!!!!
                    ans.add(cur.val);
                    mostRight.right = cur;
                    cur = cur.left;
                    continue;
                } else {
                    mostRight.right = null;
                }
            } else {
                // 只來到自己一次的點,來到就收集。
                ans.add(cur.val);
            }
            cur = cur.right;
        }
        return ans;
    }
}

測評連結:LeetCode 144. Binary Tree Preorder Traversal

Morris遍歷實現中序遍歷

針對遍歷一次的點,遍歷到就收集,針對遍歷兩次的點,第一次遍歷到不收集,第二次遍歷才收集,整個流程跑完,則得到了中序遍歷的結果。

程式碼如下:

class Solution {
    public List<Integer> inorderTraversal(TreeNode root) {
        if (root == null) {
            return new ArrayList<>();
        }
        List<Integer> ans = new ArrayList<>();
        TreeNode mostRight;
        TreeNode cur = root;
        while (cur != null) {
            mostRight = cur.left;
            if (mostRight != null) {
                while (mostRight.right != null && mostRight.right != cur) {
                    mostRight = mostRight.right;
                }
                if (mostRight.right == null) {
                    mostRight.right = cur;
                    cur = cur.left;
                    continue;
                } else {
                    // 來到自己兩次的點,第二次來到才收集
                    ans.add(cur.val);
                    mostRight.right = null;
                }
            } else {
                // 只來到自己一次的點,來到就收集
                ans.add(cur.val);
            }
            cur = cur.right;
        }
        return ans;
    }
}

測評連結:LeetCode 94. Binary Tree Inorder Traversal

Morris遍歷實現後序遍歷

Morris遍歷實現後序遍歷相對比較麻煩,處理時機只放在能回到自己兩次的點,能回到自己兩次的點在第二次回到自己的時刻,不列印它自己,而是逆序列印他左樹的右邊界, 整個遍歷結束後,單獨逆序列印整棵樹的右邊界,即得到了後序遍歷的結果。

程式碼如下:

class Solution {
    public List<Integer> postorderTraversal(TreeNode head) {
        List<Integer> ans = new ArrayList<>();
        if (null == head) {
            return ans;
        }
        TreeNode cur = head;
        TreeNode mostRight;
        while (cur != null) {
            mostRight = cur.left;
            if (mostRight != null) {
                while (mostRight.right != null && mostRight.right != cur) {
                    mostRight = mostRight.right;
                }
                if (mostRight.right == null) {
                    mostRight.right = cur;
                    cur = cur.left;
                    continue;
                } else {
                    // 有左樹的點第二次到達自己的時候
                    mostRight.right = null;
                    collectLeftTreeRightEdge(cur.left, ans);
                }
            }
            cur = cur.right;
        }
        collectLeftTreeRightEdge(head, ans);
        return ans;
    }

    // 逆序收集左樹的右邊界
    private static void collectLeftTreeRightEdge(TreeNode head, List<Integer> ans) {
        TreeNode tail = reverse(head);
        TreeNode c = tail;
        while (c != null) {
            ans.add(c.val);
            c = c.right;
        }
        reverse(tail);
    }

    public static TreeNode reverse(TreeNode node) {
        TreeNode pre = null;
        TreeNode cur = node;
        while (cur != null) {
            TreeNode t = cur.right;
            cur.right = pre;
            pre = cur;
            cur = t;
        }
        return pre;
    }
}

需要注意兩點:

第一點,collectLeftTreeRightEdge方法即逆序收集左樹的有邊界,由於每個節點沒有指向父的指標,所以,要實現逆序,需要針對右邊界採用反轉連結串列的方式。即reverse函式的邏輯。

第二點,在collectLeftTreeRightEdge方法呼叫完反轉連結串列操作後,還要還原整個右邊界。否則整棵樹的指標就指亂了。

測評連結:LeetCode 145. Binary Tree Postorder Traversal

更多

演算法和資料結構筆記

參考資料

演算法和資料結構體系班-左程雲

相關文章