news 2026/9/14 15:11:23

二叉树中序遍历:递归、迭代与Morris算法详解

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
二叉树中序遍历:递归、迭代与Morris算法详解

1. 二叉树中序遍历的核心概念

中序遍历(Inorder Traversal)是二叉树遍历中最基础也最重要的方式之一。它的遍历顺序遵循"左子树-根节点-右子树"的原则,这种遍历方式特别适合需要按照节点值大小顺序输出的场景。

在二叉搜索树(BST)中,中序遍历会按照从小到大的顺序访问所有节点。这是因为二叉搜索树的性质决定了左子节点的值小于根节点,而根节点的值又小于右子节点。通过中序遍历,我们可以高效地获取有序数据序列。

注意:中序遍历虽然概念简单,但在实际编码实现时,递归和非递归两种写法有着完全不同的思维模式,这也是面试中经常考察的重点。

2. 递归解法实现与原理分析

递归实现中序遍历是最直观的解法,它直接反映了中序遍历的定义。下面我们以Java语言为例,详细解析递归解法的实现细节:

class Solution { public List<Integer> inorderTraversal(TreeNode root) { List<Integer> res = new ArrayList<>(); inorder(root, res); return res; } private void inorder(TreeNode node, List<Integer> res) { if (node == null) return; inorder(node.left, res); // 递归遍历左子树 res.add(node.val); // 访问根节点 inorder(node.right, res); // 递归遍历右子树 } }

这段代码的时间复杂度是O(n),其中n是二叉树的节点数,因为每个节点都会被访问一次。空间复杂度在最坏情况下(二叉树退化为链表)也是O(n),主要是递归调用栈的开销。

递归解法的优势在于代码简洁明了,直接反映了算法逻辑。但它也存在明显的局限性:

  1. 当二叉树深度很大时(比如超过1000层),会导致栈溢出
  2. 递归调用会产生额外的函数调用开销
  3. 调试复杂的递归调用比较困难

3. 迭代解法与栈的应用

为了克服递归的缺点,我们可以使用迭代法配合栈来实现中序遍历。这种方法虽然代码稍复杂,但避免了递归的系统开销,也更适合处理深度很大的树。

class Solution { public List<Integer> inorderTraversal(TreeNode root) { List<Integer> res = new ArrayList<>(); Stack<TreeNode> stack = new Stack<>(); 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; } }

迭代解法的核心在于:

  1. 使用栈来模拟递归调用的系统栈
  2. 先尽可能地将左子节点压入栈中
  3. 弹出栈顶元素进行访问后,转向其右子树

这种解法的时间复杂度同样是O(n),空间复杂度在最坏情况下也是O(n),但实际使用的内存通常比递归解法更可控。

4. Morris遍历:O(1)空间复杂度的巧妙解法

Morris遍历是一种空间复杂度仅为O(1)的算法,它通过利用树中的空指针来实现遍历,不需要使用栈或递归。这种算法由Joseph Morris在1979年提出,非常巧妙但也较难理解。

class Solution { public List<Integer> inorderTraversal(TreeNode root) { List<Integer> res = new ArrayList<>(); TreeNode curr = root; while (curr != null) { if (curr.left == null) { res.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; // 断开线索 res.add(curr.val); curr = curr.right; } } } return res; } }

Morris遍历的核心思想是:

  1. 如果当前节点的左子节点为空,访问当前节点并转向右子节点
  2. 如果左子节点不为空,找到当前节点在中序遍历下的前驱节点
    • 如果前驱节点的右指针为空,将其指向当前节点(建立线索),然后转向左子节点
    • 如果前驱节点的右指针指向当前节点(说明已经建立过线索),断开线索,访问当前节点,然后转向右子节点

虽然Morris遍历的空间复杂度最优,但由于其实现复杂且会临时修改树的结构,在实际工程中并不常用,更多用于面试和算法竞赛中。

5. 不同解法的性能对比与适用场景

为了帮助开发者选择最合适的解法,我们对三种方法进行了详细对比:

解法类型时间复杂度空间复杂度代码复杂度适用场景
递归解法O(n)O(h)简单树深度不大,代码简洁优先
迭代解法O(n)O(h)中等通用场景,避免递归开销
Morris遍历O(n)O(1)复杂空间严格受限,允许修改树结构

在实际应用中:

  • 对于日常开发,递归解法在大多数情况下已经足够
  • 在处理深度未知或可能很大的树时,应优先考虑迭代解法
  • 只有在内存极其受限且允许修改树结构时,才考虑Morris遍历

6. 中序遍历的变种与应用场景

中序遍历不仅仅是简单的算法题,它在实际工程中有多种重要应用:

  1. 二叉搜索树验证:通过中序遍历检查结果是否有序
  2. 表达式树求值:中序遍历可以正确计算表达式树的值
  3. 序列化和反序列化:中序遍历序列结合其他遍历序列可以重建二叉树
  4. 范围查询:在BST中快速找到某个范围内的所有节点

例如,验证二叉搜索树的代码实现:

public boolean isValidBST(TreeNode root) { Stack<TreeNode> stack = new Stack<>(); TreeNode prev = null; TreeNode curr = root; while (curr != null || !stack.isEmpty()) { while (curr != null) { stack.push(curr); curr = curr.left; } curr = stack.pop(); if (prev != null && prev.val >= curr.val) { return false; } prev = curr; curr = curr.right; } return true; }

这个实现利用中序遍历的特性,只需要比较当前节点和前一个节点的值即可判断BST是否有效。

7. 常见错误与调试技巧

在实现中序遍历时,开发者常会遇到以下问题:

  1. 递归终止条件遗漏:忘记检查节点是否为null,导致无限递归
  2. 栈溢出:在深度很大的树上使用递归解法
  3. 指针丢失:在迭代解法中错误地移动指针导致遍历不完整
  4. 顺序错误:混淆了访问节点的顺序(如先访问根节点导致前序遍历)

调试技巧:

  • 对于递归解法,可以添加深度参数打印缩进,可视化递归过程
  • 对于迭代解法,可以在每次栈操作后打印栈内容,观察遍历路径
  • 使用小型测试用例(如3个节点的树)手动模拟算法执行过程

例如,调试版的递归实现:

private void inorder(TreeNode node, List<Integer> res, int depth) { if (node == null) { System.out.println(" ".repeat(depth*2) + "null"); return; } System.out.println(" ".repeat(depth*2) + "Enter: " + node.val); inorder(node.left, res, depth+1); System.out.println(" ".repeat(depth*2) + "Visit: " + node.val); res.add(node.val); inorder(node.right, res, depth+1); System.out.println(" ".repeat(depth*2) + "Exit: " + node.val); }

这种调试方法可以清晰展示递归的进入、访问和退出过程,帮助理解算法执行流程。

8. 扩展思考:中序遍历与其他遍历的关系

二叉树有三种基本遍历方式:前序、中序和后序。它们之间的关系和转换是面试中的高频考点。

  1. 前序+中序重建二叉树:前序遍历的第一个元素是根节点,中序遍历中根节点左侧是左子树,右侧是右子树
  2. 后序+中序重建二叉树:后序遍历的最后一个元素是根节点,同样可以利用中序遍历划分左右子树
  3. 层次遍历与中序结合:可以提供树的结构信息

例如,根据前序和中序遍历序列重建二叉树的代码:

public TreeNode buildTree(int[] preorder, int[] inorder) { Map<Integer, Integer> inMap = new HashMap<>(); for (int i = 0; i < inorder.length; i++) { inMap.put(inorder[i], i); } return build(preorder, 0, preorder.length-1, inorder, 0, inorder.length-1, inMap); } private TreeNode build(int[] preorder, int preStart, int preEnd, int[] inorder, int inStart, int inEnd, Map<Integer, Integer> inMap) { if (preStart > preEnd || inStart > inEnd) return null; TreeNode root = new TreeNode(preorder[preStart]); int inRoot = inMap.get(root.val); int numsLeft = inRoot - inStart; root.left = build(preorder, preStart+1, preStart+numsLeft, inorder, inStart, inRoot-1, inMap); root.right = build(preorder, preStart+numsLeft+1, preEnd, inorder, inRoot+1, inEnd, inMap); return root; }

这个实现利用哈希表快速定位中序遍历中的根节点位置,从而高效地划分左右子树,实现树的重建。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/14 15:10:00

Go2rtc 模块体系详解:协议、格式与编解码能力矩阵

Go2rtc 模块体系详解&#xff1a;协议、格式与编解码能力矩阵 【免费下载链接】go2rtc Ultimate camera streaming application 项目地址: https://gitcode.com/GitHub_Trending/go/go2rtc 本篇基于 go2rtc 仓库中的 internal/README.md 文档展开&#xff0c;系统讲解 g…

作者头像 李华
网站建设 2026/9/14 15:08:08

MediaPipe+Unity:手部面部关键点实时驱动虚拟角色

简介&#xff1a;基于Python与MediaPipe实现手部、面部实时识别&#xff0c;并驱动Unity端虚拟人物运动的完整项目源码&#xff0c;面向计算机相关专业正在筹备毕业设计、课程设计或期末大作业的学生&#xff0c;也适合希望从零上手视觉驱动Unity项目的实战学习者。项目经导师指…

作者头像 李华
网站建设 2026/9/14 15:07:25

Dozzle Agent 模式完全指南:用 TLS 加密连接远程 Docker 主机

Dozzle Agent 模式完全指南&#xff1a;用 TLS 加密连接远程 Docker 主机 【免费下载链接】dozzle Realtime log viewer for containers. Supports Docker, Swarm and K8s. 项目地址: https://gitcode.com/GitHub_Trending/do/dozzle Dozzle 的 Agent&#xff08;代理&…

作者头像 李华