「最大子数组和」的 Kadane 算法中回过神来,我们正式踏入一个全新的领域——二叉树

4 阅读5分钟

从「最大子数组和」的 Kadane 算法中回过神来,我们正式踏入一个全新的领域——二叉树

如果说数组和链表是线性的“一维世界”,那么二叉树就是充满分支的“二维世界”。它是后续众多高级数据结构(如二叉搜索树、堆、平衡树)的基石。而理解二叉树的起点,就是掌握它的四种基本遍历方式

很多人觉得遍历很简单,递归几行就搞定了。但当面试官追问“如何用迭代实现?”或者“如何统一三种遍历的迭代写法?”时,很多人就卡住了。今天就让我们从最直观的递归开始,一步步深入到迭代,最终抵达那个让人眼前一亮的“统一迭代法”。

📖 二叉树的四种遍历方式

在开始之前,我们先明确这四种遍历方式的定义:

  • 前序遍历 -> 左子树 -> 右子树
  • 中序遍历:左子树 -> -> 右子树
  • 后序遍历:左子树 -> 右子树 ->
  • 层序遍历:从上到下、从左到右,逐层访问

第一层:递归法 —— 最直观的“分而治之”

递归是理解树结构最自然的方式。只要按照遍历的顺序,在递归函数的不同位置处理当前节点即可。

// 递归三要素
class Solution {
    // 1. 确定递归函数的参数和返回值
    public List<Integer> preorderTraversal(TreeNode root) {
        List<Integer> res = new ArrayList<>();
        // 2. 确定终止条件
        preorder(root, res);
        return res;
    }

    void preorder(TreeNode node, List<Integer> res) {
        if (node == null) return; // 终止条件
        
        // 3. 确定单层递归的逻辑
        res.add(node.val);       // 前序:处理根
        preorder(node.left, res);
        // 中序:处理根 (res.add(node.val);)
        preorder(node.right, res);
        // 后序:处理根 (res.add(node.val);)
    }
}
  • 复杂度:时间 O(n),空间 O(n)(递归栈深度)。

第二层:迭代法(标准版) —— 手动模拟递归栈

递归是系统帮我们维护了一个栈。迭代则是我们手动维护一个栈来模拟这个过程。

前序遍历(最简单)

前序遍历的顺序是“根-左-右”。我们可以利用栈的“先进后出”特性,先压入右孩子,再压入左孩子,这样出栈的顺序就是“左-右”。

class Solution {
    public List<Integer> preorderTraversal(TreeNode root) {
        List<Integer> res = new ArrayList<>();
        if (root == null) return res;
        Stack<TreeNode> stack = new Stack<>();
        stack.push(root);
        while (!stack.isEmpty()) {
            TreeNode node = stack.pop();
            res.add(node.val); // 处理根节点
            // 先压右,再压左,保证左子树先被处理
            if (node.right != null) stack.push(node.right);
            if (node.left != null) stack.push(node.left);
        }
        return res;
    }
}

中序遍历(需一路向左)

中序遍历“左-根-右”的逻辑是:需要一直找到最左的节点,然后再处理根和右子树。

class Solution {
    public List<Integer> inorderTraversal(TreeNode root) {
        List<Integer> res = new ArrayList<>();
        Stack<TreeNode> stack = new Stack<>();
        TreeNode cur = root;
        while (cur != null || !stack.isEmpty()) {
            // 1. 一路向左,把所有左节点压入栈
            while (cur != null) {
                stack.push(cur);
                cur = cur.left;
            }
            // 2. 弹出最左节点,处理它(根)
            cur = stack.pop();
            res.add(cur.val);
            // 3. 转向右子树
            cur = cur.right;
        }
        return res;
    }
}

后序遍历(最烧脑)

后序“左-右-根”是最复杂的。一个技巧是:前序是“根-左-右”,我们稍作修改得到“根-右-左”,再反转结果就是“左-右-根”。

class Solution {
    public List<Integer> postorderTraversal(TreeNode root) {
        List<Integer> res = new ArrayList<>();
        if (root == null) return res;
        Stack<TreeNode> stack = new Stack<>();
        stack.push(root);
        // 1. 按照“根-右-左”的顺序遍历
        while (!stack.isEmpty()) {
            TreeNode node = stack.pop();
            res.add(node.val);
            if (node.left != null) stack.push(node.left);
            if (node.right != null) stack.push(node.right);
        }
        // 2. 反转结果,得到“左-右-根”
        Collections.reverse(res);
        return res;
    }
}

第三层:迭代法(统一版) —— “一套代码走天下”

上面三种迭代法的逻辑各不相同,容易记混。有一种更优雅的统一写法:在栈中同时存储节点和一个空标记,用来标识该节点是否应该被处理。

核心思想:如果从栈中弹出的是节点,说明它还没被处理,就按照逆序将其和孩子节点压回栈中,并在节点后插入一个null标记。如果弹出的是null,说明下一个节点该被处理了。

class Solution {
    public List<Integer> preorderTraversal(TreeNode root) {
        List<Integer> res = new ArrayList<>();
        if (root == null) return res;
        Stack<Object> stack = new Stack<>(); // 用Object类型来容纳TreeNode和null
        stack.push(root);
        while (!stack.isEmpty()) {
            Object obj = stack.pop();
            if (obj instanceof TreeNode) {
                TreeNode node = (TreeNode) obj;
                // 前序:顺序是 根-左-右,入栈顺序应为 右-左-根
                if (node.right != null) stack.push(node.right);
                if (node.left != null) stack.push(node.left);
                stack.push(node);   // 当前节点
                stack.push(null);   // 标记
            } else {
                // 遇到null标记,下一个节点就是要处理的节点
                res.add(((TreeNode) stack.pop()).val);
            }
        }
        return res;
    }
}

改变遍历顺序只需调整入栈顺序

  • 前序(根-左-右):入栈顺序 右 -> 左 -> 根
  • 中序(左-根-右):入栈顺序 右 -> 根 -> 左
  • 后序(左-右-根):入栈顺序 根 -> 右 -> 左

这种方式极大地降低了记忆成本,理解了这一种,就等于理解了三种。

第四层:层序遍历(BFS) —— 用“队列”逐层推进

以上三种都属于深度优先搜索(DFS),而层序遍历是广度优先搜索(BFS)

它利用**队列(Queue)**的先进先出特性,像水波一样一圈圈地向外扩散。关键是记录每一层的节点数 size

class Solution {
    public List<List<Integer>> levelOrder(TreeNode root) {
        List<List<Integer>> res = new ArrayList<>();
        if (root == null) return res;
        Queue<TreeNode> queue = new LinkedList<>();
        queue.offer(root);
        while (!queue.isEmpty()) {
            int size = queue.size(); // 关键:记录当前层的节点数
            List<Integer> level = new ArrayList<>();
            for (int i = 0; i < size; i++) {
                TreeNode node = queue.poll();
                level.add(node.val);
                if (node.left != null) queue.offer(node.left);
                if (node.right != null) queue.offer(node.right);
            }
            res.add(level);
        }
        return res;
    }
}

💎 总结

遍历方式核心数据结构特点应用场景
前序遍历栈 (Stack)根节点最先被访问复制二叉树、求二叉树深度
中序遍历栈 (Stack)结果有序(对BST而言)验证二叉搜索树(BST)
后序遍历栈 (Stack)根节点最后被访问删除二叉树、计算树的高度
层序遍历队列 (Queue)按层处理,BFS思想求树的宽度、找最底层最左节点

注:递归法最直观,但迭代法更考验对数据结构的理解,且无栈溢出风险。


掌握了这四种遍历方式,就等于拿到了操作二叉树的第一把钥匙。下一站,我们将带着这些工具,去挑战力扣的一道经典题目:「从前序与中序遍历序列构造二叉树」(LC 105),看看如何利用遍历顺序反推出一棵树的结构。不见不散!🚀