跳到主要内容
极客日志极客日志面向AI+效率的开发者社区
首页博客GitHub 精选镜像工具UI配色美学隐私政策关于联系
搜索内容 / 工具 / 仓库 / 镜像...⌘K搜索
注册
博客列表
C++算法

数据结构与算法:Morris 遍历

综述由AI生成Morris 遍历是一种空间复杂度为 O(1) 的二叉树遍历算法,通过临时修改节点指针建立线索。文章详细阐述了其核心原理,包括前序、中序和后序遍历的实现逻辑,并提供了在验证二叉搜索树、计算最小深度及查找最近公共祖先等经典问题中的具体应用代码。该算法时间复杂度为 O(n),适用于对内存敏感的场景。

Kubernet发布于 2026/3/27更新于 2026/5/2424 浏览
数据结构与算法:Morris 遍历

一、原理

1.内容

(1)核心

没有左子树的节点只到达一次,有左子树的节点会到达两次,其中用左子树最右节点的右指针状态来标记第几次到达。

(2)过程

首先,开始时 cur 在头节点,cur 为空时整个过程停止。 若 cur 没有左孩子,就向右移动。 若 cur 有左孩子,就去找 cur 左子树的最右节点 mostRight。若 mostRight 的右指针为空,那么就让其指向 cur,然后 cur 向左移动。若 mostRight 右指针指向 cur,就让其指向空,然后 cur 向右移动。

文章配图

首先 cur 肯定先来到 a 节点。之后,因为 a 节点有左孩子,那么就去 a 节点左子树的最右节点,所以就是 d 节点。因为此时 d 节点的右指针为空,所以让其指向 a 节点,然后 cur 来到 b 节点。 之后,因为 b 节点有左孩子,所以还是去找左子树的最右节点,那么就是 c 节点。同样因为此时 c 节点右指针为空,所以让其指向 b 节点,然后 cur 来到 c 节点。 当 cur 来到 c 节点时,因为没有左孩子,那么就向右移动,所以 cur 回到 b 节点。此时再去找左子树的最右节点,可以发现 c 节点的右指针指向自己,所以 c 节点的右指针指向空,然后 cur 往右去 d 节点。 之后因为没有左孩子,所以往右回到 a 节点,同样去找左子树的最右节点,发现 d 节点的右指针指回来了,所以将其指为空,cur 往右来到 e 节点。 之后就是把 f 节点的右指针指向 e,然后来到 f,之后回到 e,把 f 的右指针指回空,然后去 g,完成遍历。观察整个过程,不难发现时间复杂度就是 O(n) 的,空间复杂度 O(1)。

2.二叉树的前序遍历

class Solution { 
public: 
    vector<int> preorderTraversal(TreeNode* root) { 
        vector<int>ans; 
        return morrisPreorder(root,ans); 
    } 
    vector<int>morrisPreorder(TreeNode* head,vector<int>&ans) { 
        TreeNode* cur=head; 
        TreeNode* mostRight=NULL; 
        while(cur!=NULL) { 
            mostRight=cur->left; 
            if(mostRight!=NULL) { 
                while(mostRight->right!=NULL&&mostRight->right!=cur) { 
                    mostRight=mostRight->right; 
                } 
                if(mostRight->right==NULL) { 
                    ans.push_back(cur->val); 
                    mostRight->right=cur; 
                    cur=cur->left; 
                    continue; 
                } 
                else { 
                    mostRight->right=NULL; 
                } 
            } 
            else { 
                ans.push_back(cur->val); 
            } 
            cur=cur->right; 
        } 
        return ans; 
    } 
};

先序遍历就是在 morris 遍历的过程中,只在第一次来到时打印该节点,第二次来到时不打印。而观察 morris 遍历的特点,当最右节点的右指针是 cur,那么就说明此时是第二次来到这个节点,所以就不打印。

3.二叉树的中序遍历

class Solution { 
public: 
    vector<int> inorderTraversal(TreeNode* root) { 
        vector<int>ans; 
        return morrisInorder(root,ans); 
    } 
    vector<int>morrisInorder(TreeNode* head,vector<int>&ans) { 
        TreeNode* cur=head; 
        TreeNode* mostRight=NULL; 
        while(cur!=NULL) { 
            mostRight=cur->left; 
            if(mostRight!=NULL) { 
                while(mostRight->right!=NULL&&mostRight->right!=cur) { 
                    mostRight=mostRight->right; 
                } 
                if(mostRight->right==NULL) { 
                    mostRight->right=cur; 
                    cur=cur->left; 
                    continue; 
                } 
                else { 
                    ans.push_back(cur->val); 
                    mostRight->right=NULL; 
                } 
            } 
            else { 
                ans.push_back(cur->val); 
            } 
            cur=cur->right; 
        } 
        return ans; 
    } 
};

中序遍历就是在 morris 遍历的过程中,若只来一次,那么就直接打印,否则就在第二次来的时候再打印。根据 morris 遍历,有左子树的节点会来两遍,所以如果没有就直接打印,有的话就在最右节点右指针指向自己时输出。

4.二叉树的后序遍历

class Solution { 
public: 
    vector<int> postorderTraversal(TreeNode* root) { 
        vector<int>ans; 
        return morrisPostorder(root,ans); 
    } 
    vector<int> morrisPostorder(TreeNode* head,vector<int>&ans) { 
        TreeNode* cur=head; 
        TreeNode* mostRight=NULL; 
        while(cur!=NULL) { 
            mostRight=cur->left; 
            if(mostRight!=NULL) { 
                while(mostRight->right!=NULL&&mostRight->right!=cur) { 
                    mostRight=mostRight->right; 
                } 
                if(mostRight->right==NULL) { 
                    mostRight->right=cur; 
                    cur=cur->left; 
                    continue; 
                } 
                else { 
                    mostRight->right=NULL; 
                    collect(cur->left,ans); 
                } 
            } 
            cur=cur->right; 
        } 
        collect(head,ans); 
        return ans; 
    } 
    void collect(TreeNode* head,vector<int>&ans) { 
        TreeNode* tail=reverse(head); 
        TreeNode* cur=tail; 
        while(cur!=NULL) { 
            ans.push_back(cur->val); 
            cur=cur->right; 
        } 
        reverse(tail); 
    } 
    TreeNode* reverse(TreeNode* cur) { 
        TreeNode* pre=NULL; 
        TreeNode* next=NULL; 
        while(cur!=NULL) { 
            next=cur->right; 
            cur->right=pre; 
            pre=cur; 
            cur=next; 
        } 
        return pre; 
    } 
};

后序遍历就是在 morris 遍历时,在第二次到达某个节点时,去逆序收集该节点左树的右边界,最后逆序收集整棵树的右边界。对于逆序操作,就类似于单链表的反转,记得最后需要反转回去。

5.Morris 遍历的局限性

很容易就能看出,局限性就是每个节点来到的次数比较少。因为 morris 遍历每个点最多来两次,而 dfs 每个点会来三次,所以一些需要 dfs 整合信息的情况 morris 就不适用了.

二、应用

1.验证二叉搜索树

class Solution { 
public: 
    bool isValidBST(TreeNode* root) { 
        TreeNode* cur=root; 
        TreeNode* mostRight=NULL; 
        TreeNode* pre=NULL; 
        bool ans=true; 
        while(cur!=NULL) { 
            mostRight=cur->left; 
            if(mostRight!=NULL) { 
                while(mostRight->right!=NULL&&mostRight->right!=cur) { 
                    mostRight=mostRight->right; 
                } 
                if(mostRight->right==NULL) { 
                    mostRight->right=cur; 
                    cur=cur->left; 
                    continue; 
                } 
                else { 
                    mostRight->right=NULL; 
                } 
            } 
            if(pre!=NULL&&pre->val>=cur->val) { 
                ans=false; 
            } 
            pre=cur; 
            cur=cur->right; 
        } 
        return ans; 
    } 
};

因为中序遍历时值单调递增就是 BST,所以直接中序遍历即可。所以多用一个 pre 变量记录上一个位置,然后每次比较即可。注意这里不能直接返回,需要都把那么改过的右指针改回来,最后再返回。

2.二叉树的最小深度

class Solution { 
public: 
    int minDepth(TreeNode* head) { 
        if(head==NULL) { 
            return 0; 
        } 
        TreeNode* cur=head; 
        TreeNode* mostRight=NULL; 
        int preLevel=0; 
        int rightLen=0; 
        int ans=1e9; 
        while(cur!=NULL) { 
            mostRight=cur->left; 
            if(mostRight!=NULL) { 
                rightLen=1; 
                while(mostRight->right!=NULL&&mostRight->right!=cur) { 
                    rightLen++; 
                    mostRight=mostRight->right; 
                } 
                if(mostRight->right==NULL) { 
                    preLevel++; 
                    mostRight->right=cur; 
                    cur=cur->left; 
                    continue; 
                } 
                else { 
                    if(mostRight->left==NULL) { 
                        ans=min(ans,preLevel); 
                    } 
                    preLevel-=rightLen; 
                    mostRight->right=NULL; 
                } 
            } 
            else { 
                preLevel++; 
            } 
            cur=cur->right; 
        } 
        rightLen=1; 
        cur=head; 
        while(cur->right!=NULL) { 
            rightLen++; 
            cur=cur->right; 
        } 
        if(cur->left==NULL) { 
            ans=min(ans,rightLen); 
        } 
        return ans; 
    } 
};

考虑再多设置两个变量,preLevel 表示上一个节点所在的层数,rightLen 为树右边界的长度,所以在找最右节点时可以求得 rightLen。之后,若是第一次来,因为之后 cur 要左移,所以 preLevel 直接加一。而如果是第二次来,那么如果此时的最右节点是叶节点,那么就可以直接统计 ans 了。否则,当前的层数就是左树最右节点的层数减去这个最右边界的长度。另外,如果压根没有左树,那么必然是从上方节点来的,所以 preLevel 直接加一即可。

注意,当遍历结束后,整棵树的最右节点是没有被统计过的,所以需要额外特判一下。

3.二叉树的最近公共祖先

class Solution { 
public: 
    TreeNode* lowestCommonAncestor(TreeNode* head, TreeNode* o1, TreeNode* o2) { 
        if(preOrder(o1->left,o1,o2)!=NULL||preOrder(o1->right,o1,o2)!=NULL) { 
            return o1; 
        } 
        if(preOrder(o2->left,o1,o2)!=NULL||preOrder(o2->right,o1,o2)!=NULL) { 
            return o2; 
        } 
        TreeNode* lleft=preOrder(head,o1,o2); 
        TreeNode* cur=head; 
        TreeNode* mostRight=NULL; 
        TreeNode* ans=NULL; 
        while(cur!=NULL) { 
            mostRight=cur->left; 
            if(mostRight!=NULL) { 
                while(mostRight->right!=NULL&&mostRight->right!=cur) { 
                    mostRight=mostRight->right; 
                } 
                if(mostRight->right==NULL) { 
                    mostRight->right=cur; 
                    cur=cur->left; 
                    continue; 
                } 
                else { 
                    mostRight->right=NULL; 
                    if(ans==NULL) { 
                        if(rightCheck(cur->left,lleft)) { 
                            if(preOrder(lleft->right,o1,o2)!=NULL) { 
                                ans=lleft; 
                            } 
                            lleft=cur; 
                        } 
                    } 
                } 
            } 
            cur=cur->right; 
        } 
        return ans!=NULL?ans:lleft; 
    } 
    TreeNode* preOrder(TreeNode* head,TreeNode* o1,TreeNode* o2) { 
        TreeNode* cur=head; 
        TreeNode* mostRight=NULL; 
        TreeNode* ans=NULL; 
        while(cur!=NULL) { 
            mostRight=cur->left; 
            if(mostRight!=NULL) { 
                while(mostRight->right!=NULL&&mostRight->right!=cur) { 
                    mostRight=mostRight->right; 
                } 
                if(mostRight->right==NULL) { 
                    if(ans==NULL&&(cur==o1||cur==o2)) { 
                        ans=cur; 
                    } 
                    mostRight->right=cur; 
                    cur=cur->left; 
                    continue; 
                } 
                else { 
                    mostRight->right=NULL; 
                } 
            } 
            else { 
                if(ans==NULL&&(cur==o1||cur==o2)) { 
                    ans=cur; 
                } 
            } 
            cur=cur->right; 
        } 
        return ans; 
    } 
    bool rightCheck(TreeNode* head, TreeNode* target) { 
        while(head!=NULL) { 
            if(head==target) { 
                return true; 
            } 
            head=head->right; 
        } 
        return false; 
    } 
};

感觉这个的思路和 tarjan 算法求 lca 很类似。

首先,定义 preOrder 方法为从某个节点开始,o1 和 o2 谁先被找到就返回谁,所以上来可以先特判两者是否其中一个是另一个的 lca。之后,再从 head 开始 preOrder 一遍,那么此时谁先被找到谁就是左侧的节点 left。

之后去跑 morris 遍历,当第二次来到当前节点时,去 check 看 left 是否在 cur 左树右边界上。如果在,那么就去看 left 的右树里是否有另一个。如果有,那么此时的 left 就是 lca。否则就把 left 设置为当前节点 cur。注意最后需要额外再考虑一下最后的 left。

大体的原理就是因为已经找到 left 了,所以就只用查右树即可。

目录

  1. 一、原理
  2. 1.内容
  3. (1)核心
  4. (2)过程
  5. 2.二叉树的前序遍历
  6. 3.二叉树的中序遍历
  7. 4.二叉树的后序遍历
  8. 5.Morris 遍历的局限性
  9. 二、应用
  10. 1.验证二叉搜索树
  11. 2.二叉树的最小深度
  12. 3.二叉树的最近公共祖先
  • 💰 8折买阿里云服务器限时8折了解详情
  • Magick API 一键接入全球大模型注册送1000万token查看
  • 🤖 一键搭建Deepseek满血版了解详情
  • 一键打造专属AI 智能体了解详情
极客日志微信公众号二维码

微信扫一扫,关注极客日志

微信公众号「极客日志V2」,在微信中扫描左侧二维码关注。展示文案:极客日志V2 zeeklog

更多推荐文章

查看全部
  • Java 拼图小游戏开发实战:界面、逻辑与交互优化
  • 基于 Vue 3 构建企业级 Web Components 组件库
  • AI 短视频制作技术指南:文本、图片与视频生成
  • VS Code 中切换或退出 GitHub Copilot 账号方法
  • CTFShow Web 命令执行 29-124 实战通关指南
  • C++11 新特性:可变参数模板、类功能增强及 STL 变化
  • 自然语言处理在社交媒体分析中的应用与实战
  • FPGA 温度采集系统设计:MAX6675 驱动与 Qt 上位机曲线绘制
  • Java 中 Double 类型精度丢失原理与示例
  • 基于 AI 辅助开发的在线图书借阅平台设计与实现
  • 利用大疆 SRT 数据实现高精度 AR 视频投射
  • 双向最大匹配算法在古诗词与现代文分词中的应用效果
  • OpenClaw 对接飞书机器人配置踩坑:消息无响应与 Gateway 断开排查
  • 协作机器人轴孔装配的轨迹优化与智能搜索技术
  • AI 办公实战指南:7 套书籍助你精准提效与职场进阶
  • 植物大战僵尸融合版:多平台安装配置与性能优化指南
  • C++ 哈希表原理与底层实现详解
  • 大模型时代:重构技术开发的生产与组织方式
  • PaddleOCR-VL 0.9B 本地一键部署教程
  • Python 异步环境下连接池泄漏的 4 种根因与解决方案

相关免费在线工具

  • 加密/解密文本

    使用加密算法(如AES、TripleDES、Rabbit或RC4)加密和解密文本明文。 在线工具,加密/解密文本在线工具,online

  • Gemini 图片去水印

    基于开源反向 Alpha 混合算法去除 Gemini/Nano Banana 图片水印,支持批量处理与下载。 在线工具,Gemini 图片去水印在线工具,online

  • Base64 字符串编码/解码

    将字符串编码和解码为其 Base64 格式表示形式即可。 在线工具,Base64 字符串编码/解码在线工具,online

  • Base64 文件转换器

    将字符串、文件或图像转换为其 Base64 表示形式。 在线工具,Base64 文件转换器在线工具,online

  • Markdown转HTML

    将 Markdown(GFM)转为 HTML 片段,浏览器内 marked 解析;与 HTML转Markdown 互为补充。 在线工具,Markdown转HTML在线工具,online

  • HTML转Markdown

    将 HTML 片段转为 GitHub Flavored Markdown,支持标题、列表、链接、代码块与表格等;浏览器内处理,可链接预填。 在线工具,HTML转Markdown在线工具,online