深入理解二叉树遍历:从递归代码到调用栈

二叉树遍历要解决的问题只有一个:按照什么顺序处理当前节点、左子树和右子树。

左右子树本身也是二叉树,所以可以用同一个函数继续处理。这就是递归。前序、中序和后序并非三种不同思想,只是访问当前节点的时机不同。

一、遍历顺序由访问位置决定

对于任意节点,递归框架都是:

void traverse(TreeNode root) {
    if (root == null) {
        return;
    }

    // 位置一
    traverse(root.left);

    // 位置二
    traverse(root.right);

    // 位置三
}

访问操作放在不同位置,就形成三种遍历:

前序:根 → 左 → 右
中序:左 → 根 → 右
后序:左 → 右 → 根

对应代码如下:

// 前序遍历
void preorder(TreeNode root, List<Integer> result) {
    if (root == null) {
        return;
    }

    result.add(root.value);
    preorder(root.left, result);
    preorder(root.right, result);
}
// 中序遍历
void inorder(TreeNode root, List<Integer> result) {
    if (root == null) {
        return;
    }

    inorder(root.left, result);
    result.add(root.value);
    inorder(root.right, result);
}
// 后序遍历
void postorder(TreeNode root, List<Integer> result) {
    if (root == null) {
        return;
    }

    postorder(root.left, result);
    postorder(root.right, result);
    result.add(root.value);
}

所以,“前、中、后”描述的是根节点相对于左右子树的访问位置:

进入节点时访问              → 前序
左子树处理完后访问          → 中序
左右子树都处理完后访问      → 后序

二、调用栈如何产生遍历顺序

重点理解:

  1. 栈存储的是调用位置。
  2. 梳理清楚前、中、后遍历时,各元素是如何压栈的。

使用下面的二叉树:

            1
          /   \
         2     3
        / \   / \
       4   5 6   7

递归调用时,每进入一个节点,就把该节点的函数压入调用栈。子函数执行期间,父函数暂停;子函数返回后,父函数从暂停位置继续。

需要区分:

压栈:进入函数
访问:处理节点
出栈:函数执行完毕

访问节点不等于函数出栈。

1. 前序遍历

前序遍历进入节点后立即访问,因此压栈路径直接形成访问顺序。

进入并访问 1

栈顶
┌──────────────┐
│ 1:等待左子树 │
└──────────────┘
栈底

输出:1
进入并访问 2

栈顶
┌──────────────┐
│ 2:等待左子树 │
├──────────────┤
│ 1:等待左子树 │
└──────────────┘
栈底

输出:1 2
进入并访问 4

栈顶
┌──────────────┐
│ 4:正在执行   │
├──────────────┤
│ 2:等待左子树 │
├──────────────┤
│ 1:等待左子树 │
└──────────────┘
栈底

输出:1 2 4

4 完成后出栈,再进入 52 完成后出栈,再进入 3、6、7

完整访问顺序:
1 → 2 → 4 → 5 → 3 → 6 → 7

2. 中序遍历

中序遍历先进入左子树,因此开始时只压栈,不访问。

依次进入 1、2、4

栈顶
┌──────────────┐
│ 4:等待左子树 │
├──────────────┤
│ 2:等待左子树 │
├──────────────┤
│ 1:等待左子树 │
└──────────────┘
栈底

输出:空

4 的左子树为空,所以访问 4 并返回。随后恢复 2,访问 2,再进入右子树 5

栈顶
┌──────────────┐
│ 5:正在执行   │
├──────────────┤
│ 2:等待右子树 │
├──────────────┤
│ 1:等待左子树 │
└──────────────┘
栈底

输出:4 → 2

5 完成后,2 才能出栈。随后访问 1,再以相同方式处理右子树。

完整访问顺序:
4 → 2 → 5 → 1 → 6 → 3 → 7

中序遍历的栈中保存的是:左子树尚未完成,或者右子树尚未处理的节点。

3. 后序遍历

后序遍历必须等左右子树都完成后,才能访问当前节点。

依次进入 1、2、4

栈顶
┌──────────────┐
│ 4:等待子树   │
├──────────────┤
│ 2:等待左子树 │
├──────────────┤
│ 1:等待左子树 │
└──────────────┘
栈底

输出:空

4 没有子树,因此访问并出栈。接着进入并访问 5。此时 2 的左右子树都已完成,才访问 2

栈顶
┌────────────────┐
│ 2:左右均已完成 │
├────────────────┤
│ 1:等待左子树   │
└────────────────┘
栈底

输出:4 → 5 → 2

右子树同理,依次访问 6、7、3。最后,节点 1 的左右子树都完成,访问 1

完整访问顺序:
4 → 5 → 2 → 6 → 7 → 3 → 1

后序遍历体现的是依赖关系:必须先得到左右子树的结果,才能处理当前节点。

三、完整实现与复杂度

import java.util.ArrayList;
import java.util.List;

public class BinaryTreeTraversal {

    static class TreeNode {
        int value;
        TreeNode left;
        TreeNode right;

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

    // 前序:根 -> 左 -> 右
    static void preorder(
            TreeNode root,
            List<Integer> result) {

        if (root == null) {
            return;
        }

        result.add(root.value);
        preorder(root.left, result);
        preorder(root.right, result);
    }

    // 中序:左 -> 根 -> 右
    static void inorder(
            TreeNode root,
            List<Integer> result) {

        if (root == null) {
            return;
        }

        inorder(root.left, result);
        result.add(root.value);
        inorder(root.right, result);
    }

    // 后序:左 -> 右 -> 根
    static void postorder(
            TreeNode root,
            List<Integer> result) {

        if (root == null) {
            return;
        }

        postorder(root.left, result);
        postorder(root.right, result);
        result.add(root.value);
    }

    public static void main(String[] args) {
        /*
         *             1
         *           /   \
         *          2     3
         *         / \   / \
         *        4   5 6   7
         */
        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);

        root.right.left = new TreeNode(6);
        root.right.right = new TreeNode(7);

        List<Integer> preorderResult = new ArrayList<>();
        List<Integer> inorderResult = new ArrayList<>();
        List<Integer> postorderResult = new ArrayList<>();

        preorder(root, preorderResult);
        inorder(root, inorderResult);
        postorder(root, postorderResult);

        System.out.println("前序遍历:" + preorderResult);
        System.out.println("中序遍历:" + inorderResult);
        System.out.println("后序遍历:" + postorderResult);
    }
}

输出:

前序遍历:[1, 2, 4, 5, 3, 6, 7]
中序遍历:[4, 2, 5, 1, 6, 3, 7]
后序遍历:[4, 5, 2, 6, 7, 3, 1]

每个节点只被处理一次,因此:

时间复杂度:O(n)
空间复杂度:O(h)

其中,n 是节点数量,h 是树高。空间消耗来自递归调用栈。

最终可以将三种遍历统一为一句话:

递归负责处理左右子树,访问根节点的位置决定遍历类型:访问在左递归之前是前序,在左右递归之间是中序,在右递归之后是后序。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

roman_日积跬步-终至千里

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值