news 2026/10/26 22:38:41

Morris遍历算法:O(1)空间复杂度的二叉树遍历

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Morris遍历算法:O(1)空间复杂度的二叉树遍历

1. Morris遍历算法核心原理剖析

Morris遍历是一种巧妙的二叉树遍历算法,其最大特点是在不使用递归或显式栈的情况下实现O(1)空间复杂度的中序遍历。这个算法由Joseph Morris在1979年提出,特别适合内存受限场景下的树结构处理。

算法核心在于利用叶子节点的空指针来临时存储回溯信息。具体来说,对于当前节点curr:

  1. 如果curr无左子树,直接访问curr并转向右子树
  2. 如果curr有左子树,找到其左子树的最右节点pred(即中序遍历的前驱节点)
    • 若pred的右指针为空,将其指向curr(建立临时链接),然后curr向左移动
    • 若pred的右指针指向curr(说明已建立过链接),则断开该链接,访问curr,然后curr向右移动

这种"借道"前驱节点右指针的方式,使得算法可以在遍历完左子树后顺利返回到父节点,而无需额外的存储空间。

2. Morris中序遍历标准实现

以下是Java语言的经典实现模板:

public List<Integer> morrisInorder(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 pred = findPredecessor(curr); if (pred.right == null) { pred.right = curr; // 建立临时链接 curr = curr.left; } else { pred.right = null; // 恢复树结构 res.add(curr.val); curr = curr.right; } } } return res; } private TreeNode findPredecessor(TreeNode node) { TreeNode pred = node.left; while (pred.right != null && pred.right != node) { pred = pred.right; } return pred; }

关键点说明:

  1. 时间复杂度为O(n):虽然每个前驱节点会被访问两次,但总操作次数仍在O(n)量级
  2. 空间复杂度O(1):仅使用固定数量的指针变量
  3. 算法会修改树结构(临时改变右指针),但在结束后会恢复原状

3. 前序与后序遍历的Morris实现

3.1 Morris前序遍历

仅需调整节点访问时机,在第一次遇到节点时立即访问:

public List<Integer> morrisPreorder(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 pred = findPredecessor(curr); if (pred.right == null) { res.add(curr.val); // 唯一修改处 pred.right = curr; curr = curr.left; } else { pred.right = null; curr = curr.right; } } } return res; }

3.2 Morris后序遍历

后序遍历的实现较为复杂,需要反转部分节点路径:

public List<Integer> morrisPostorder(TreeNode root) { List<Integer> res = new ArrayList<>(); TreeNode dummy = new TreeNode(0); dummy.left = root; TreeNode curr = dummy; while (curr != null) { if (curr.left == null) { curr = curr.right; } else { TreeNode pred = findPredecessor(curr); if (pred.right == null) { pred.right = curr; curr = curr.left; } else { pred.right = null; addPath(curr.left, res); // 关键差异点 curr = curr.right; } } } return res; } private void addPath(TreeNode node, List<Integer> res) { int count = 0; // 反转右子节点路径 TreeNode prev = null, curr = node; while (curr != null) { TreeNode next = curr.right; curr.right = prev; prev = curr; curr = next; count++; } // 遍历并访问节点 curr = prev; while (count-- > 0) { res.add(curr.val); curr = curr.right; } // 恢复路径 curr = prev; prev = null; while (curr != null) { TreeNode next = curr.right; curr.right = prev; prev = curr; curr = next; } }

4. 高频面试题精讲

4.1 验证二叉搜索树

题目:使用Morris遍历验证二叉树是否为BST。

解决方案:

public boolean isValidBST(TreeNode root) { TreeNode curr = root; Integer prev = null; while (curr != null) { if (curr.left == null) { if (prev != null && curr.val <= prev) return false; prev = curr.val; curr = curr.right; } else { TreeNode pred = findPredecessor(curr); if (pred.right == null) { pred.right = curr; curr = curr.left; } else { pred.right = null; if (prev != null && curr.val <= prev) return false; prev = curr.val; curr = curr.right; } } } return true; }

4.2 二叉树中第K小的元素

题目:找出BST中第K小的元素(进阶要求:使用Morris遍历)

解决方案:

public int kthSmallest(TreeNode root, int k) { TreeNode curr = root; int count = 0; while (curr != null) { if (curr.left == null) { if (++count == k) return curr.val; curr = curr.right; } else { TreeNode pred = findPredecessor(curr); if (pred.right == null) { pred.right = curr; curr = curr.left; } else { pred.right = null; if (++count == k) return curr.val; curr = curr.right; } } } return -1; // 根据题意k有效,此处不会执行 }

4.3 二叉树展开为链表

题目:将二叉树原地展开为单链表(Morris遍历解法)

解决方案:

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

5. 算法复杂度与适用场景分析

5.1 时间复杂度对比

算法类型平均时间复杂度最坏时间复杂度空间复杂度
递归遍历O(n)O(n)O(h)
迭代遍历O(n)O(n)O(h)
Morris遍历O(n)O(n)O(1)

其中h表示树的高度,n表示节点数量。虽然Morris遍历的时间常数较大,但在空间敏感场景优势明显。

5.2 适用场景

  1. 内存受限环境:嵌入式系统、低功耗设备等
  2. 巨型树结构处理:当树的高度极大时避免栈溢出
  3. 只读场景:注意算法会临时修改树结构
  4. 面试考察:展示对遍历本质的理解

5.3 限制与注意事项

  1. 线程不安全:多线程环境下可能引发问题
  2. 破坏性操作:虽然会恢复,但遍历过程中树结构被修改
  3. 调试困难:指针操作复杂,不易跟踪状态
  4. 不适用于需要多次遍历的场景:重复建立临时链接影响效率

6. 面试实战技巧

6.1 白板编码要点

  1. 先说明算法思想再写代码
  2. 重点标注指针操作部分
  3. 画出遍历过程中的树结构变化
  4. 明确区分三种遍历方式的代码差异

6.2 常见follow-up问题

  1. 如何修改算法实现反向遍历?
  2. 如何处理带有父指针的树结构?
  3. 在并发环境下如何保证线程安全?
  4. 如何扩展算法处理n叉树?

6.3 性能优化思路

  1. 提前终止:在搜索类问题中满足条件后立即退出
  2. 并行处理:对左右子树采用并行遍历(需解决同步问题)
  3. 批量处理:对访问节点进行批量化操作

7. 实际工程应用案例

7.1 数据库索引遍历

某些嵌入式数据库使用Morris遍历进行索引扫描,特别是在内存有限的设备上。例如在SQLite的某些实现中,对B-Tree索引的遍历就采用了类似思想。

7.2 垃圾回收标记

在标记-清除垃圾回收算法中,Morris遍历可用于标记阶段,减少内存使用。这对于JVM等运行时环境的内存管理有重要意义。

7.3 文件系统遍历

某些嵌入式文件系统在遍历目录结构时采用这种技术,避免递归导致的栈溢出问题,特别是在深度嵌套的目录结构中表现优异。

8. 变种与扩展算法

8.1 线索二叉树

Morris遍历与线索二叉树(Threaded Binary Tree)有相似思想,但线索二叉树是永久性修改树结构,而Morris遍历是临时性的。

8.2 双向Morris遍历

通过同时维护前驱和后继指针,可以实现双向遍历,但实现复杂度显著增加。

8.3 并行化改造

通过精细的同步控制,可以使Morris遍历的左右子树处理并行化,但需要解决指针冲突问题。

9. 调试与验证技巧

9.1 可视化调试方法

  1. 打印遍历路径:在每个节点访问时打印状态
  2. 图形化展示:使用Graphviz等工具生成遍历过程动画
  3. 断言检查:验证树结构是否被正确恢复

9.2 边界测试用例

  1. 空树
  2. 单节点树
  3. 完全左斜树/完全右斜树
  4. 大规模随机树

9.3 与递归遍历对比验证

// 验证Morris遍历结果是否正确 List<Integer> morrisResult = morrisInorder(root); List<Integer> recursiveResult = recursiveInorder(root); assert morrisResult.equals(recursiveResult);

10. 经典面试题解答

10.1 二叉树直径问题

题目:求二叉树直径(任意两节点间的最长路径)

Morris遍历解法:

public int diameterOfBinaryTree(TreeNode root) { int diameter = 0; TreeNode curr = root; while (curr != null) { if (curr.left != null) { TreeNode pred = findPredecessor(curr); if (pred.right == null) { pred.right = curr; curr = curr.left; } else { pred.right = null; // 计算左侧深度 int leftDepth = getDepth(curr.left); // 计算右侧深度 int rightDepth = getDepth(curr.right); diameter = Math.max(diameter, leftDepth + rightDepth); curr = curr.right; } } else { curr = curr.right; } } return diameter; } private int getDepth(TreeNode node) { int depth = 0; while (node != null) { depth++; node = node.right; // 注意这里使用右子树计算 } return depth; }

10.2 二叉树最大路径和

题目:求二叉树中任意路径的最大和(路径可以不经过根节点)

Morris遍历解法:

public int maxPathSum(TreeNode root) { int maxSum = Integer.MIN_VALUE; TreeNode curr = root; while (curr != null) { if (curr.left != null) { TreeNode pred = findPredecessor(curr); if (pred.right == null) { pred.right = curr; curr = curr.left; } else { pred.right = null; // 计算当前节点为根的路径和 int[] left = computeMaxSinglePath(curr.left); int[] right = computeMaxSinglePath(curr.right); maxSum = Math.max(maxSum, Math.max(left[0], right[0])); maxSum = Math.max(maxSum, left[1] + curr.val + right[1]); curr = curr.right; } } else { curr = curr.right; } } return maxSum; } // 返回[单边最大路径和,包含当前节点的单边最大路径和] private int[] computeMaxSinglePath(TreeNode node) { // 实现省略,可用类似后序遍历的方式计算 }

10.3 最近公共祖先(LCA)

题目:找到二叉树中两个节点的最近公共祖先

Morris遍历解法:

public TreeNode lowestCommonAncestor(TreeNode root, TreeNode p, TreeNode q) { TreeNode ancestor = null; TreeNode curr = root; while (curr != null) { if (curr.left == null) { if (curr == p || curr == q) { if (ancestor == null) ancestor = curr; else return ancestor; } curr = curr.right; } else { TreeNode pred = findPredecessor(curr); if (pred.right == null) { pred.right = curr; curr = curr.left; } else { pred.right = null; if (curr == p || curr == q) { if (ancestor == null) ancestor = curr; else return ancestor; } curr = curr.right; } } } return ancestor; }
版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/26 22:38:36

claude-mem 记忆层设计:从存储、检索到更新的工程实践

1. 从零认识 claude-mem&#xff1a;它到底解决什么问题第一次看到claude-mem这个名字&#xff0c;我脑子里蹦出来的第一个念头是&#xff1a;终于有人把「记忆」这件事单独拎出来做了。做过一段时间 AI 应用开发的人都知道&#xff0c;大模型本身是无状态的&#xff0c;每次对…

作者头像 李华
网站建设 2026/10/26 22:37:06

元宇宙办公室:分布式团队的沉浸式协作未来

1. 元宇宙办公室&#xff1a;分布式团队的终极协作形态当GitHub宣布全员远程办公时&#xff0c;整个科技圈都在讨论分布式团队的未来。三年后的今天&#xff0c;我们突然发现&#xff1a;Zoom会议和Slack消息组成的"数字流水线"正在消耗创意工作者的灵魂。每周40小时…

作者头像 李华
网站建设 2026/10/26 22:36:43

Agent-Reach:统一CLI聚合层,打通Python与Rust多Agent管理

1. 项目缘起与核心定位Agent-Reach 这个名字第一次出现在我视野里的时候&#xff0c;我正被一堆零散的 AI Agent 工具链折腾得够呛。那段时间我在同时维护三套不同架构的 Agent 项目&#xff0c;一套基于 Python 的轻量级任务编排&#xff0c;一套用 Rust 写的高并发消息处理&a…

作者头像 李华
网站建设 2026/10/26 22:34:52

Agent-Reach:为AI智能体打造可靠的触达层与协作网络

“Agent-Reach"这名字是我做内部项目时临时起的&#xff0c;后来发现它意外地贴切——因为我做的这套东西&#xff0c;核心就是解决一个问题&#xff1a;让AI智能体能真正"够得着"它需要的工具、数据和协作者。今年大伙都在卷多Agent框架&#xff0c;各家都在吹…

作者头像 李华
网站建设 2026/10/26 22:34:21

e2e 端到端测试实战:从 profile1 配置到核心路径编排

1. 从“e2e”这个标题说起&#xff1a;它到底指什么第一次看到“e2e”这三个字符&#xff0c;很多人会愣一下。它不像“Docker”“Kubernetes”那样一眼能看出领域&#xff0c;也不像“React”“Vue”那样自带技术栈标签。但如果你在软件工程、测试、通信或者产品交付这些圈子里…

作者头像 李华
网站建设 2026/10/26 22:32:36

claude-mem 深度解析:为 Claude 构建持久化记忆系统的工程实践

1. 项目缘起与核心定位第一次看到claude-mem这个名字&#xff0c;我的直觉是&#xff1a;这应该是一个给 Claude 系列模型做“记忆层”的项目。事实也确实如此。它要解决的是一个所有长期跟大模型打交道的人都会遇到的痛点——模型没有持久记忆。每次开新会话&#xff0c;之前聊…

作者头像 李华