【算法练习Day12】树的递归遍历非递归遍历

news/2024/7/20 22:52:41 标签: 算法, 深度优先

在这里插入图片描述

​📝个人主页:@Sherry的成长之路
🏠学习社区:Sherry的成长之路(个人社区)
📖专栏链接:练题
🎯长路漫漫浩浩,万事皆有期待

文章目录

  • 递归遍历
    • 前序遍历
    • 中序遍历
    • 后序遍历
  • 非递归遍历
    • 前序遍历
    • 后序遍历
    • 中序遍历
    • 标记迭代法
  • 总结:

这一期讲树这个数据结构的相关知识

首先我们要明白树的两种通用遍历分别是深度优先搜索,和广度优先搜索。这里我们介绍深度优先搜索的三种表现形式:前序遍历,中序遍历和后序遍历。这三种搜索方式可以用递归法或者迭代法表示出来。事实上,很多递归能写出来的代码,大都可以使用迭代法表示出来。

递归遍历

前序遍历

class Solution 
{
public:
vector<int>result;
void dfs(TreeNode* root)
{
    if(root)
    result.push_back(root->val);
    if(root)
    dfs(root->left);
    if(root)
    dfs(root->right);
}
    vector<int> preorderTraversal(TreeNode* root) 
    {
        if(root==nullptr)
        return result;
        dfs(root);
        return result;
    }
};

前序遍历的规则是“中左右“,即先遍历树的中间节点,再分别遍历左右两子树,并在其遍历左右两子树时仍然遵循此规则。所以我们可以容易的理解dfs代码部分先将中间节点保存后,分别进行左子树和右子树的递归。

中序遍历和后序遍历的递归代码,都和前序遍历差不多,只是略微调整一下进入子树的时机而已,下面直接给出代码。

中序遍历

class Solution 
{
public:
	void dfs(TreeNode* root,vector<int>& result)
	{
	    if(root==nullptr)return ;
	    dfs(root->left,result);
	    result.push_back(root->val);
	    dfs(root->right,result);
	}
    vector<int> inorderTraversal(TreeNode* root) 
    {
        vector<int>result;
        dfs(root,result);
        return result;
    }
};

后序遍历

class Solution 
{
public:
	void dfs(TreeNode* root,vector<int>& result)
	{
	    if(root==nullptr)return ;
	    dfs(root->left,result);
	    dfs(root->right,result);
	    result.push_back(root->val);
	}
    vector<int> inorderTraversal(TreeNode* root) 
    {
        vector<int>result;
        dfs(root,result);
        return result;
    }
};

说完了递归遍历,我们再来看看非递归遍历

非递归遍历

非递归遍历中的迭代遍历,前后序的代码是差不多的,但是中序遍历有很大差别

前序遍历

先说前序遍历的迭代,思路是用一个栈来模拟递归的操作

为什么我们会想到使用栈来模拟呢?

因为递归实际上就是编译器将函数各参数放入递归内部,返回时再将其弹出,所以我们用一个栈来模拟递归操作,是再合适不过的。我们写一个循环判断栈中是否为空,为空则说明没有元素要处理了,那么循环内的逻辑就是先创立一个临时的节点指针指向当前栈口处元素,先判断其是否为空,为空不能操作,否则会操作空指针。
不为空时我们先将遍历到的节点直接放入数组中,因为前序遍历是先处理中间节点,这之后我们按照先放入右节点再放入左节点的规律来使节点指针向后遍历。

这是为什么呢?

原因在于栈的独特定义,我们要先放入右节点再放入左节点,才能在下一步时候先处理左节点!以下代码

class Solution 
{
public:
    vector<int> preorderTraversal(TreeNode* root) 
    {
        stack<TreeNode*> s;
        vector<int>result;
        if(root==nullptr)return result;
        s.push(root);//加入的是节点而并非节点对应的值,这里要尤其注意
        while(!s.empty())
        {
            TreeNode* node=s.top();
            s.pop();
            if(node)
            result.push_back(node->val);
            else continue;
            s.push(node->right);
            s.push(node->left);
        }
        return result;
    }
};

后序遍历

后序遍历思路很类似,需要注意的是后序遍历的情况为”左右中“,我们可以按照前序遍历的代码模板将前序遍历的压栈操作改为先放入左节点再放入右节点,这样它对应的原本遍历方法是中右左,当全部遍历完成之后,我们将数组部分反转,即可得到后序遍历。

class Solution {
public:
    vector<int> postorderTraversal(TreeNode* root) {
        stack<TreeNode*>s;
        vector<int>result;
        if(root==nullptr)
        return result;
        s.push(root);
        while(!s.empty())
        {
            TreeNode*node=s.top();
            s.pop();
            if(node)
            result.push_back(node->val);
            else continue;
            s.push(node->left);
            s.push(node->right);
        }
        reverse(result.begin(),result.end());
        return result;
    }
};

这里可以看出,后序和前序的代码差别不大,也就是改了入栈的顺序,和反转了一下数组。

中序遍历

接着,我们再来看看中序遍历。

中序遍历的思路是:我们需要建立一个指针来存储各节点,然后我们将它们放入栈内,我们先一直向二叉树的左节点遍历直到无法继续为止,弹出栈顶元素,此时栈顶元素即为我们要找的元素,为什么呢?

因为我们是一直向左走,直到无法再向左走,弹出的那个元素,此时就是这个子树的中间节点(由于没有左节点)直接放入答案数组内,再遍历这个节点的右节点(不要忘记将其压入栈内),如果它有左节点接着遍历,如果它没有,那么就直接弹出,重复上述操作。

为什么中序遍历不能像前后序那样只调整位置呢?
拿前序遍历举例它是先遍历的中间节点也是先处理的中间节点,后序的代码前面思路也是如此,只是后面有调整位置,换句话说,是此时遍历的节点正是我们此时要处理的数据!

那我们在遍历中序时候可以先遍历左边子树达成一样的逻辑吗?答案肯定是否定的,因为我们一开始只有root这个节点,而它指向了根节点,也就是说我们注定是先遍历中间节点,所以这样的方法并不适合中序迭代。

class Solution {
public:
    vector<int> inorderTraversal(TreeNode* root) {
        stack<TreeNode*>s;
        vector<int>result;
        TreeNode*cur=root;
        while(cur!=nullptr||!s.empty()){
            if(cur){
                s.push(cur);cur=cur->left;
            }
            else{
                cur=s.top();s.pop();
                if(cur)result.push_back(cur->val);
                cur=cur->right;
            }
        }
        return result;
    }
};

标记迭代法

那么有没有可以将三种迭代统一起来的代码呢?也是有的,这里的思路也是创建一个栈来模拟,不同的是,我们将凡是已经遍历过的数据一股脑地放进去,在我们要处理的数据之后紧接着加入一个null来标记,这种思路也被称为标记迭代法。当我们遍历到null的时候,就对下一数据进行加入答案数组的处理。

中序

class Solution {
public:
    vector<int> inorderTraversal(TreeNode* root) {
        vector<int> result;
        stack<TreeNode*> st;
        if (root != NULL) st.push(root);
        while (!st.empty()) {
            TreeNode* node = st.top();
            if (node != NULL) {
                st.pop(); // 将该节点弹出,避免重复操作,下面再将右中左节点添加到栈中
                if (node->right) st.push(node->right);  // 添加右节点(空节点不入栈)
 
                st.push(node);                          // 添加中节点
                st.push(NULL); // 中节点访问过,但是还没有处理,加入空节点做为标记。
 
                if (node->left) st.push(node->left);    // 添加左节点(空节点不入栈)
            } else { // 只有遇到空节点的时候,才将下一个节点放进结果集
                st.pop();           // 将空节点弹出
                node = st.top();    // 重新取出栈中元素
                st.pop();
                result.push_back(node->val); // 加入到结果集
            }
        }
        return result;
    }
};

该思路旨在if条件语句中处理压栈操作,else里处理加入答案数组,使代码变得简介,但是思路难想到,建议只记一种思路。

总结:

今天我们完成了树的递归遍历和非递归遍历,了解了一种新的方法标记迭代法,相关的思想需要多复习回顾。接下来,我们继续进行算法练习。希望我的文章和讲解能对大家的学习提供一些帮助。

当然,本文仍有许多不足之处,欢迎各位小伙伴们随时私信交流、批评指正!我们下期见~

在这里插入图片描述


http://www.niftyadmin.cn/n/5067496.html

相关文章

MySQL中的 增 删 查 改(CRUD)

先创建一个名为&#xff1a; title 表&#xff1a;下文的所有操作都基于此表 注&#xff1a;因为MySQL对大小写不敏感所以大写小写都可以。 新增 insert into 表名 value(数据&#xff0c;数据),.......&#xff1b; 可以单行&#xff0c;多行插入。 insert into 表名&#…

Linux基本指令介绍系列第四篇

文章目录 前言一、Linux基本指令介绍1、more指令2、less指令3、head指令4、tail指令5、bc指令6、管道文件介绍7、与时间相关的指令 总结 前言 本文介绍Linux使用时的部分指令&#xff0c;读者如果想了解更多基本指令的使用&#xff0c;可以关注博主的后续的文章。 博主使用的实…

潮流来袭!中国首届虚拟艺术巡展NFS将在广州YCC!天宜盛大开启!

中国首届虚拟艺术巡展 NFT Showcase (NFS) 即将来袭&#xff0c;本活动由 Web3 营销公司 Beep Crypto 精心策划&#xff0c;将于 10 月 5 日至 10 月 17 日在广州潮流策展空间 YCC!天宜举行。 活动的主题围绕虚拟时尚展开&#xff0c;汇聚了一系列令人激动的展品&#xff0c;涵…

给列起别名(关键字:as)

MySQL从小白到总裁完整教程目录:https://blog.csdn.net/weixin_67859959/article/details/129334507?spm1001.2014.3001.5502 语法格式: select 列名1 as 别名1, 列名2 as 别名2, 列名n as 别名n from 表名; 说明&#xff1a;可以省略as&#xff0c;列名和别名之间使用空格…

SpringBoot整合Neo4j

一、前言 Neo4j是一个高性能的&#xff0c;NOSQL图形数据库&#xff0c;它的内部就是一个高性能的图形引擎&#xff0c;专门为应用程序提供嵌入式&#xff0c;磁盘的高性能存储和遍历图形结构的能力。Spring Boot是一个旨在简化创建独立的&#xff0c;生产级别的Spring基础应用…

mycat实现mysql读写分离

架构图&#xff1a; 视频地址

408计网应用层总结

网络应用模型 ■客户/服务器模型&#xff08;C/S&#xff09;&#xff1a;客户是服务请求方&#xff0c;服务器是服务提供方 ■P2P模型&#xff1a;各主机都是客户&#xff0c;也都是服务器&#xff08;任意一对计算机成称为对等方&#xff09; 注&#xff1a; 1.客户…

OpenMMLab【超级视客营】——支持InverseForm Loss(MMSegmentation的第三个PR)

文章目录 1. 任务目标1.1 issue1.2 原理相关资料&#xff08;论文讲解&#xff09;InverseFormSTN(Spatial Transformer Networks) 1.3 实现相关资料&#xff08;相关PR&#xff09; 2. 理解原理3. 代码实现3.X checklist3.0 Issue中的有效内容3.1 MMSegmentation支持multiple …