【二叉树】前、中、后 序遍历(递归+迭代)

本文详细介绍了二叉树的前序、中序和后序遍历的递归与迭代实现方法,包括直接做法和标记法,并提供了反转前序遍历结果以得到后序遍历的方法。内容涵盖了算法实现细节和思路解析。

leetcode 练习题目

144. 二叉树的前序遍历
94. 二叉树的中序遍历
145. 二叉树的后序遍历

二叉树定义

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. 先序遍历

递归

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

    private void dfs(TreeNode root, List<Integer> res) {
        if (root == null) {
            return;
        }
        res.add(root.val);
        dfs(root.left, res);
        dfs(root.right, res);
    }
}

迭代

直接做法

用栈模拟递归。先序遍历比较简单,访问的顺序和添加到结果的顺序是一致的,先序是:中左右,访问二叉树也是先访问中间节点,所以在访问的同时将当前节点加入结果列表中即可。

唯一需要注意的是,因为是用栈模拟的递归,所以若希望以 中左右 的顺序加入结果,需要先把右节点加入栈中,再把左节点接入栈中。

class Solution {
    public List<Integer> preorderTraversal(TreeNode root) {
        List<Integer> res = new ArrayList<>();
        Deque<TreeNode> stack = new LinkedList<>();
        if (root != null) {
            stack.push(root);
        }
        while (!stack.isEmpty()) {
            TreeNode curr = stack.pop();
            res.add(curr.val);
            if (curr.right != null) {
                stack.push(curr.right);
            }
            if (curr.left != null) {
                stack.push(curr.left);
            }
        }
        return res;
    }
}
标记法

先序遍历是:中左右。由于是栈,访问到某一个节点,所以先把右节点入栈,再把左节点入栈,并且不把当前节点加入结果,把访问过的当前节点再次放入栈中,但是在其后再 push 到栈中一个标记 null。如果遇到栈顶是 null ,就把栈顶加入结果。

这样放的意义在于,用栈来维护结果的顺序,带 null 的节点再出栈时,访问顺序一定是 中左右。如果当前节点加入栈中,在下一次循环访问栈顶的时候,中间节点带 null 必然会加入结果,然后是左节点作为中间节点,左节点的右节点和左节点入栈,再把左节点入栈再在其后 push 一个标记 null,然后再下一次循环左节点带 null 必然会加入结果,以此类推,左子树都结束之后,再才是右节点作为中间节点。所以可以得到 [中, [左子树], [右子树]] 这样的效果。

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

2. 中序遍历

递归

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

    private void dfs(TreeNode root, List<Integer> res) {
        if (root == null) {
            return;
        }
        dfs(root.left, res);
        res.add(root.val);
        dfs(root.right, res);
    }
}

迭代

直接做法

中序遍历是:左中右。所以需要先访问当前节点最左的节点。定义一个当前节点变量,使得它一直循环向左走直到变为 null,走到当前节点的最左,在走的过程中将路过的节点入栈记录下来。在走到最左后取栈顶,此时栈顶就是最左那个节点。将栈顶添加到结果(这相当于最左的中间节点),然后转向当前节点的右孩子继续(因为中间节点访问完,按顺序后面是右节点)

class Solution {
    public List<Integer> inorderTraversal(TreeNode root) {
        List<Integer> res = new ArrayList<>();
        Deque<TreeNode> stack = new LinkedList<>();
        TreeNode curr = root;
        while (curr != null || !stack.isEmpty()) {
            while (curr != null) {
                stack.push(curr);
                curr = curr.left;
            }
            curr = stack.pop();
            res.add(curr.val);
            curr = curr.right;
        }
        return res;
    }
}
标记法

中序遍历是:左中右。由于是栈,访问到某一个节点,所以先把右节点入栈,再中间节点,并在其后再 push 到栈中一个标记 null,再把左节点入栈,并且不把当前节点加入结果。之后如果遇到栈顶是 null ,就把栈顶加入结果。

这样放的意义在于,用栈来维护结果的顺序,带 null 的节点再出栈时,访问顺序一定是 [[左子树], 中, [右子树]]。如果当前节点加入栈中,在下一次循环访问栈顶的时候,左节点作为中间节点,左节点的右节点入栈,再把左节点入栈再在其后 push 一个标记 null,再把左节点的左节点入栈,以此类推,如此左子树都结束之后,再才是中间节点,中间节点带 null 加入结果,然后是右节点作为中间节点。所以总体来看即 [[左子树], 中, [右子树]]

class Solution {
    public List<Integer> inorderTraversal(TreeNode root) {
        List<Integer> res = new ArrayList<>();
        Deque<TreeNode> stack = new LinkedList<>();
        if (root != null) {
            stack.push(root);
        }
        while (!stack.isEmpty()) {
            TreeNode curr = stack.pop();
            if (curr != null) {
                if (curr.right != null) {
                    stack.push(curr.right);
                }
                stack.push(curr);
                stack.push(null);
                if (curr.left != null) {
                    stack.push(curr.left);
                }
            } else {
                curr = stack.pop();
                res.add(curr.val);
            }
        }
        return res;
    }
}

3. 后序遍历

递归

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

    private void dfs(TreeNode root, List<Integer> res) {
        if (root == null) {
            return;
        }
        dfs(root.left, res);
        dfs(root.right, res);
        res.add(root.val);
    }
}

迭代

直接做法

后序比较困难,因为后序遍历的顺序是:左右中,一直走左节点,走到头,还是不能访问中间节点,得把右节点再访问完,回来才能访问中间节点。那就需要判断,中间节点的右子树什么时候都访问完了?因为后序遍历的顺序是: [[左子树], [右子树]], 中,如果中间节点的右节点是结果集的最后一个元素,那么说明它的右子树都访问过了,这时可以访问中间节点了。

所以可以记录上一次的输出。首先一直循环向左走直到为 null,走到最左,在走的过程中将节点入栈记录下来,走到 null 之后取栈顶,当前栈顶就是最左的节点,如果栈顶右孩子为 null,或者栈顶右孩子是上一次的输出,添加栈顶到结果集,否则还没到这个中间节点,还得把它入栈,然后向右孩子继续探索,直到它的右子树都访问完,即上一次的输出是它的右孩子。这样即能保证 [[左子树], [右子树]], 中 的顺序。

class Solution {
    public List<Integer> postorderTraversal(TreeNode root) {
        List<Integer> res = new ArrayList<>();
        Deque<TreeNode> stack = new LinkedList<>();
        TreeNode curr = root;
        TreeNode pre = null;
        while (curr != null || !stack.isEmpty()) {
            while (curr != null) {
                stack.push(curr);
                curr = curr.left;
            }
            curr = stack.pop();
            if (curr.right == null || curr.right == pre) {
                res.add(curr.val);
                pre = curr;
                curr = null;
            } else {
                stack.push(curr);
                curr = curr.right;
            }
        }
        return res;
    }
}
标记法

后序遍历是:左右中。由于是栈,访问到某一个节点,所以先把中间节点入栈,并在其后再 push 到栈中一个标记 null,再把右节点入栈,再左节点入栈,并且不把当前节点加入结果。之后如果遇到栈顶是 null ,就把栈顶加入结果。

这样放的意义在于,用栈来维护结果的顺序,带 null 的节点再出栈时,访问顺序一定是 [[左子树], [右子树], 中]。如果当前节点加入栈中,在下一次循环访问栈顶的时候,左节点作为中间节点,左节点入栈再在其后 push 一个标记 null,然后把左节点的右节点入栈,再把左节点的左节点入栈,以此类推,如此左子树都结束之后,再才是右节点作为中间节点,最后才是带 null 的中间节点,这时中间节点带 null 加入结果。所以总体来看即 [[左子树], [右子树], 中]

class Solution {
    public List<Integer> postorderTraversal(TreeNode root) {
        List<Integer> res = new ArrayList<>();
        Deque<TreeNode> stack = new LinkedList<>();
        if (root != null) {
            stack.push(root);
        }
        while (!stack.isEmpty()) {
            TreeNode curr = stack.pop();
            if (curr != null) {
                stack.push(curr);
                stack.push(null);
                if (curr.right != null) {
                    stack.push(curr.right);
                }
                if (curr.left != null) {
                    stack.push(curr.left);
                }
            } else {
                curr = stack.pop();
                res.add(curr.val);
            }
        }
        return res;
    }
}
反转类前序遍历结果

前序遍历的顺序是:[中, [左子树], [右子树]],后序遍历是:[[左子树], [右子树], 中],将容易写的先序遍历的直接做法的写法改成 [中, [右子树], [左子树]],得到这样的类前序遍历的结果之后,最后再反转结果即是后序遍历的结果。

class Solution {
    public List<Integer> postorderTraversal(TreeNode root) {
        List<Integer> res = new ArrayList<>();
        Deque<TreeNode> stack = new LinkedList<>();
        if (root != null) {
            stack.push(root);
        }
        while (!stack.isEmpty()) {
            TreeNode curr = stack.pop();
            res.add(curr.val);
            if (curr.left != null) {
                stack.push(curr.left);
            }
            if (curr.right != null) {
                stack.push(curr.right);
            }
        }
        Collections.reverse(res);
        return res;
    }
}

Reference

  1. 代码随想录:二叉树:3. 二叉树的迭代遍历
  2. 代码随想录:二叉树:4. 二叉树的统一迭代法 (标记法)
  3. leetcode:145. 二叉树的后序遍历:官方题解 (后序迭代直接做法)
  4. leetcode:145. 二叉树的后序遍历:题解:Program120:JAVA 迭代法 (后序迭代直接做法)
当然可以!下面将详细讲解并实现二叉树的**序、中序、后序遍历**的**递归迭代方式**,并对它们的**时间与空间复杂度进行分析**。 --- ## ✅ 一、定义二叉树节点 ```cpp struct TreeNode { int val; TreeNode* left; TreeNode* right; TreeNode(int x) : val(x), left(nullptr), right(nullptr) {} }; ``` --- ## ✅ 二、递归遍历 ### 1. 序遍历(根 -> 左 -> 右) ```cpp void preorderTraversal(TreeNode* root) { if (!root) return; std::cout << root->val << " "; preorderTraversal(root->left); preorderTraversal(root->right); } ``` ### 2. 中序遍历(左 -> 根 -> 右) ```cpp void inorderTraversal(TreeNode* root) { if (!root) return; inorderTraversal(root->left); std::cout << root->val << " "; inorderTraversal(root->right); } ``` ### 3. 后序遍历(左 -> 右 -> 根) ```cpp void postorderTraversal(TreeNode* root) { if (!root) return; postorderTraversal(root->left); postorderTraversal(root->right); std::cout << root->val << " "; } ``` --- ## ✅ 三、迭代遍历 ### 1. 序遍历迭代) ```cpp void preorderIterative(TreeNode* root) { if (!root) return; std::stack<TreeNode*> stack; stack.push(root); while (!stack.empty()) { TreeNode* node = stack.top(); stack.pop(); std::cout << node->val << " "; if (node->right) stack.push(node->right); // 先压右,后出左 if (node->left) stack.push(node->left); } } ``` ### 2. 中序遍历迭代) ```cpp void inorderIterative(TreeNode* root) { std::stack<TreeNode*> stack; TreeNode* curr = root; while (curr || !stack.empty()) { while (curr) { stack.push(curr); curr = curr->left; } curr = stack.top(); stack.pop(); std::cout << curr->val << " "; curr = curr->right; } } ``` ### 3. 后序遍历迭代) #### 方法一:使用两个栈 ```cpp void postorderIterative(TreeNode* root) { if (!root) return; std::stack<TreeNode*> s1, s2; s1.push(root); while (!s1.empty()) { TreeNode* node = s1.top(); s1.pop(); s2.push(node); if (node->left) s1.push(node->left); if (node->right) s1.push(node->right); } while (!s2.empty()) { std::cout << s2.top()->val << " "; s2.pop(); } } ``` --- ## ✅ 四、复杂度分析 | 遍历方式 | 时间复杂度 | 空间复杂度 | 说明 | |----------|------------|-------------|------| | 递归序 | O(n) | O(h) | h为树的高度(递归栈) | | 递归中序 | O(n) | O(h) | 同上 | | 递归后序 | O(n) | O(h) | 同上 | | 迭代序 | O(n) | O(h) | 使用栈模拟递归 | | 迭代中序 | O(n) | O(h) | 同上 | | 迭代后序 | O(n) | O(n) | 可能使用双栈,空间略大 | > **n** 为节点总数,**h** 为树的高度(平均为 log n,最坏为 n) --- ## ✅ 五、完整测试示例 ```cpp int main() { TreeNode* root = new TreeNode(1); root->left = new TreeNode(2); root->right = new TreeNode(3); root->left->left = new TreeNode(4); root->left->right = new TreeNode(5); std::cout << "递归: "; preorderTraversal(root); std::cout << std::endl; std::cout << "中序递归: "; inorderTraversal(root); std::cout << std::endl; std::cout << "后序递归: "; postorderTraversal(root); std::cout << std::endl; std::cout << "迭代: "; preorderIterative(root); std::cout << std::endl; std::cout << "中序迭代: "; inorderIterative(root); std::cout << std::endl; std::cout << "后序迭代: "; postorderIterative(root); std::cout << std::endl; return 0; } ``` ---
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值