二叉树遍历(前中后序的递归/非递归遍历、层序遍历)

二叉树的遍历

1. 二叉树的前序、中序、后序遍历

  • 前、中、后序遍历又叫深度优先遍历
    • 注:严格来说,深度优先遍历是先访问当前节点再继续递归访问,因此,只有前序遍历是严格意义上的深度优先遍历
  • 首先需要知道下面几点:
  1. 任何一颗二叉树,都由根节点、左子树、右子树构成。

​ 如图:

  1. 分治算法:分而治之。大问题分成类似的子问题,子问题再分成子问题……直到子问题不能再分割。对树也可以做类似的处理,对一棵树不断地分割,直到子树为空时,分割停止。
  2. 关于二叉树的许多问题我们都要利用分治思想,将一棵完整的树分解成根和左右两棵子树,然后再对这两棵子树进行相同的递归处理,最后得到结果。
  3. 如果对递归的过程想的不太清楚,建议画递归展开图来辅助理解

1.1 前序(先根)遍历

  • 遍历顺序:根- > 左子树 -> 右子树(即先访问根,再访问左子树,最后在访问右子树)

  • 如上图中:A -> B -> C -> NULL -> NULL -> D -> NULL -> NULL -> E -> NULL -> NULL

typedef char BTDataType;
typedef struct BinaryTreeNode
{
   struct BinaryTreeNode *left;	//指向左子树
   struct BinaryTreeNode *right;	//指向右子树
    BTDataType data;
}BTNode;

void PrevOrder(BTNode * root)	//前序遍历
{
   if (!root)
       return;
   
   printf("%d ",root->data);	//根
   PrevOrder(root->left);		//左子树
   PrevOrder(root->right);		//右子树
   return;
}

递归顺序图:

在这里插入图片描述

递归展开图:

1.2 中序(中根)遍历

  • 遍历顺序:左子树 -> 根 -> 右子树(即先访问左子树,再访问根,最后在访问右子树)

  • 如上图中:NULL -> C -> NULL -> B -> NULL -> D -> NULL -> A -> NULL -> E -> NULL

void InOrder(BTNode* root)
{
   if (!root)
   	return;
   InOrder(root->left);	//左子树
   printf("%c ", root->data);		//根
   InOrder(root->right);	//右子树
   return;
}

递归顺序图:

在这里插入图片描述

1.3 后序(后根)遍历

  • 遍历顺序:左子树 -> 右子树 -> 根(即先访问左子树,再访问左=右子树,最后在访问根)

  • 如上图中:NULL -> NULL -> C -> NULL -> NULL -> D -> B -> NULL -> NULL -> E -> A

void PostOrder(BTNode* root)
{
   if (!root)
   {
   	printf("NULL ");
   	return;
   }
   PostOrder(root->left);	//左子树
   PostOrder(root->right);		//右子树
   printf("%c ", root->data);		//根
}

递归顺序图:

在这里插入图片描述

2. 二叉树前中后序的非递归遍历

  • 在利用递归来进行二叉树的前中后序遍历时,我们通常将一棵二叉树看成三部分:根、左子树、右子树
  • 但是对于前中后序的非递归遍历,我们需要转变思路:应当将一棵二叉树看成两部分:左路节点、左路节点的右子树

在这里插入图片描述

2.1 前序非递归

前序遍历的顺序是:根 -> 左子树 -> 右子树

具体到一棵二叉树,就是自顶向下将左路节点遍历完,再自底向上遍历左路节点的右子树。如图:

在这里插入图片描述

为了能够在遍历完左路节点后还能得到这个节点从而得到这个节点的右子树,我们需要利用栈来对左路节点进行存储

实现代码:

class Solution {
public:
    vector<int> preorderTraversal(TreeNode* root) {
        vector<int> ret;
        stack<TreeNode*> st;

        TreeNode* cur = root;
        while (cur || !st.empty())
        {
            //遍历左路节点,将左路节点的值打印的同时将节点入栈
            while (cur)
            {
                ret.push_back(cur->val);
                st.push(cur);

                cur = cur->left;
            }

            TreeNode* tmp = st.top();
            st.pop();
			
            //此时cur即为左路节点的右子树
            //将这棵右子树看成一颗完整的二叉树,进行相同的操作
            cur = tmp->right;
        }
        
        return ret;
    }
};

2.2 中序非递归

中序遍历的顺序是:左子树 -> 根 -> 右子树

具体到一棵二叉树,即从左路节点的最深处开始,先遍历这个节点,再遍历这个节点的右子树,自底向上。

在这里插入图片描述

同样,为了能够找到之前的左路节点,也需要一个栈来对左路节点进行保存

实现代码:

class Solution {
public:
    vector<int> inorderTraversal(TreeNode* root) {
        vector<int> ret;
        stack<TreeNode*> st;

        TreeNode* cur = root;
        while (cur || !st.empty())
        {
            //遍历左路节点的时候将左路节点入栈
            //由于左子树先于根,因此先不要打印左路节点(根)的值
            while (cur)
            {
                st.push(cur);
                cur = cur->left;
            }
			
            //程序第一次走到这里时,tmp就是左路节点的最深处
            //tmp的左子树为nullptr(或已经遍历完tmp的左子树),因此打印其(根)值
            TreeNode* tmp = st.top();
            st.pop();
            ret.push_back(tmp->val);
            
            //遍历左路节点的右子树
            cur = tmp->right;
        }
        
        return ret;
    }
};

2.3 后序非递归

后序的遍历顺序为:左子树 -> 右子树 -> 根

具体到一棵二叉树,即从左路节点的最深处开始,先遍历左路节点的右子树,再遍历左路节点,自底向上。如图:

在这里插入图片描述

同样,也需要一个栈来保存之前的左路节点

此外,由于后序遍历的顺序为:左子树 -> 右子树 -> 根,需要遍历完根(左路节点)的左子树和右子树后才能对其值进行打印,在这个过程中,我们会经过两次根,且只能在第二次经过根时才能打印根的值,为了确保我们打印根的时机,可以利用一个指针prev来记录之前遍历的位置如果prev停留在左路节点的右子树的根节点,就说明此时左右子树已经遍历完,可以打印根的值

实现代码:

class Solution {
public:
    vector<int> postorderTraversal(TreeNode* root) {
        vector<int> ret;
        stack<TreeNode*> st;

        TreeNode* cur = root;
        TreeNode* prev = nullptr;

        while (cur || !st.empty())
        {
            //将左路节点入栈
            while (cur)
            {
                st.push(cur);
                cur = cur->left;
            }

            TreeNode* tmp = st.top();
            
            //如果左路节点的右子树为空或者prev停留在右子树的根
            //说明根的左子树和右子树都已遍历完
            //打印根值(遍历根),同时跟新prev的位置
            if (tmp->right == nullptr || prev == tmp->right)
            {
                ret.push_back(tmp->val);
                st.pop();

                prev = tmp;
            }
            else	//否则,说明根的右子树没有遍历完,遍历右子树
                cur = tmp->right;
        }
        
        return ret;
    }
};

2. 二叉树的层序遍历

  • 层序遍历又叫广度优先遍历

  • 设二叉树的根节点所在层数为1,层序遍历就是从根节点出发,首先访问第一层的节点,然后从左到右访问第二层上的节点,接着访问第三层的节点,以此类推,自上而下,自左往右逐层访问树的结点的过程就是层序遍历

  • 层序遍历借助队列的先进先出思想来实现

  • 核心思想:上一层带下一层

  • 如图就是对上面那棵树的层序遍历示意图:

    在这里插入图片描述

  • 实现代码

typedef BTNode* QDataType;		//队列元素类型
typedef struct QueueNode
{
   struct QueueNode* next;
   QDataType data;
}QueueNode;
typedef struct Queue	//定义存放指向队头,队尾指针的结构体
{
   QueueNode* head;	//指向队头
   QueueNode* tail;	//指向队尾
}Queue;

//层序遍历
void LevelOrder(BTNode* root)		
{
   Queue *q = (Queue *)malloc(sizeof(Queue));	//创建队列
   QueueInit(q);	//初始化队列
   
   //如果根节点为空,直接退出函数
   if (!root)
   	return;
   
   QueuePush(q, root);		//先将根节点入队入队
   while (!QueueEmpty(q))		//当队列不为空
   {
   	BTNode* front = QueueFront(q);		//接收队头元素
   	QueuePop(q);		//出队头元素
       
   	printf("%c ", front->data);	//访问该节点
       
   	if (front->left)		//如果左子树不为空
   		QueuePush(q, front->left);			//左子树入队
   	if (front->right)		//如果右子树不为空
   		QueuePush(q, front->right);		//右子树入队
   }
   
   printf("\n");
   QueueDestroy(q);		//销毁队列	
}

3. 二叉树遍历的应用

在这里插入图片描述

  • 由二叉树和层序遍历的思想,我们可以构造出这棵树

  • 再有前序遍历 根- > 左子树 -> 右子树 的思想,可以知道,这棵树的前序序列为:A B D H E C F G

在这里插入图片描述

  • 这道题是由二叉树的前序序列和中序序列来确定二叉树,我们知道中序遍历的思想是 左子树 -> 根 -> 右子树 ,根将左子树和右子树分割开来,那么我们就可以先用前序序列确定根,再用中序序列确定根的左右子树,这样就可以将这棵二叉树确定了,如图:

在这里插入图片描述

  • 显然根节点为E

我们同样可以用代码,利用一棵二叉树的前序序列和中序序列来将这棵二叉树还原👉Leetcode -> 从前序与中序遍历序列构造二叉树

class Solution {
public:
    //前序序列即为根序列
    //在中序序列找到根后可以将序列分为左右两部分,这两部分分别就是跟的左子树序列和右子树序列
    TreeNode* _buildTree(vector<int>& preorder, vector<int>& inorder, int& prei, int inBegin, int inEnd)
    {
        //区间长度小于1,直接返回空
        if (inBegin > inEnd)
            return nullptr;

        //前序序列即为根序列
        TreeNode* root = new TreeNode(preorder[prei++]);

        //在中序序列中找到根
        int pos = 0;
        while (1)
        {
            if (root->val != inorder[pos])
                pos++;
            else
                break;
        }

        //分别前往左子树和右子树进行连接
        root->left = _buildTree(preorder, inorder, prei, inBegin, pos - 1);
        root->right = _buildTree(preorder, inorder, prei, pos + 1, inEnd);

        return root;
    }
    TreeNode* buildTree(vector<int>& preorder, vector<int>& inorder) {
        int i = 0;
        TreeNode* root = _buildTree(preorder, inorder, i, 0, inorder.size() - 1);

        return root;
    }
};

在这里插入图片描述

  • 这题和第二题类似,同样是先由后序遍历(左子树 -> 右子树 -> 根)确定根节点,再由中序遍历确定根的左右子树,只是用后序遍历确定根节点时要从最后开始。如图:

在这里插入图片描述

  • 易得前序遍历为a b c d e

  • 我们同样可以用代码,利用一棵二叉树的前序序列和中序序列来将这棵二叉树还原👉Leetcode -> 从中序与后序遍历序列构造二叉树

class Solution {
public:
    //思想同前序序列和中序序列确定二叉树类似
    TreeNode* _buildTree(vector<int>& inorder, vector<int>& postorder, int& posi, int inBegin, int inEnd)
    {
        if (inBegin > inEnd)
            return nullptr;

        TreeNode* root = new TreeNode(postorder[posi--]);

        int pos = 0;
        while (1)
		        {
            if (root->val != inorder[pos])
                pos++;
            else
                break;
        }

        root->right = _buildTree(inorder, postorder, posi, pos + 1, inEnd);
        root->left = _buildTree(inorder, postorder, posi, inBegin, pos - 1);

		        return root;
    }
    TreeNode* buildTree(vector<int>& inorder, vector<int>& postorder) {
        int i = postorder.size() - 1;
        TreeNode* root = _buildTree(inorder, postorder, i, 0, inorder.size() - 1);

        return root;
    }
};

总结:

由于二叉树的中序遍历可以分割二叉树的左右节点,因此 前序序列 + 中序序列 / 后序序列 + 中序序列 都可以构建出一棵二叉树,而单独序列和 前序序列 + 后序序列就不行。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/443930.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

Spring学习 基础(三)MVC

5、Spring MVC 传统Web模式&#xff1a; Model:系统涉及的数据&#xff0c;也就是 dao 和 bean。View&#xff1a;展示模型中的数据&#xff0c;只是用来展示。Controller&#xff1a;处理用户请求都发送给 &#xff0c;返回数据给 JSP 并展示给用户。 随着 Spring 轻量级开发…

Vue项目实战-空间论坛(2)

项目实战 实现userlist页面 获取userlist列表&#xff0c;可使用ajax,axios 实现 这里采用ajax实现&#xff0c;需要添加Jquery依赖&#xff0c;然后在UserListView.vue中引入 在UserListView.vue组件的入口函数中定义users变量&#xff0c;并引入ref 使用ajax从云端动…

目标检测——监控下打架检测数据集

一、简述 首先&#xff0c;监控下打架检测是维护公共安全的重要手段。在公共场所、学校、监狱等地方&#xff0c;打架事件往往难以避免。通过安装打架检测监控系统&#xff0c;可以实时监控并准确识别打架事件&#xff0c;及时采取必要的应对措施&#xff0c;有效地减少打架事…

手写分布式配置中心(五)整合springboot(不自动刷新的)

springboot中使用配置方式有四种&#xff0c;分别是environment、BeanDefinition、Value、ConfigurationProperties。具体的原理可以看我之前的一篇文章https://blog.csdn.net/cjc000/article/details/132800290。代码在https://gitee.com/summer-cat001/config-center 原理 …

PTA L2-004 这是二叉搜索树吗?

一棵二叉搜索树可被递归地定义为具有下列性质的二叉树&#xff1a;对于任一结点&#xff0c; 其左子树中所有结点的键值小于该结点的键值&#xff1b;其右子树中所有结点的键值大于等于该结点的键值&#xff1b;其左右子树都是二叉搜索树。 所谓二叉搜索树的“镜像”&#xf…

减少PDF文件大小的方法,亲测巨好用!!!

周六晚上&#xff0c;导师突然发了两个pdf&#xff0c;让把大小改成1M以下&#xff01;&#xff01;&#xff01; 试了很多方法最后&#xff0c;发现了个最好使用的&#xff0c;不过需要下载下Adobe Acrobat文件编辑软件&#xff0c;下载地址如下 链接&#xff1a;https://pan.…

基于Java的开放实验室管理系统(Vue.js+SpringBoot)

目录 一、摘要1.1 项目介绍1.2 项目录屏 二、研究内容2.1 实验室类型模块2.2 实验室模块2.3 实验管理模块2.4 实验设备模块2.5 实验订单模块 三、系统设计3.1 用例设计3.2 数据库设计 四、系统展示五、样例代码5.1 查询实验室设备5.2 实验放号5.3 实验预定 六、免责说明 一、摘…

LVS+Keepalived 高可用集群

一、Keepalived工具介绍 支持故障自动切换(Failover) 支持节点健康状态检查(Health Checking) 基于vrrp协议完成地址流动 为vip地址所在的节点生成ipvs规则(在配置文件中预先定义) 为ipvs集群的各RS做健康状态检测 基于脚本调用接口完成脚本中定义的功能&#xff0c;进而…

链表|19.删除链表的倒数第N个节点

力扣题目链接 struct ListNode* removeNthFromEnd(struct ListNode* head, int n) {//定义虚拟头节点dummy 并初始化使其指向headstruct ListNode* dummy malloc(sizeof(struct ListNode));dummy->val 0;dummy->next head;//定义 fast slow 双指针struct ListNode* f…

springboot整合shiro的实战教程(一)

文章目录 1.权限的管理1.1 什么是权限管理1.2 什么是身份认证1.3 什么是授权 2.什么是shiro3.shiro的核心架构3.1 Subject3.2 SecurityManager3.3 Authenticator3.4 Authorizer3.5 Realm3.6 SessionManager3.7 SessionDAO3.8 CacheManager3.9 Cryptography 4. shiro中的认证4.1…

Cocos Creator 2d光照

godot游戏引擎是有2d光照的&#xff0c;用起来感觉还是很强大的&#xff0c;不知道他是怎么搞的&#xff0c;有时间看看他们怎么实现的。 之前一直以为cocos社区里面没有2d光照的实现&#xff0c;偶然看到2d实现的具体逻辑&#xff0c;现在整理如下&#xff0c; 一&#xff1…

CAS 登出方案

1.配置 CAS 服务器端 添加配置cas.logout.followServiceRedirects:true&#xff0c;使支持 CAS 退出时支持输入 service 参数为跳转路径 2.配置客户端服务,添加session清除操作 3.前端文件添加跳转重定向 1) 直接在客户端调用http请求/cas/logout去注销不能携带cookie信息, 无…

Jmeter 性能 —— 模拟百万高并发压测思路!

测试场景&#xff1a;模拟百万级的订单量一个物流信息的查询接口。 条件&#xff1a;接口响应时间<150ms以内。10万并发量每秒。 设计性能测试方案&#xff1a; 1、生产环境 10W/S --并发量&#xff08;架构师/技术负责人提供&#xff09;20台机器&#xff08;4G*4核配置…

【探索C++容器:set和map的使用】

[本节目标] 1. 关联式容器 2. 键值对 3. 树形结构的关联式容器 1. 关联式容器 在初阶阶段&#xff0c;我们已经接触过STL中的部分容器&#xff0c;比如&#xff1a;vector、list、deque、forward_list(C11)等&#xff0c;这些容器统称为序列式容器&#xff0c;因为其底层为…

第三百八十九回

文章目录 1. 概念介绍2. 使用方法2.1 获取所有时区2.2 转换时区时间 3. 示例代码4. 内容总结 我们在上一章回中介绍了"分享一些好的Flutter站点"相关的内容&#xff0c;本章回中将介绍timezone包.闲话休提&#xff0c;让我们一起Talk Flutter吧。 1. 概念介绍 我们在…

qsort函数的用法及参数的讲解

第一种用法展示&#xff1a;&#xff08;整形数组的qsort&#xff09; 一&#xff0c;qsort函数的定义&#xff1a; qsort 函数的定义&#xff1a;void qsort (void* base, size_t num, size_t size, int (*compar)(const void*,const void*)); 使用其需要包含头文件&#x…

Echarts 报提示 There is a chart instance already initialized on the dom.

问题原因&#xff1a; 每次执行 Echarts图例方法都会拿到相关的dom元素执行Echarts图例初始化操作 但是每次执行的时候拿到的dom元素又是相同的&#xff0c;Echarts初始化执行的时候检查到这个dom上面已经有了一个 图表了 就不会再重新拿到这个dom元素执行初始化操作 解决方案&…

CSRF攻击解析:原理、防御与应对策略

&#x1f90d; 前端开发工程师、技术日更博主、已过CET6 &#x1f368; 阿珊和她的猫_CSDN博客专家、23年度博客之星前端领域TOP1 &#x1f560; 牛客高级专题作者、打造专栏《前端面试必备》 、《2024面试高频手撕题》 &#x1f35a; 蓝桥云课签约作者、上架课程《Vue.js 和 E…

【蓝牙协议栈】【经典蓝牙】【BLE蓝牙】蓝牙协议规范(射频、基带链路控制、链路管理)

目录 1. 蓝牙协议规范&#xff08;射频、基带链路控制、链路管理&#xff09; 1.1 射频协议 1.2 基带与链路控制协议 1.3 链路管理器 1. 蓝牙协议规范&#xff08;射频、基带链路控制、链路管理&#xff09; 蓝牙协议是蓝牙设备间交换信息所应该遵守的规则。与开放系…

【数据库】数据库学习使用总结DDL DML 及常用的条件查询语句

目录 一、数据库介绍 二、数据库系统 DBMS&#xff1a; 三、DDL 1、操作数据库&#xff08;创建和删除&#xff09; 创建表 ——也可以利用navicat等工具直接创建 删除表 2、约束&#xff1a; 主键约束 唯一的标识一条数据&#xff0c;该字段的数据不允许重复 主键不可…