二叉树的遍历

二叉树的遍历主要有3种模式,前序,中序和后序。其递归版本,非常简单,大致如下:

void Traversal(TreeNode* root){
    if(!root)
        return;
    // visit root (preorder)
    Traversal(root->left);
    // visit root (inorder)
    Traversal(root->right);
    // visit root (postorder)
}

根据要求不同,分别在对应地方访问节点就可以了。

但是一方面,这种递归版本由于函数调用会影响效率,另一方面面试时很多面试官会让你转化成非递归版本。本来说不难,但是为了给面试官留下好印象,还是要非常熟悉这一套的,要求0失误快速写出来。


非递归版本

由于是递归转非递归,因此肯定会用到栈,即用栈去模拟计算机函数调用的过程。

前序遍历非常容易模拟,因为root先于其左右子树被访问,因此每次从栈顶取到root后直接访问,然后将其右儿子和左儿子依次压入栈中就可以了(注意先压右儿子,因为按照栈是FILO,所以右儿子会被后访问到)。

 vector<int> preorderTraversal(TreeNode *root) {
        vector<int> ans;
        if(root == NULL)
            return ans;

        stack<TreeNode*> st;
        st.emplace(root);

        while(!st.empty()){
            //访问root
            TreeNode * tmp = st.top();
            st.pop();
            ans.emplace_back(tmp->val);

            //先压右儿子
            if(tmp->right != NULL) st.emplace(tmp->right);
            //再压左儿子
            if(tmp->left != NULL) st.emplace(tmp->left);
        }
        return ans;
    }

但是到了中序和后序遍历就稍微麻烦一点了,因为root并不是第一个被访问到,因此当我们从栈顶弹出root时,会遇到一个麻烦,即它是第一次被访问到,还是已经访问完它的左右子树了呢?

如果是第一次被访问到,显然现在我们还不能正式遍历它,而是要先遍历它的子树,那么这个时候我们就需要把它暂时放回到栈中,等遍历完它的子树后,再从栈中把它取出来遍历。 那么如何判断它的左右子树被遍历到,当然我们可以给每个节点加一个变量来标记它是否已经被遍历过;或者利用C++中的pair作为栈的基本元素, 除了在栈中记录节点,同时在栈中记录每个节点进出栈的次数,通过进出栈的次数决定它的左右子树是否被遍历完了。

但是不论哪种方法,都要利用额外的空间,显然这样做虽然能行,但是不够好。当然有更好的解决办法。

下面我们先考虑中序遍历,当我们拿到一个节点root时:

  1. 显然最先遍历的是它的左子树(记为 T1 T1 的根记为 l1 ),然后才回来访问root和root的右子树;
  2. 而当我们第一次访问子树 T1 ,应该先遍历它的左子树(记为 T2 T2 的根记为 l2 ),然后才是 l1 和它的右子树
  3. 以此类推,每次都是先遍历节点的左子树,然后才是节点本身

    那么既然这样,我们拿到一个节点root后先按顺序将它以及 l1,l2,,lh 压入栈中。然后再从栈中取出元素,这时栈顶元素一定是树中最左下角的元素 lh ,此时我们遍历它,然后开始遍历 lh 的右子树就可以了。当访问完 lh 的右子树时,我们再从栈顶弹出元素,这个元素按照我们的压栈顺序一定是 lh 的父节点 lh1 ,而此时由于以 lh 为根的子树恰好刚遍历完,即 lh1 的左子树遍历完了,自然我们就需要遍历 lh1 ,然后再遍历 lh1 的右子树就可以了。

以此类推,这样我们就可以完成中序遍历。

vector<int> inorderTraversal(TreeNode *root) {
        vector<int> ans;

        TreeNode* t = root;
        stack<TreeNode* > st;
        while(t != NULL || !st.empty()){
            //如果t不空,将t左边的后代(即l1,l2...)依次压入栈中
            while( t != NULL){
                st.emplace(t);
                t = t->left;
            }
            if(!st.empty()){
                //此时栈顶元素的左子树一定遍历完了,那么我们开始遍历栈顶元素
                t = st.top();
                st.pop();
                ans.emplace_back(t->val);

                //然后开始遍历栈顶元素的右子树
                t = t->right;
            }
        }
        return ans;
    }

最后是后序遍历,还是用压栈的方法,但是比上面简单但又巧妙很多。由于后序遍历的特点是节点本身最后被访问到,这样就给了我们一个简单的方法判断某个节点root的左右子树是否被遍历到:

因为我们想遍历root时,一定是其右子树被遍历完的时候。而遍历右子树时,最后遍历到的一定是右子树的根,即root的右儿子。此时我们只需要一个变量pre, 用来记录之前遍历的那个节点,。然后和root的右儿子做一下比较,如果两者相同,说明右子树已经遍历完,这时我们直接遍历root就可以了。当然遇到root没有右儿子时,说明右子树为空,这样我们就要拿pre和root的左儿子比较。如果root连左儿子都没有,那么说明它是叶子节点,直接访问就可以了。

vector<int> postorderTraversal(TreeNode *root) {
        vector<int> ans;
        if(root == NULL)
            return ans;

        //pre表示之前遍历到的那个节点
        TreeNode *pre = NULL, *tmp;

        stack<TreeNode* >st;
        st.emplace(root);

        while(!st.empty()){
            tmp = st.top();
            //如果pre等于tmp的右儿子;或者没有右儿子的情况下,pre等于其左儿子,或者甚至连左儿子都没有。说明tmp的左右子树已经访问完了,那么我们开始访问tmp,并更新pre
            if( (tmp->right != NULL && tmp->right == pre) ||
                (tmp->right == NULL && (tmp->left == pre ||
                tmp->left == NULL)) ){

                pre = tmp;
                st.pop();
                ans.emplace_back(tmp->val);
            }
            //否则将tmp的右儿子和左儿子加入栈中
            else{
                if(tmp->right != NULL) st.emplace(tmp->right);
                if(tmp->left != NULL) st.emplace(tmp->left);
            }
        }
        return ans;
    }

除了上面这些方法,还有其他用栈模拟的,这里不再冗述。不过需要指的提一下的是,如果允许遍历时修改节点指针,只要保证遍历完之后能恢复原状,那么可以不用栈就完成遍历,即O(1)的空间,具体的方法就是Morris Traversal,有兴趣的同学自己去看看吧,这种方法能够在空间要求极为严格的情况下,完成二叉树的搜索。

内容概要:文章以“智能网页数据标注工具”为例,深入探讨了谷歌浏览器扩展在毕业设计中的实战应用。通过开发具备实体识别、情感分类等功能的浏览器扩展,学生能够融合前端开发、自然语言处理(NLP)、本地存储与模型推理等技术,实现高效的网页数据标注系统。文中详细解析了扩展的技术架构,涵盖Manifest V3配置、内容脚本与Service Worker协作、TensorFlow.js模型在浏览器端的轻量化部署与推理流程,并提供了核心代码实现,包括文本选择、标注工具栏动态生成、高亮显示及模型预测功能。同时展望了多模态标注、主动学习与边缘计算协同等未来发展方向。; 适合人群:具备前端开发基础、熟悉JavaScript和浏览器机制,有一定AI模型应用经验的计算机相关专业本科生或研究生,尤其适合将浏览器扩展与人工智能结合进行毕业设计的学生。; 使用场景及目标:①掌握浏览器扩展开发全流程,理解内容脚本、Service Worker与弹出页的通信机制;②实现在浏览器端运行轻量级AI模型(如NER、情感分析)的技术方案;③构建可用于真实场景的数据标注工具,提升标注效率并探索主动学习、协同标注等智能化功能。; 阅读建议:建议结合代码实例搭建开发环境,逐步实现标注功能并集成本地模型推理。重点关注模型轻量化、内存管理与DOM操作的稳定性,在实践中理解浏览器扩展的安全机制与性能优化策略。
基于Gin+GORM+Casbin+Vue.js的权限管理系统是一个采用前后端分离架构的企业级权限管理解决方案,专为软件工程和计算机科学专业的毕业设计项目开发。该系统基于Go语言构建后端服务,结合Vue.js前端框架,实现了完整的权限控制和管理功能,适用于各类需要精细化权限管理的应用场景。 系统后端采用Gin作为Web框架,提供高性能的HTTP服务;使用GORM作为ORM框架,简化数据库操作;集成Casbin实现灵活的权限控制模型。前端基于vue-element-admin模板开发,提供现代化的用户界面和交互体验。系统采用分层架构和模块化设计,确保代码的可维护性和可扩展性。 主要功能包括用户管理、角色管理、权限管理、菜单管理、操作日志等核心模块。用户管理模块支持用户信息的增删改查和状态管理;角色管理模块允许定义不同角色并分配相应权限;权限管理模块基于Casbin实现细粒度的访问控制;菜单管理模块动态生成前端导航菜单;操作日志模块记录系统关键操作,便于审计和追踪。 技术栈方面,后端使用Go语言开发,结合Gin、GORM、Casbin等成熟框架;前端使用Vue.js、Element UI等现代前端技术;数据库支持MySQL、PostgreSQL等主流关系型数据库;采用RESTful API设计规范,确保前后端通信的标准化。系统还应用了单例模式、工厂模式、依赖注入等设计模式,提升代码质量和可测试性。 该权限管理系统适用于企业管理系统、内部办公平台、多租户SaaS应用等需要复杂权限控制的场景。作为毕业设计项目,它提供了完整的源码和论文文档,帮助学生深入理解前后端分离架构、权限控制原理、现代Web开发技术等关键知识点。系统设计规范,代码结构清晰,注释完整,非常适合作为计算机相关专业的毕业设计参考或实际项目开发的基础框架。 资源包含完整的系统源码、数据库设计文档、部署说明和毕
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值