当前位置: 首页 > news >正文

【Leetcode】二叉树的遍历

二叉树的遍历

在leetcode上刷题的时候还是能感觉出来对于二叉树类型的题目掌握并不是很好,所以基于一篇高质量题解梳理一下自己的思路。

①对于树和二叉树数据结构相关知识补充
《数据结构(3):树与二叉树(上)》
②leetcode上的题解原文
《遍历二叉树的方法合集》

中序遍历

1. 中序遍历的描述
如果二叉树为空,则不进行任何处理,否则:
①中序遍历地访问左子树
②访问根节点
③中序遍历地访问右子树

2. 访问轨迹图

在这里插入图片描述
3. 实现代码

(0)问题描述
给定一个二叉树(树节点),返回它的中序遍历(vector容器)。

Leetcode-94 二叉树的中序遍历

(1)递归实现

/**
 * Definition for a binary tree node.
 * struct TreeNode {
 *     int val;
 *     TreeNode *left;
 *     TreeNode *right;
 *     TreeNode() : val(0), left(nullptr), right(nullptr) {}
 *     TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
 *     TreeNode(int x, TreeNode *left, TreeNode *right) : val(x), left(left), right(right) {}
 * };
 */
class Solution {
public:
    void inorder(TreeNode* node,vector<int> &b){
        if(node){
            inorder(node->left,b);
            b.push_back(node->val);
            inorder(node->right,b);
        }
    }
    vector<int> inorderTraversal(TreeNode* root) {
        vector<int> res;
        inorder(root,res);
        return res;
    }
};

(2)迭代实现

迭代方法就是采用遍历(或者循环等)其他方法来模拟递归调用时程序运行的过程。

在进行递归调用时,系统创建了栈来存储运行状态。我们可以自行构建栈的数据结构,结合栈“先进后出”的特点,按照特定的顺序将节点压栈,再弹栈、输出即可。

对于中序遍历,每一棵子树,都要按照先左子树再根节点,最后右子树的顺序进行遍历。
用栈模拟递归的思路是——针对当前的每一棵子树,先找这棵子树的左子树的最左节点(因为该节点是当前递归的终点),在找最左节点的时候还要利用栈对路径进行记录,以便继续模拟递归进行回溯。
最左节点处理完后,回到上一层的根节点,输出根节点,再移到右子节点,按照上述相同的步骤进行处理。

/**
 * Definition for a binary tree node.
 * struct TreeNode {
 *     int val;
 *     TreeNode *left;
 *     TreeNode *right;
 *     TreeNode() : val(0), left(nullptr), right(nullptr) {}
 *     TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
 *     TreeNode(int x, TreeNode *left, TreeNode *right) : val(x), left(left), right(right) {}
 * };
 */
class Solution {
public:
    vector<int> inorderTraversal(TreeNode* root) {
        stack<TreeNode*> st;
    	vector<int> res;
        if(root==nullptr)
    	    return res;
        
        TreeNode *cur = root;
        while(!st.empty()||cur != nullptr){
            while(cur!=nullptr){//将左子树的最左路径全部压入
                st.push(cur);
                cur = cur->left;
            }
            TreeNode *n = st.top();
            st.pop();
            res.push_back(n->val);
            if(n->right)
                cur = n->right;
        }
		return res;
    }
};

(3)Morris方法遍历实现

Morris方法实质上是借助了线索二叉树这样的数据结构,利用二叉树中的空指针域来存储每一个结点的前驱或者后继,从而不需要花费额外的存储空间。
《知乎上讲解Morris中序遍历-流程图以及过程图》
《Morris遍历二叉树的前、中、后序遍历即代码实现》
我也是看上面两篇文章来学习Morris方法的,读者可以阅读原作者的文章进行了解~

Morris的方法其实也有一个从“改变了树原来的结构”到“不改变树的结构”的优化过程,现在给出的很多题解是直接使用不改变树的结构的方法。

以中序遍历为例,来梳理一下Morris遍历的思路与代码实现。
中序遍历建立线索树(Morris方法)的本质在于,每一个(子)树根节点的左子树并不一定是该节点的前驱,所以我们想要找到每个根节点的前驱(该节点左子树的最右节点)。

按照这样的思路,我们把左子树全部处理一遍,再根据之前保留的后继,处理右子树。

①流程图-----改变了树的结构
在这里插入图片描述

/**
 * Definition for a binary tree node.
 * struct TreeNode {
 *     int val;
 *     TreeNode *left;
 *     TreeNode *right;
 *     TreeNode() : val(0), left(nullptr), right(nullptr) {}
 *     TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
 *     TreeNode(int x, TreeNode *left, TreeNode *right) : val(x), left(left), right(right) {}
 * };
 */
class Solution {
public:
    vector<int> inorderTraversal(TreeNode* root) {
        vector<int> res;
        TreeNode *cur = root;
        TreeNode *pre;
        while(cur != nullptr){//当前节点非空时
            if(cur->left == nullptr){//左子树为空时,访问当前节点,转向右子树
                res.push_back(cur->val);
                cur = cur->right;//每次左子树遇空回溯的时候,转向右子树实际上都是转向后继了
            }
            else{//左子树非空时
                pre = cur->left;
                while(pre->right)
                    pre = pre->right;//找到左子树的最右子节点
                pre->right =cur;//该节点的右指针指向当前节点
                TreeNode *tmp = cur;
                cur = cur->left;//移向左子树
                tmp->left = nullptr;//删除左指针,防止循环
            }
        }
        return res;
    }
};

②优化-----不改变树的结构

  • 仍然需要建立线索指针,但是在遍历之后就会删除
  • 因为不能改变树结构,所以当前节点会经过多次遍历
  • 为了避免循环,在第二次遍历到cur节点时候需要强制转移到右子树

p.s.上述的这一系列过程在下图中得以体现
在这里插入图片描述

/**
 * Definition for a binary tree node.
 * struct TreeNode {
 *     int val;
 *     TreeNode *left;
 *     TreeNode *right;
 *     TreeNode() : val(0), left(nullptr), right(nullptr) {}
 *     TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
 *     TreeNode(int x, TreeNode *left, TreeNode *right) : val(x), left(left), right(right) {}
 * };
 */
class Solution {
public:
    vector<int> inorderTraversal(TreeNode* root) {
        vector<int> res;
        TreeNode *cur = root;
        TreeNode *pre = nullptr;
        while(cur != nullptr){//当前节点非空时
            if(cur->left == nullptr){//左子树为空时,访问当前节点,转向右子树
                res.push_back(cur->val);
                cur = cur->right;
            }
            else{//左子树非空时
                pre = cur->left;
                while(pre->right && pre->right!=cur)//新增判断条件要求是第一次访问该节点,防止循环
                    pre = pre->right;//找到左子树的最右子节点
                if(pre->right == nullptr){//循环终止条件有两个,需要判断是哪一个导致循环终止的
                    pre->right =cur;//该节点的右指针指向当前节点
                    cur = cur->left;//移向左子树
                }
                else{//第二次访问节点时
                    pre->right == nullptr;//删除指针
                    res.push_back(cur->val);
                    cur = cur->right;//强制转向右子树
                }
            }
        }
        return res;
    }
};

p.s. 看了好几篇博客,代码应该是没问题的,但是在乐扣上提交一直是栈溢出…


前序遍历

1. 先(前)序遍历的描述
如果二叉树为空,则不进行任何处理,否则:
①先访问根节点
②再先序遍历地访问左子树
③先序遍历地访问右子树

2. 访问轨迹图
在这里插入图片描述
3. 实现代码

(0)问题描述
给定一个二叉树(树节点),返回它的前序遍历(vector容器)。

Leetcode-144 二叉树的前序遍历

(1)递归实现

void PreOrder(BiTree T){
    if(T!=NULL){
        visit(T);
        PreOrder(T->lchild);
        PreOrder(T->rchild);
    }
}

(2)迭代实现

迭代方法就是采用遍历(或者循环等)其他方法来模拟递归调用时程序运行的过程。

在进行递归调用时,系统创建了栈来存储运行状态。我们可以自行构建栈的数据结构,结合栈“先进后出”的特点,按照特定的顺序将节点压栈,再弹栈、输出即可。

对于先序遍历,我们希望二叉树按照“根节点-左子树-右子树”的顺序进行输出,那么我们可以在输出当前根节点之后,按照先右子树,再左子树的顺序将节点压入栈

/**
 * Definition for a binary tree node.
 * struct TreeNode {
 *     int val;
 *     TreeNode *left;
 *     TreeNode *right;
 *     TreeNode(int x) : val(x), left(NULL), right(NULL) {}
 * };
 */
class Solution {
public:
    vector<int> preorderTraversal(TreeNode* root) {
    	stack<TreeNode*> st;
    	vector<int> res;
        if(root)
    	    st.push(root);
        else return res;//不提前进行判空会有超时错误
    	while(!st.empty()){
    		TreeNode *cur = st.top();
    		st.pop();
            if(cur)//以下三个判断都不能漏
    		    res.push_back(cur->val);
            if(cur->right)
    		    st.push(cur->right);
            if(cur->left)
    		    st.push(cur->left);
    		
		}
		return res;
    }
            
};

(3)Morris方法遍历实现

先序遍历和中序遍历用Morris方法的思路是大致相同的,但是因为左子树和根节点的输出顺序问题,“访问当前节点”这一操作的顺序不一样。
中序遍历在当前节点为空时(或者在对某一个节点访问第二次时)对节点进行访问,把节点访问延后了,使得根节点在左子树之后访问。

但是先序遍历是在找到了当前根节点的前驱节点之后,马上就对当前根节点进行访问,以保证,每走到一个子树的根节点,都是先对根节点进行访问。

/**
 * Definition for a binary tree node.
 * struct TreeNode {
 *     int val;
 *     TreeNode *left;
 *     TreeNode *right;
 *     TreeNode() : val(0), left(nullptr), right(nullptr) {}
 *     TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
 *     TreeNode(int x, TreeNode *left, TreeNode *right) : val(x), left(left), right(right) {}
 * };
 */
class Solution {
public:
    vector<int> preorderTraversal(TreeNode* root) {
        TreeNode* cur = root;
        TreeNode* pre = nullptr;
        vector<int> res;
        
        while(cur!=nullptr){
            if(cur->left == nullptr){
                res.push_back(cur->val);
                cur = cur->right;
            }
            else{
                pre = cur->left;
                while(pre->right && pre->right != cur){
                    pre = pre->right;
                }
                if(pre->right == nullptr){
                    pre->right = cur;
                    res.push_back(cur->val);
                    cur = cur->left;
                }
                else{
                    pre->right = nullptr;
                    cur = cur->right;
                }

            }
        }
        return res;
    }
};

后序遍历

1. 后序遍历的描述
如果二叉树为空,则不进行任何处理,否则:
①先后序遍历左子树
②再后序遍历右子树
③最后访问根节点

2. 访问轨迹图

在这里插入图片描述

3. 实现代码

(0)问题描述
给定一个二叉树(树节点),返回它的后序遍历(vector容器)。

Leetcode-145 二叉树的后序遍历

(1)递归实现

/**
 * Definition for a binary tree node.
 * struct TreeNode {
 *     int val;
 *     TreeNode *left;
 *     TreeNode *right;
 *     TreeNode(int x) : val(x), left(NULL), right(NULL) {}
 * };
 */
class Solution {
public:
    void postorder(TreeNode* node,vector<int> &b){
        if(node){
            postorder(node->left,b);
            postorder(node->right,b);
            b.push_back(node->val);
        }
    }
    vector<int> postorderTraversal(TreeNode* root) {
        vector<int> res;
        postorder(root,res);
        return res;
    }
};

(2)迭代实现

迭代方法就是采用遍历(或者循环等)其他方法来模拟递归调用时程序运行的过程。

在进行递归调用时,系统创建了栈来存储运行状态。我们可以自行构建栈的数据结构,结合栈“先进后出”的特点,按照特定的顺序将节点压栈,再弹栈、输出即可。

对于先序遍历,我们希望二叉树按照“根节点-左子树-右子树”的顺序进行输出,那么我们可以在输出当前根节点之后,按照先右子树,再左子树的顺序将节点压入栈

/**
 * Definition for a binary tree node.
 * struct TreeNode {
 *     int val;
 *     TreeNode *left;
 *     TreeNode *right;
 *     TreeNode(int x) : val(x), left(NULL), right(NULL) {}
 * };
 */
class Solution {
public:
    vector<int> preorderTraversal(TreeNode* root) {
    	stack<TreeNode*> st;
    	vector<int> res;
        if(root)
    	    st.push(root);
        else return res;//不提前进行判空会有超时错误
    	while(!st.empty()){
    		TreeNode *cur = st.top();
    		st.pop();
            if(cur)//以下三个判断都不能漏
    		    res.push_back(cur->val);
            if(cur->right)
    		    st.push(cur->right);
            if(cur->left)
    		    st.push(cur->left);
    		
		}
		return res;
    }
            
};

(3)Morris方法遍历

后序遍历整体上要比中序和先序更加复杂一点,思路如下:
在这里插入图片描述

/**
 * Definition for a binary tree node.
 * struct TreeNode {
 *     int val;
 *     TreeNode *left;
 *     TreeNode *right;
 *     TreeNode(int x) : val(x), left(NULL), right(NULL) {}
 * };
 */
class Solution {
public:
    void Reverse(TreeNode* from,TreeNode* to){//反转链表
        TreeNode* x = from;
        TreeNode* y = from->right;
        TreeNode* z;
        if(from == to)
            return ;
        x->right = NULL;
        while(x != to){
            z = y->right;//保留右部链表
            y->right = x;
            x = y;
            y = z;
        }
    }
    void printreverse(TreeNode* from,TreeNode* to,vector<int>& res){
        //反序将链表数值保存进入res之中
        Reverse(from,to);
        TreeNode* p = to;
        while(true){
            res.push_back(p->val);
            if(p == from)
                break;
            p = p->right;
        }
        reverse(to,from);
    }
    vector<int> postorderTraversal(TreeNode* root) {
        vector<int> res;
        TreeNode dump(-1);//创建辅助结点
        dump.left = root;
        TreeNode* cur = &dump;
        TreeNode* pre = NULL;

        while(cur !=NULL){
            if(cur->left == NULL)
                cur = cur->right;//左子树为空,则转向右子树
            else{
                pre = cur->left;
                while(pre->right && pre->right != cur)
                    pre = pre->right;//找到当前节点左子树的最右子节点
                if(pre->right == NULL){
                    pre->right = cur;
                    cur = cur->left;
                }
                else{
                    printreverse(cur->left,pre,res);//如果第二次访问到该节点,逆序访问当前节点到其后继的链表
                    pre->right = NULL;
                    cur = cur->right;//转向右子树
                }
            }
        }
        return res;
    }
};

相关文章:

  • 【矩阵论】矩阵的相似标准型(4)(5)
  • 【PyTorch】深度学习基础:神经网络
  • 【矩阵论】矩阵的相似标准型(5)
  • 【台大李宏毅|ML】Gradient Descent
  • 【矩阵论】Hermite二次型(1)
  • 【矩阵论】Hermite二次型(2)
  • 【MIT算法导论】快排及随机化算法
  • 【矩阵论】Hermite二次型(3)
  • 【PyTorch】深度神经网络及训练
  • 【矩阵论】范数和矩阵函数(1)
  • 【PyTorch】入门知识
  • 【矩阵论】范数和矩阵函数(2)
  • 【矩阵论】矩阵的广义逆
  • 【吴恩达CNN】week1:卷积与池化
  • 【最优化】最优化理论的基本概念
  • 时间复杂度分析经典问题——最大子序列和
  • 10个最佳ES6特性 ES7与ES8的特性
  • cookie和session
  • Create React App 使用
  • Cumulo 的 ClojureScript 模块已经成型
  • ES6, React, Redux, Webpack写的一个爬 GitHub 的网页
  • Js基础知识(四) - js运行原理与机制
  • js写一个简单的选项卡
  • js作用域和this的理解
  • python 装饰器(一)
  • Vue学习第二天
  • 分享一个自己写的基于canvas的原生js图片爆炸插件
  • 事件委托的小应用
  • 数据仓库的几种建模方法
  • 双管齐下,VMware的容器新战略
  • 它承受着该等级不该有的简单, leetcode 564 寻找最近的回文数
  • 腾讯视频格式如何转换成mp4 将下载的qlv文件转换成mp4的方法
  • 通过npm或yarn自动生成vue组件
  • 微信小程序设置上一页数据
  • 阿里云ACE认证之理解CDN技术
  • ​软考-高级-系统架构设计师教程(清华第2版)【第1章-绪论-思维导图】​
  • #、%和$符号在OGNL表达式中经常出现
  • #laravel 通过手动安装依赖PHPExcel#
  • #Ubuntu(修改root信息)
  • $(selector).each()和$.each()的区别
  • (51单片机)第五章-A/D和D/A工作原理-A/D
  • (C语言版)链表(三)——实现双向链表创建、删除、插入、释放内存等简单操作...
  • (十六)一篇文章学会Java的常用API
  • (转)AS3正则:元子符,元序列,标志,数量表达符
  • ****Linux下Mysql的安装和配置
  • 、写入Shellcode到注册表上线
  • .NET 8.0 中有哪些新的变化?
  • .NET Framework与.NET Framework SDK有什么不同?
  • .net 提取注释生成API文档 帮助文档
  • .NET/C# 判断某个类是否是泛型类型或泛型接口的子类型
  • .net的socket示例
  • .net和jar包windows服务部署
  • @select 怎么写存储过程_你知道select语句和update语句分别是怎么执行的吗?
  • [Android]常见的数据传递方式
  • [asp.net core]project.json(2)