当你面对一棵表达式树 (1 + 2) * 3 时,不同的遍历顺序会得到完全不同的结果:前序遍历产生前缀表达式 * + 1 2 3,中序遍历还原原始表达式 1 + 2 * 3(需要括号),后序遍历产生后缀表达式 1 2 + 3 *——这正是逆波兰表示法,可以直接用于栈式计算器。这就是二叉树遍历的核心价值:遍历顺序决定了数据的处理方式

二叉树作为最基础的树形数据结构,每个节点最多有两个子节点。这种简洁的结构却能表达层次关系、递归逻辑和决策过程。而遍历——按照某种规则访问每个节点恰好一次——是所有树操作的基石。无论是查找、插入、删除,还是序列化、反序列化,本质上都是遍历的变种。

为什么只有四种基本遍历

给定一棵二叉树,对于任意节点,有三个访问时机:进入节点时、遍历完左子树后、遍历完右子树后。这三个时机分别对应前序、中序、后序遍历。如果用 D 表示访问根节点,L 表示遍历左子树,R 表示遍历右子树,那么:

  • 前序遍历 (DLR):先访问根,再左子树,最后右子树
  • 中序遍历 (LDR):先左子树,再访问根,最后右子树
  • 后序遍历 (LRD):先左子树,再右子树,最后访问根

层序遍历则是另一种维度——横向扫描。它从根节点开始,逐层从左到右访问节点,本质上是广度优先搜索 (BFS)。

graph TB
    subgraph 示例二叉树
        A((1)) --> B((2))
        A --> C((3))
        B --> D((4))
        B --> E((5))
    end

对于上面这棵树,四种遍历的结果分别是:

  • 前序:1, 2, 4, 5, 3
  • 中序:4, 2, 5, 1, 3
  • 后序:4, 5, 2, 3, 1
  • 层序:1, 2, 3, 4, 5

这四种遍历各有其独特应用:

遍历方式 典型应用
前序遍历 复制二叉树、序列化二叉树、前缀表达式
中序遍历 BST有序输出、表达式树求值(中缀)
后序遍历 删除二叉树、计算目录大小、后缀表达式
层序遍历 按层处理、右视图、最大宽度

递归实现:最直观的表达

递归实现与遍历定义直接对应,代码几乎可以逐字翻译。以中序遍历为例:

public List<Integer> inorderTraversal(TreeNode root) {
    List<Integer> result = new ArrayList<>();
    inorder(root, result);
    return result;
}

private void inorder(TreeNode node, List<Integer> result) {
    if (node == null) return;
    inorder(node.left, result);   // L
    result.add(node.val);          // D
    inorder(node.right, result);   // R
}

前序和后序只需调整 result.add(node.val) 的位置。递归的优雅在于它完全反映了问题的递归定义:一棵二叉树的遍历 = 根节点的访问 + 左子树遍历 + 右子树遍历。

但递归的代价是空间复杂度 O(h),其中 h 是树高。对于平衡树这是 O(log n),对于退化为链表的树则是 O(n)。调用栈存储了从根到当前节点的完整路径,这正是递归"记住"如何返回的机制。

迭代实现:用显式栈模拟调用栈

递归的本质是编译器帮我们维护了一个隐式栈。迭代实现则显式地管理这个栈。以中序遍历为例,核心思想是:一直向左走到底,途中节点入栈;到底后弹出栈顶访问,然后转向右子树

public List<Integer> inorderTraversal(TreeNode root) {
    List<Integer> result = new ArrayList<>();
    Deque<TreeNode> stack = new ArrayDeque<>();
    TreeNode curr = root;
    
    while (curr != null || !stack.isEmpty()) {
        // 一直向左走
        while (curr != null) {
            stack.push(curr);
            curr = curr.left;
        }
        // 弹出栈顶访问
        curr = stack.pop();
        result.add(curr.val);
        // 转向右子树
        curr = curr.right;
    }
    return result;
}

前序遍历的迭代实现稍有不同——访问时机在入栈时:

public List<Integer> preorderTraversal(TreeNode root) {
    List<Integer> result = new ArrayList<>();
    if (root == null) return result;
    
    Deque<TreeNode> stack = new ArrayDeque<>();
    stack.push(root);
    
    while (!stack.isEmpty()) {
        TreeNode node = stack.pop();
        result.add(node.val);       // 先访问
        if (node.right != null) stack.push(node.right);  // 右子树先入栈
        if (node.left != null) stack.push(node.left);    // 左子树后入栈(先出)
    }
    return result;
}

后序遍历的迭代实现是最复杂的,因为需要"左-右-根"的顺序,而我们无法预先知道右子树是否处理完毕。常用技巧是反向后序遍历:先得到"根-右-左"的序列,再反转得到"左-右-根"。

public List<Integer> postorderTraversal(TreeNode root) {
    LinkedList<Integer> result = new LinkedList<>();
    if (root == null) return result;
    
    Deque<TreeNode> stack = new ArrayDeque<>();
    stack.push(root);
    
    while (!stack.isEmpty()) {
        TreeNode node = stack.pop();
        result.addFirst(node.val);    // 头插法实现反转
        if (node.left != null) stack.push(node.left);
        if (node.right != null) stack.push(node.right);
    }
    return result;
}

Morris遍历:O(1)空间的魔法

1979年,Joseph M. Morris 在论文《Traversing Binary Trees Simply and Cheaply》中提出了一种革命性的遍历方法。其核心思想来自线索二叉树 (Threaded Binary Tree)——利用叶子节点的空指针,临时指向中序遍历的前驱或后继节点。

Morris遍历的关键步骤:

  1. 如果当前节点没有左子树,访问它并转向右子树
  2. 如果当前节点有左子树,找到其左子树的最右节点(中序前驱)
  3. 如果前驱的右指针为空,将其指向当前节点(建立线索),转向左子树
  4. 如果前驱的右指针指向当前节点(线索已存在),断开线索,访问当前节点,转向右子树
public List<Integer> inorderTraversal(TreeNode root) {
    List<Integer> result = new ArrayList<>();
    TreeNode curr = root;
    
    while (curr != null) {
        if (curr.left == null) {
            // 没有左子树,直接访问
            result.add(curr.val);
            curr = curr.right;
        } else {
            // 找到中序前驱
            TreeNode predecessor = curr.left;
            while (predecessor.right != null && predecessor.right != curr) {
                predecessor = predecessor.right;
            }
            
            if (predecessor.right == null) {
                // 建立线索
                predecessor.right = curr;
                curr = curr.left;
            } else {
                // 线索已存在,断开并访问
                predecessor.right = null;
                result.add(curr.val);
                curr = curr.right;
            }
        }
    }
    return result;
}

Morris遍历的时间复杂度仍是 O(n),因为每条边最多被访问三次(向下、通过线索向上、断开线索)。但空间复杂度降到了 O(1),这在内存受限的嵌入式系统或处理超大规模树时非常有价值。

graph TB
    subgraph Morris遍历建立线索的过程
        direction LR
        A1((1)) -.->|建立线索| B1((2))
        B1((2)) --> C1((4))
        B1 --> D1((5))
        A1 --> E1((3))
        C1 -.->|建立线索| A1
        D1 -.->|建立线索| A1
    end

上图展示了Morris遍历中如何利用空指针建立临时线索(虚线),使得遍历完成后可以回到父节点,从而避免使用栈。

LeetCode题目分类详解

遍历基础题 (LeetCode 94, 144, 145)

这三道题分别要求实现中序、前序、后序遍历。上面已经给出了完整代码。面试时一个常见追问是:能否用统一的方式实现三种遍历?

答案是使用"标记法"——在每个节点入栈时附带一个标记,表示是否已访问过:

public List<Integer> inorderTraversal(TreeNode root) {
    List<Integer> result = new ArrayList<>();
    Deque<Object[]> stack = new ArrayDeque<>();
    
    if (root != null) stack.push(new Object[]{root, false});
    
    while (!stack.isEmpty()) {
        Object[] item = stack.pop();
        TreeNode node = (TreeNode) item[0];
        boolean visited = (boolean) item[1];
        
        if (visited) {
            result.add(node.val);
        } else {
            // 中序:左 -> 根 -> 右,入栈顺序相反
            if (node.right != null) stack.push(new Object[]{node.right, false});
            stack.push(new Object[]{node, true});
            if (node.left != null) stack.push(new Object[]{node.left, false});
        }
    }
    return result;
}

只需调整入栈顺序即可切换遍历方式。

层序遍历 (LeetCode 102, 103, 107)

层序遍历使用队列实现 BFS:

public List<List<Integer>> levelOrder(TreeNode root) {
    List<List<Integer>> result = new ArrayList<>();
    if (root == null) return result;
    
    Queue<TreeNode> queue = new LinkedList<>();
    queue.offer(root);
    
    while (!queue.isEmpty()) {
        int levelSize = queue.size();
        List<Integer> level = new ArrayList<>();
        
        for (int i = 0; i < levelSize; 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);
        }
        result.add(level);
    }
    return result;
}

LeetCode 103 锯齿形层序遍历只需在偶数层反转结果:

if (result.size() % 2 == 1) {
    Collections.reverse(level);
}

二叉树构造 (LeetCode 105, 106, 108, 109)

从前序+中序或后序+中序构造二叉树是经典的分治问题。核心原理是:

  • 前序遍历的第一个元素是根节点
  • 中序遍历中根节点将序列分为左右子树
  • 递归处理左右子树
// LeetCode 105: 从前序与中序遍历序列构造二叉树
private int preIndex = 0;
private Map<Integer, Integer> inorderMap = new HashMap<>();

public TreeNode buildTree(int[] preorder, int[] inorder) {
    for (int i = 0; i < inorder.length; i++) {
        inorderMap.put(inorder[i], i);
    }
    return build(preorder, 0, inorder.length - 1);
}

private TreeNode build(int[] preorder, int inStart, int inEnd) {
    if (inStart > inEnd) return null;
    
    int rootVal = preorder[preIndex++];
    TreeNode root = new TreeNode(rootVal);
    
    int inIndex = inorderMap.get(rootVal);
    root.left = build(preorder, inStart, inIndex - 1);
    root.right = build(preorder, inIndex + 1, inEnd);
    
    return root;
}

LeetCode 108 将有序数组转为平衡 BST,关键是选择中间元素作为根:

public TreeNode sortedArrayToBST(int[] nums) {
    return buildBST(nums, 0, nums.length - 1);
}

private TreeNode buildBST(int[] nums, int left, int right) {
    if (left > right) return null;
    int mid = left + (right - left) / 2;
    TreeNode root = new TreeNode(nums[mid]);
    root.left = buildBST(nums, left, mid - 1);
    root.right = buildBST(nums, mid + 1, right);
    return root;
}

二叉树修改 (LeetCode 226, 114)

LeetCode 226 翻转二叉树是一道经典的递归题:

public TreeNode invertTree(TreeNode root) {
    if (root == null) return null;
    
    TreeNode temp = root.left;
    root.left = root.right;
    root.right = temp;
    
    invertTree(root.left);
    invertTree(root.right);
    
    return root;
}

LeetCode 114 二叉树展开为链表要求原地修改:

public void flatten(TreeNode root) {
    TreeNode curr = root;
    while (curr != null) {
        if (curr.left != null) {
            TreeNode predecessor = curr.left;
            while (predecessor.right != null) {
                predecessor = predecessor.right;
            }
            predecessor.right = curr.right;
            curr.right = curr.left;
            curr.left = null;
        }
        curr = curr.right;
    }
}

路径问题 (LeetCode 112, 113, 124, 257)

LeetCode 112 路径总和检查是否存在根到叶路径等于目标和:

public boolean hasPathSum(TreeNode root, int targetSum) {
    if (root == null) return false;
    if (root.left == null && root.right == null) {
        return root.val == targetSum;
    }
    return hasPathSum(root.left, targetSum - root.val) 
        || hasPathSum(root.right, targetSum - root.val);
}

LeetCode 124 二叉树最大路径和是一道困难题,关键是理解"路径"的定义:

private int maxSum = Integer.MIN_VALUE;

public int maxPathSum(TreeNode root) {
    maxGain(root);
    return maxSum;
}

private int maxGain(TreeNode node) {
    if (node == null) return 0;
    
    int leftGain = Math.max(maxGain(node.left), 0);
    int rightGain = Math.max(maxGain(node.right), 0);
    
    // 以当前节点为根的路径和
    int newPathSum = node.val + leftGain + rightGain;
    maxSum = Math.max(maxSum, newPathSum);
    
    // 返回以当前节点为端点的最大路径和
    return node.val + Math.max(leftGain, rightGain);
}

属性判断 (LeetCode 101, 104, 110)

LeetCode 101 对称二叉树需要同时递归两棵子树:

public boolean isSymmetric(TreeNode root) {
    return isMirror(root, root);
}

private boolean isMirror(TreeNode t1, TreeNode t2) {
    if (t1 == null && t2 == null) return true;
    if (t1 == null || t2 == null) return false;
    return t1.val == t2.val 
        && isMirror(t1.left, t2.right) 
        && isMirror(t1.right, t2.left);
}

LeetCode 110 平衡二叉树需要在计算高度的同时判断平衡:

public boolean isBalanced(TreeNode root) {
    return height(root) != -1;
}

private int height(TreeNode node) {
    if (node == null) return 0;
    
    int leftHeight = height(node.left);
    if (leftHeight == -1) return -1;
    
    int rightHeight = height(node.right);
    if (rightHeight == -1) return -1;
    
    if (Math.abs(leftHeight - rightHeight) > 1) return -1;
    
    return Math.max(leftHeight, rightHeight) + 1;
}

最近公共祖先 (LeetCode 235, 236)

LeetCode 236 二叉树的最近公共祖先是高频面试题:

public TreeNode lowestCommonAncestor(TreeNode root, TreeNode p, TreeNode q) {
    if (root == null || root == p || root == q) return root;
    
    TreeNode left = lowestCommonAncestor(root.left, p, q);
    TreeNode right = lowestCommonAncestor(root.right, p, q);
    
    if (left != null && right != null) return root;
    return left != null ? left : right;
}

对于 BST (LeetCode 235),可以利用 BST 的性质简化:

public TreeNode lowestCommonAncestor(TreeNode root, TreeNode p, TreeNode q) {
    if (p.val < root.val && q.val < root.val) {
        return lowestCommonAncestor(root.left, p, q);
    }
    if (p.val > root.val && q.val > root.val) {
        return lowestCommonAncestor(root.right, p, q);
    }
    return root;
}

视图问题 (LeetCode 199)

LeetCode 199 二叉树右视图返回从右侧能看到的所有节点:

public List<Integer> rightSideView(TreeNode root) {
    List<Integer> result = new ArrayList<>();
    if (root == null) return result;
    
    Queue<TreeNode> queue = new LinkedList<>();
    queue.offer(root);
    
    while (!queue.isEmpty()) {
        int levelSize = queue.size();
        for (int i = 0; i < levelSize; i++) {
            TreeNode node = queue.poll();
            if (i == levelSize - 1) {
                result.add(node.val);
            }
            if (node.left != null) queue.offer(node.left);
            if (node.right != null) queue.offer(node.right);
        }
    }
    return result;
}

解题模式总结

解决二叉树问题,本质上就是选择合适的遍历方式:

  1. 需要先处理子节点再处理父节点 → 后序遍历(如计算树高、求和、LCA)
  2. 需要先处理父节点再处理子节点 → 前序遍历(如复制树、路径问题)
  3. 需要按层级处理 → 层序遍历(如右视图、按层统计)
  4. BST 需要有序访问 → 中序遍历

递归是最自然的思路,但迭代和 Morris 遍历展示了如何用显式数据结构替代隐式调用栈。理解这三种实现方式的等价性,是掌握树形数据结构的关键一步。

参考文献