二叉树先序、中序和后序遍历的Java实现(包含递归、迭代和Morris解法)

本文详细探讨了二叉树的三种遍历方法:递归、迭代实现以及Morris遍历。通过实例展示了如何用递归、栈辅助的迭代方法,以及利用空闲指针的Morris算法分别完成前序、中序和后序遍历。空间效率和时间复杂度都被深入剖析。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

二叉树的遍历

0 相关资源

  1. Leetcode题目链接:二叉树的前序遍历中序遍历后序遍历
  2. 源代码:包含二叉树三种遍历方式三种解法GitHub仓库

1 递归解法

三种遍历方式的递归解法是最容易想到的解决方案,主要采用DFS深度优先遍历的思想,在递归函数中进行先左子树后右子树的深度遍历,唯一的不同就是当前节点的输出位置,代码如下。

时空复杂度均为O(n)。

private List<Integer> res = new ArrayList<>();

public List<Integer> Traversal_Recursion(TreeNode root) {
    dfs(root);
    return res;
}

private void dfs(TreeNode node) {
    if (node == null) {
        return;
    }
    /*
	 * 先序遍历:先根节点输出,再左子树递归,最后右子树递归。
	 */
	res.add(node.val);
    dfs(node.left);
    dfs(node.right);
    /*
	 * 中序遍历:先左子树递归,再根节点输出,最后右子树递归。
	 * dfs(node.left);
	 * res.add(node.val);
     * dfs(node.right);
	 */
    /*
	 * 后序遍历:先左子树递归,再右子树递归,最后根节点输出。
	 * dfs(node.left);
     * dfs(node.right);
     * res.add(node.val);
	 */
}

2 迭代解法

迭代解法主要依靠栈stack这种数据结构的帮助,最终节点的出栈顺序即为相应的遍历顺序(后序遍历略有不同),下面针对每种遍历方式进行详细说明。

时空复杂度也均为O(n)。

  1. 先序遍历:先序遍历先将根节点入栈,如果栈不为空,每次循环从栈中取出一个节点并输出,再将该节点的左右子节点(如果存在的话)放入栈中,这里需要注意的是先将右子节点入栈,再将左子节点入栈,从而保证先序遍历根→左→右的遍历顺序。
public List<Integer> preorderTraversal_Iteration(TreeNode root) {
    if (root == null) {
        return res;
    }
    Deque<TreeNode> stack = new LinkedList<>();
    stack.addFirst(root);
    while (!stack.isEmpty()) {
        TreeNode node = stack.removeFirst();
        res.add(node.val);
        //注意这里要先加右子节点,再加左子节点,这样出栈的时候才是先左后右
        if (node.right != null) {
            stack.addFirst(node.right);
        }
        if (node.left != null) {
            stack.addFirst(node.left);
        }
    }
    return res;
}
  1. 中序遍历:中序遍历每次循环都需要先找到当前root节点的最左子节点,期间需要将寻找路径中的节点依次放入栈中,找到后直接输出该最左子节点即可(它没有左子节点了),然后转到右子节点进行下一轮循环。但是每轮循环也存在一开始root直接为null的情况,这意味着上轮循环中的root节点的右子节点为null,此时只需要从栈中再取出一个节点加入到答案中即可,因为取出的节点的左子树已遍历完毕,然后同理转到当前节点的右子节点进行下一轮循环。
public List<Integer> inorderTraversal_Iteration(TreeNode root) {
    Deque<TreeNode> stack = new LinkedList<>();
    while (root != null || !stack.isEmpty()) {
        //先一股脑找到该节点的最左子节点
        while (root != null) {
            stack.addFirst(root);//期间将路径中的节点压入栈中
            root = root.left;
        }
        //以上while循环也存在root直接为null的情况,代表上轮循环的root节点的右子节点为null
        //这时从栈中再取出一个节点加入到答案中即可,取出的节点的左子树已遍历完毕
        root = stack.removeFirst();
        res.add(root.val);
        root = root.right;//转到右子节点
    }
    return res;
}
  1. 后序遍历:后序遍历一开始和中序遍历类似,每次循环都需要先找到当前root节点的最左子节点,期间将寻找路径中的节点依次放入栈中,但是当找到最左子节点后,不能立刻输出答案,需要先保证找到的最左子节点的右子树为null或已遍历时才可以输出答案,否则就要转到右子树去遍历(当前节点放回栈中),等待右子树遍历后,再去输出当前节点。同理每轮循环也存在一开始root直接为null的情况,这意味着上轮循环中的root节点的右子节点为null或已遍历,此时当前节点就能加入到答案中了。
public List<Integer> postorderTraversal_Iteration(TreeNode root) {
    Deque<TreeNode> stack = new LinkedList<>();
    TreeNode prev = null;
    while (root != null || !stack.isEmpty()) {
        //root==null时不进行while循环:来自下面的if,上一轮右子树为空或是右子树已遍历
        while (root != null) {//一直遍历到该节点的最左子节点的左子节点(null)
            stack.addFirst(root);
            root = root.left;
        }
        /*
         * 这时出栈的元素有两种情况:
         * 1.一直遍历到的最左子节点;
         * 2.新弹出一个已存储的节点;
         */
        root = stack.removeFirst();
        if (root.right == null || root.right == prev) {//右子树为空或是右子树已遍历
            res.add(root.val);//加入该节点
            prev = root;//标记该节点已遍历
            root = null;//root置为null,下一轮可以从栈中弹出新节点
        } else {
            stack.addFirst(root);//因为右子树存在,把弹出的节点再放回去
            root = root.right;//转到右子节点
        }
    }
    return res;
}

3 Morris解法

有一种巧妙的方法可以在线性时间内,只占用常数空间来实现前序、中序和后续遍历。这种方法由J.H.Morris在1979年的论文《Traversing Binary Trees Simply and Cheaply》中首次提出,因此被称为Morris遍历。

时间复杂度O(n),空间复杂度O(1)。

Morris遍历的核心思想是利用树的大量空闲指针,实现空间开销的极限缩减,其遍历规则总结如下:

  1. 新建临时节点,令该节点为root;
  2. 如果当前节点的左子节点为空,遍历当前节点的右子节点;
  3. 如果当前节点的左子节点不为空,在当前节点的左子树中找到最右节点作为当前节点的前驱节点
  4. 如果前驱节点的右子节点为空,将前驱节点的右子节点设置为当前节点,当前节点更新为当前节点的左子节点;如果前驱节点的右子节点为当前节点,将它的右子节点重新设为空,当前节点更新为当前节点的右子节点;
  5. 重复步骤2和步骤3(4),直到遍历结束。

如下图所示,给出了一个Morris遍历的例子:
在这里插入图片描述
在Morris遍历的过程中,在合适的位置输出节点的值即可完成二叉树的前序、中序和后序遍历:

  • 前序遍历:在每次循环中,以下两种情况成立时输出当前节点即是前序遍历顺序。
    1. 如果当前节点的左子树为空;
    2. 如果当前节点的左子树不为空,且左子树的最右节点的右指针为空(第一次遍历到);
  • 中序遍历:在每次循环中,以下两种情况成立时输出当前节点即是中序遍历顺序。
    1. 如果当前节点的左子树为空;
    2. 如果当前节点的左子树不为空,且左子树的最右节点的右指针指向当前节点(第二次遍历到);
  • 后序遍历:在每次循环中,如果当前节点的左子树不为空,且左子树的最右节点的右指针指向当前节点(第二次遍历到)时,将当前节点的左子节点(+以该左子节点为根节点一直延伸的右子节点路径)反序后添加到答案中,最后不要忘记在整个循环结束后,在对总的根节点也进行类似的操作后即为后序遍历顺序。

三种遍历方式的Morris实现参见文章开头给出的GitHub仓库。

### 力扣二叉树遍历Java实现思路 #### 方法一:递归解法 递归是一种直观且易于理解的方法。通过定义一个辅助函数 `dfs`,按照 **左子树 -> 根节点 -> 右子树** 的顺依次处理每个节点。对于每一个节点,如果它不为空,则递归遍历其左子树,接着将该节点的值加入结果列表,最后递归遍历右子树。 以下是具体的代码实现: ```java class Solution { public List<Integer> inorderTraversal(TreeNode root) { List<Integer> res = new ArrayList<>(); dfs(res, root); return res; } private void dfs(List<Integer> res, TreeNode node) { if (node == null) { return; } dfs(res, node.left); // 递归遍历左子树 res.add(node.val); // 访问根节点 dfs(res, node.right); // 递归遍历右子树 } } ``` 这种方法的时间复杂度空间复杂度均为 O(n),其中 n 是二叉树中的节点数[^3]。 --- #### 方法二:迭代解法 迭代方法的核心思想是手动模拟递归过程中使用的栈结构。由于递归本质上也是利用系统栈完成操作,因此可以通过显式的栈来替代系统的隐式调用栈。 具体步骤如下: 1. 初始化一个空的结果列表 `res` 一个栈 `stack`。 2. 使用一个指针变量 `current` 指向当前正在访问的节点。 3. 当前节点不为空或栈不为空时进入循环: - 如果当前节点存在,将其压入栈并继续移动到它的左子节点; - 否则弹出栈顶元素(即最近一次未完全访问的节点),记录其值,并转向其右子节点。 4. 循环结束后返回结果列表。 下面是对应的代码实现: ```java class Solution { public List<Integer> inorderTraversal(TreeNode root) { List<Integer> res = new ArrayList<>(); Deque<TreeNode> stack = new LinkedList<>(); TreeNode current = root; while (current != null || !stack.isEmpty()) { while (current != null) { // 将所有左子节点压入栈 stack.push(current); current = current.left; } current = stack.pop(); // 弹出栈顶元素 res.add(current.val); // 添加到结果集 current = current.right; // 移动至右子节点 } return res; } } ``` 此方法同样具备时间复杂度 O(n)[^2] 最坏情况下空间复杂度也为 O(n) 的特性。 --- #### 方法三:Morris 遍历 Morris 遍历是一种不需要额外存储空间的高效算法,其核心思想是在遍历时修改树的结构以建立临时链接,从而避免使用栈或递归来保存中间状态。最终恢复原始树形结构的同时完成了遍历。 主要逻辑分为以下几个部分: 1. 初始设置当前节点为根节点。 2. 对于每个节点,判断是否有左子树: - 若无左子树,则直接访问该节点并将指针移向右子树; - 若有左子树,则找到左子树中最右侧的节点(即前驱节点)并与当前节点连接起来;随后再次调整指针位置。 3. 继续上述过程直至整棵树被完整遍历完毕。 以下是 Morris 遍历的具体实现: ```java class Solution { public List<Integer> inorderTraversal(TreeNode root) { List<Integer> res = new ArrayList<>(); TreeNode current = root; while (current != null) { if (current.left == null) { // 左子树不存在 res.add(current.val); // 直接访问当前节点 current = current.right; // 转移到右子树 } else { // 存在左子树 TreeNode predecessor = findPredecessor(current); if (predecessor.right == null) { // 前驱尚未连接回当前节点 predecessor.right = current; // 创建反向链路 current = current.left; // 进入左子树 } else { // 前驱已连接回来 predecessor.right = null; // 断开反向链路 res.add(current.val); // 访问当前节点 current = current.right; // 转移到右子树 } } } return res; } private TreeNode findPredecessor(TreeNode node) { TreeNode pred = node.left; while (pred.right != null && pred.right != node) { pred = pred.right; } return pred; } } ``` 这种做法的空间复杂度仅为 O(1)[^4],因为除了输入数据外无需任何附加内存支持。 --- ### 总结 以上介绍了三种不同的 Java 实现方案用于解决力扣上的二叉树遍历问题。每种方法各有优劣,在实际应用中可以根据需求灵活选用合适的策略。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值