CD71.【C++ Dev】二叉树的三种非递归遍历方式

目录

1.知识回顾

2.前序遍历

分析

总结入栈的几种可能

循环的条件

代码

提交结果

3.中序遍历

分析

代码

提交结果

3.★后序遍历

分析

问题:如何确定是第一次访问到栈的元素还是第二次访问到栈中的元素?

方法1:使用填充的内存(依赖于架构)

判断计算机使用的架构和指针大小

代码

提交结果

方法2:寻找规律,设置一个prev变量记录上一个访问的节点

代码

运行结果

方法3:使用标记栈

代码

运行结果


1.知识回顾

106.【C语言】数据结构之二叉树的三种递归遍历方式

L17.【LeetCode题解】前序遍历

L18.【LeetCode题解】中序遍历和后序遍历

2.前序遍历

https://leetcode.cn/problems/binary-tree-preorder-traversal/

给你二叉树的根节点 root ,返回它节点值的 前序 遍历。

示例 1:

输入:root = [1,null,2,3]

输出:[1,2,3]

解释:

示例 2:

输入:root = [1,2,3,4,5,null,8,null,null,6,7,9]

输出:[1,2,4,5,6,7,3,8,9]

解释:

示例 3:

输入:root = []

输出:[]

示例 4:

输入:root = [1]

输出:[1]

提示:

  • 树中节点数目在范围 [0, 100]
  • -100 <= Node.val <= 100

进阶:递归算法很简单,你可以通过迭代算法完成吗?

分析

以这棵树[8,3,10,1,6,null,null,null,null,4,7]为例分析:

前序遍历必然最先访问左路节点,其次是左路节点的右子树,需要一直重复这个过程

那么可以分成两部分:

(红色是左路节点,蓝色是左路节点的右子树)

访问顺序:8→3→1→1的右子树→3的右子树→8的右子树

会发现:记录8→3→1遍历顺序 又要访问1、3、8的右子树(恰好是8、3、1倒过来的顺序)

可以利用栈:先访问左路节点(左路节点入前序遍历数组),后左路节点入栈(入栈的是节点的指针),访问左路节点的右子树需要使用栈顶元素,弹出的节点记录到前序遍历的数组中,而且需要使用一个指针cur从根节点开始遍历树

1. cur指向一个节点意味着访问一棵树的开始
2. 栈里面的节点意味着要访问它的右子树

树分成左路节点和右子树 右子树再分成左路节点和右子树 右子树再再分成左路节点和右子树……

先入左节点:

1的左子树为空,需要弹出栈顶元素,访问右节点

访问右子树前要先弹出左子树的所有节点:

1的左和右子树为NULL,直接弹出:

3的右子树不为NULL,弹出3后入右子树:

6的左子树为4,继续入栈:

4的左和右子树节点为NULL,直接弹出:

6的右子树不为NULL,弹出6后入右子树:

7的左和右子树节点为NULL,直接弹出:

8的右子树不为NULL,弹出8后入右子树:

10的左和右子树节点为NULL,直接弹出:

栈为空,循环结束

总结入栈的几种可能

1.栈顶的左右子树都为空,直接弹出

2.栈顶的左子树不为空,将左子树入栈

3.栈顶的右子树不为空,弹出栈顶后,将右子树入栈

//必然最先访问左路节点
while(cur)
{st.push(cur);cur=cur->left;
}//左路节点的右子树,需要取栈顶元素
auto elem=st.top();
st.pop();
ret.push_back(elem->val);
cur=elem->right;//子问题的方式访问右子树

循环的条件

显然栈不为空就继续循环,但如果只写这个条件会导致有些测试用例过不了

原因是1的左子树为空,将1出栈后,恰满足栈为空的循环退出条件,导致2和3没有访问到,需要添加cur如果不为空,也可以继续循环

而且"cur不为空"这个条件应该比"栈不为空"这个条件先判断,可以使用短路运算的规则:

while (cur || ! !st.empty())
{//......
}

代码

class Solution {
public:vector<int> preorderTraversal(TreeNode* root) {if (root==nullptr)return {};vector<int> ret;stack<TreeNode*> st;TreeNode* cur=root;while (cur || !st.empty()){//必然最先访问左路节点while(cur){st.push(cur);ret.push_back(cur->val);cur=cur->left;}//访问左路节点的右子树,需要取栈顶元素auto elem=st.top();st.pop();cur=elem->right;// 子问题的方式访问右子树} return ret;}
};

提交结果

3.中序遍历

https://leetcode.cn/problems/binary-tree-inorder-traversal/

给定一个二叉树的根节点 root ,返回 它的 中序 遍历

示例 1:

输入:root = [1,null,2,3]
输出:[1,3,2]

示例 2:

输入:root = []
输出:[]

示例 3:

输入:root = [1]
输出:[1]

提示:

  • 树中节点数目在范围 [0, 100]
  • -100 <= Node.val <= 100

进阶: 递归算法很简单,你可以通过迭代算法完成吗?

分析

中序遍历:左→根→右,即左子树访问完了才能访问根和右子树

反复执行这两步:

1. 左路节点入栈
2. 访问左路节点及左路节点的右子树

        从栈里面取到一个节点(pop)就意味这个节点左子树已经访问完了

稍微改改前序遍历的代码即可

代码

class Solution {
public:vector<int> inorderTraversal(TreeNode* root) {if (root==nullptr)return {};vector<int> ret;stack<TreeNode*> st;TreeNode* cur=root;while (cur || !st.empty()){//先遍历左子树,但不访问其中的节点,即不要push_back到ret中while(cur){st.push(cur);cur=cur->left;}//左子树遍历完了,那就访问根和右子树auto elem=st.top();st.pop();ret.push_back(elem->val);cur=elem->right;// 子问题的方式访问右子树} return ret;}
};

提交结果

3.★后序遍历

https://leetcode.cn/problems/binary-tree-postorder-traversal/

给你一棵二叉树的根节点 root ,返回其节点值的 后序遍历

示例 1:

输入:root = [1,null,2,3]

输出:[3,2,1]

解释:

示例 2:

输入:root = [1,2,3,4,5,null,8,null,null,6,7,9]

输出:[4,6,7,5,2,9,8,3,1]

解释:

示例 3:

输入:root = []

输出:[]

示例 4:

输入:root = [1]

输出:[1]

提示:

  • 树中节点的数目在范围 [0, 100]
  • -100 <= Node.val <= 100

进阶:递归算法很简单,你可以通过迭代算法完成吗?

分析

后序遍历:左→右→根,即必须访问完左子树和右子树才能访问根

以这棵树[8,3,10,1,6]为例分析:

仍然分成两部分:

(红色是左路节点,蓝色是左路节点的右子树)

左路节点入栈:

1的左和右子树为NULL,直接弹出1,将1记录到后序遍历的数组中:

虽然3的右子树不为空,但是不能弹出3,如果弹出3那就成前序遍历了,应该等第二次访问到栈里面的3才弹出3(即将3的左右子树都访问完才能访问根节点3)

入3的右子树

6的左和右子树为NULL,直接弹出6,将6记录到后序遍历的数组中:

第二次遇到3,因为3的左右子树都访问完了,可以弹出3,将3记录到后序遍历的数组中:

虽然8的右子树不为空,但是不能弹出8(理由见上),入8的右子树:

10的左和右子树为NULL,直接弹出10,将10记录到后序遍历的数组中:

第二次遇到8,因为8的左右子树都访问完了,可以弹出8,将8记录到后序遍历的数组中:

栈为空,循环结束

问题:如何确定是第一次访问到栈的元素还是第二次访问到栈中的元素?

上面提到了:"虽然3的右子树不为空,但是不能弹出3,如果弹出3那就成前序遍历了,应该等第二次访问到栈里面的3才弹出3(即将3的左右子树都访问完才能访问根节点3)"

方法1:使用填充的内存(依赖于架构)

得知题目条件"树中节点的数目在范围 [-100, 100] 内",而且TreeNode的val是用int存储的,如果val>0,那么最多只会占用int的低7位,如果val<0,那么int所有位都会占用

显然在val内部使用一个标志位来判断是第一次访问到栈的元素还是第二次访问到栈中的元素是不切实际的

突破口:结构体的内存对齐

64位下,TreeNode结构体的内存分布图:

会发现0x0004~0x007是填充的4个字节,没有用处!

填充的字节可以借用其中一个字节来标记是第一次还是第二次访问到栈中的元素,简称为标志字节

例如选这一字节:

怎么从标志字节哪里得知是第一次还是第二次访问到栈中的元素呢?

对于填充的字节,编译器会给初始值,LeetCode下的值是0xBE,Windows VS2022下的值是0xCD,下面padding_byte获取到的就是初始字节(pad v.填充)

unsigned char padding_byte = *((unsigned char*)&(root->val) + 4);

那么第一次访问过节点后可以设置标志字节为指定值,让其不等于原来编译器给的初始值就行了,例如可以设置成0x01,等到第二次访问发现是0x01,就能让栈顶元素出栈,写入到后序遍历的数组中

如果LeetCode是64位架构且没有使用gcc的-m32选项,那么上述方法是可行的,可以用宏判断:

判断计算机使用的架构和指针大小
#if defined(__x86_64__) || defined(_M_X64)printf("This is a 64-bit x86 architecture\n");
#elif defined(__i386__)printf("This is a 32-bit x86 architecture\n");
#elif defined(__arm__) || defined(_M_ARM)printf("This is an ARM architecture\n");
#elif defined(__aarch64__) || defined(_M_AARCH64)printf("This is a 64-bit ARM architecture\n");
#elseprintf("Unknown architecture\n");
#endif//__SIZEOF_POINTER__是指针大小
if (__SIZEOF_POINTER__ == 4) 
{printf("This is a 32-bit architecture.\n");
} 
else if (__SIZEOF_POINTER__ == 8) 
{   printf("This is a 64-bit architecture.\n"); 
} 
else 
{printf("Unknown architecture.\n");
}

LeetCode输出结果:

使用填充的内存的方法是可行的

代码
class Solution {
public:vector<int> postorderTraversal(TreeNode* root){if (root == nullptr)return {};vector<int> ret;unsigned char padding_byte=*((unsigned char*)&(root->val)+4);stack<TreeNode*> st;TreeNode* cur = root;while (cur || !st.empty()){//先遍历左子树,但不访问其中的节点,即不要push_back到ret中while (cur){st.push(cur);cur = cur->left;}//左子树遍历完了,那就访问根和右子树auto elem = st.top();unsigned char* flag_byte_ptr=(unsigned char*)&(elem->val)+4;if (elem->right == nullptr){ret.push_back(elem->val);st.pop();continue;}if (*flag_byte_ptr!=padding_byte){//第二次访问到,说明左右子树都访问完了,必须重新设置curst.pop();ret.push_back(elem->val);continue;}else{//第一次访问到,要设置标志字节*flag_byte_ptr=1;}cur = elem->right;// 子问题的方式访问右子树}return ret;}
};
提交结果

方法2:寻找规律,设置一个prev变量记录上一个访问的节点

访问右子树有两种情况:右为空(不用管右直接拿出栈顶元素),右不为空(需要使用上一次访问的节点)

例如下图的节点的3会访问到两次:

由方法1的栈图分析

1.如果3的右子树没有访问过,上一次访问的是3的左子树的根1

↓ 

2..如果3的右子树访问过,上一次访问的是3的右子树的根6

↓ 

设置一个变量prev记录cur访问的前一个节点即可

代码
class Solution {
public:vector<int> postorderTraversal(TreeNode* root){if (root == nullptr)return {};vector<int> ret;stack<TreeNode*> st;TreeNode* cur = root;TreeNode* prev = nullptr;while (cur || !st.empty()){//先遍历左子树,但不访问其中的节点,即不要push_back到ret中while (cur){st.push(cur);cur = cur->left;}//左子树遍历完了,那就访问根和右子树auto elem = st.top();if (elem->right == nullptr){prev=elem;ret.push_back(elem->val);st.pop();}if (prev==elem->right){//第二次访问到,说明左右子树都访问完了,必须重新设置curprev=elem;st.pop();ret.push_back(elem->val);}else//prev=cur->left{cur = elem->right;// 第一次访问到,子问题的方式访问右子树}   }return ret;}
};
运行结果

方法3:使用标记栈

标记栈的作用是标记对应的节点有没有访问过,是否访问过这个节点就看标记栈的栈顶值

代码
class Solution {
public:vector<int> postorderTraversal(TreeNode* root){if (root == nullptr)return {};vector<int> ret;stack<TreeNode*> st;stack<TreeNode*> flag;TreeNode* cur = root;while (cur || !st.empty()){//先遍历左子树,但不访问其中的节点,即不要push_back到ret中while (cur){st.push(cur);cur = cur->left;}//左子树遍历完了,那就访问根和右子树auto elem = st.top();if (elem->right == nullptr){ret.push_back(elem->val);st.pop();continue;}if (flag.size()&&flag.top()==elem){//第二次访问到,说明左右子树都访问完了,必须重新设置curst.pop();flag.pop();ret.push_back(elem->val);continue;}if (flag.empty() || flag.top()!=elem){//第一次访问到,要设置标志字节flag.push(elem);}cur = elem->right;// 子问题的方式访问右子树}return ret;}
};
运行结果

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

音视频学习(五十九):H264中的SPS

在 H.264 (也称为 AVC, Advanced Video Coding) 视频编码标准中&#xff0c;SPS (Sequence Parameter Set) 是一个至关重要的 NALU (Network Abstraction Layer Unit) 类型&#xff0c;它承载着整个视频序列共有的全局性配置信息。你可以把它理解为视频文件的“基因”&#xff…

linux实时性研究

Linux 实时性研究旨在提升 Linux 系统对外部事件的响应速度和确定性,使其能够满足实时应用的需求。以下是关于 Linux 实时性研究的一些关键内容: Linux 实时性不足的原因 中断优先级问题:在标准 Linux 内核中,中断具有最高优先级,包括软中断,这使得实时任务的优先级得不到…

Java-面试八股文-Mysql篇

MySQL篇 1、Select 语句完整的执行顺序 难度系数&#xff1a;⭐&#x1f4cc; SQL SELECT 语句书写顺序&#xff08;开发者写的顺序&#xff09; SELECT ... FROM ... JOIN ... WHERE ... GROUP BY ... HAVING ... ORDER BY ... LIMIT ...&#x1f4cc; 实际执行顺序&#…

多代理系统架构:Supervisor 与 Swarm 架构详解

多代理&#xff08;Multi-Agent&#xff09;系统正成为构建复杂 AI 应用的重要范式。本文将深入剖析两种热门的多代理架构模式——Supervisor&#xff08;主管模式&#xff09;与 Swarm&#xff08;群智模式&#xff09;&#xff0c;揭示它们的执行流程、适用场景及实现细节&am…

【深度学习】思维链(Chain of Thought, CoT):提升大模型推理能力的关键技术

思维链&#xff08;Chain of Thought, CoT&#xff09;&#xff1a;提升大模型推理能力的关键技术 文章目录思维链&#xff08;Chain of Thought, CoT&#xff09;&#xff1a;提升大模型推理能力的关键技术1 什么是思维链&#xff08;Chain of Thought, CoT&#xff09;&#…

GitHub 宕机自救指南:打造韧性开发体系

一、引言1.1 GitHub 宕机事件回顾与影响剖析在软件开发的广袤版图中&#xff0c;GitHub 宛如一座熠熠生辉的灯塔&#xff0c;为全球超 1 亿开发者照亮前行之路&#xff0c;其重要性不言而喻。它集代码托管、版本控制、协作开发以及项目管理等核心功能于一身&#xff0c;是无数开…

移动端网页调试实战,iOS WebKit Debug Proxy 的应用与替代方案

在移动端开发中&#xff0c;iOS WebView 的调试一直是个难题。不同于 Android 可以依赖 Chrome DevTools 和 ADB&#xff0c;iOS 的 WKWebView 只能通过 Safari 开发者工具调试&#xff0c;而这需要 Mac 环境和设备直连。为了弥补限制&#xff0c;社区出现了一个常用工具 —— …

焕新升级,Sermant 2.0.0 release版本重磅发布!

Sermant社区在6月底正式发布了2.0.0 release版本&#xff0c;这次更新中&#xff0c;Sermant进行了项目所属组织调整并新增了基于xDS协议的服务发现能力、预过滤启动加速机制、Sermant Backend的配置管理能力。所属组织调整使得Sermant淡化厂商属性&#xff0c;以全新的姿态更好…

sqli-labs通关笔记-第28a关GET字符注入(多重关键字过滤绕过 脚本法)

目录 一、sqlmap之tamper脚本 二、源码分析 1、代码审计 2、SQL安全性分析 三、渗透实战 1、进入靶场 2、tamper脚本 3、sqlmap渗透 SQLI-LABS 是一个专门为学习和练习 SQL 注入技术而设计的开源靶场环境&#xff0c;本小节对第28a关Less 28a基于GET字符型的SQL注入关卡…

联想打印机2268w安装

联想打印机2268w是支持无线打印的。在某度搜索&#xff0c;掀起盖子长按开机键&#xff0c;成功初始化。之后按说明应该能用手机搜索到打印机的热点&#xff0c;反复搜索都没有出现。最后没办法&#xff0c;之后好用我自己的方法安装。找了个笔记本&#xff0c;开机连接到wifi,…

【LeetCode】动态规划——72.编辑距离、10.正则表达式匹配

LeetCode题目链接 https://leetcode.cn/problems/edit-distance/description/ https://leetcode.cn/problems/regular-expression-matching/description/ 题解 72.编辑距离 本题要定义为长度为i、长度为j的字符串的最少编辑次数&#xff0c;每次判断字符的下标为i-1、j-1。dp[i…

[亲测可用]Android studio配置国内镜像源 Kotlin DSL (build.gradle.kts)

一、更改gradle下载镜像Android studio项目需要下载和更新 Gradle 及其依赖。由于网络环境&#xff0c;直接从 Gradle 官网下载可能会遇到速度慢或超时的问题。这里需要更换为使用国内的镜像站点来加速下载。官网地址&#xff08;较慢&#xff09;&#xff1a;https://services…

《跳出“技术堆砌”陷阱,构建可演进的软件系统》

很多团队陷入了“技术焦虑式开发”—盲目追逐热门框架&#xff0c;将“使用微服务”“引入云原生”“集成AI组件”当作架构先进的标签&#xff0c;却忽视了业务与技术的底层匹配逻辑。某互联网团队为了“彰显技术实力”&#xff0c;在内部协同工具中强行接入机器学习推荐模块&a…

赋能你的应用:英超实时数据接入终极指南(API vs. WebSocket)

在当今数据驱动的时代&#xff0c;为您的应用程序注入实时、准确的英超赛事数据&#xff0c;是提升用户体验、打造差异化竞争力的关键。无论是开发一款球迷必备的比分追踪App&#xff0c;一个深度专业的赛事分析平台&#xff0c;还是一个充满互动性的梦幻足球游戏&#xff0c;首…

计算机网络:(poll、epoll)

一、select的不足1. 最大监听数受限&#xff1a;FD_SETSIZE 默认 1024&#xff08;Linux&#xff09;2. 每次调用需重置 fd_set&#xff1a;内核会修改集合&#xff0c;必须每次重新 FD_SET3. 用户态与内核态拷贝开销大4. 返回后仍需遍历所有 fd 才能知道哪个就绪5. 效率随 fd …

网络编程之设置端口复用

首先来说一下为什么要设置端口复用&#xff0c;有些时候在调试服务器代码时势必会经常启动或结束服务器进程&#xff0c;这样就会出现当再次启动服务器时有可能会出现端口绑定失败的情况&#xff0c;造成这个情况的原因是由于你上次关闭服务器时有连接尚未断开等等其他原因&…

stargo缩扩容starrocks集群,实现节点服务器替换

1.背景在企业中可能需要&#xff0c;将starrocks的某一台服务器下架&#xff0c;换上另一台服务器&#xff0c;如何实现这个操作&#xff0c;本篇将进行介绍&#xff1b;节点hadoop101hadoop102hadoop103hadoop104集群原集群节点新节点fe✔✔❌&#xff08;下线&#xff09;✔&…

Linux -- 进程间通信【命名管道】

目录 一、命名管道定义 二、命名管道创建 1、指令 2、系统调用 3、删除 三、匿名管道和命名管道的区别 四、命名管道的打开规则 五、代码示例 1、comm.hpp 2、server.cc 3、client.cc 一、命名管道定义 # 匿名管道存在以下核心限制&#xff1a; 仅限亲缘关系进程&a…

LinuxC系统多线程程序设计

一.多线程程序设计1. 线程概述&#xff1a;1.1 什么是线程?线程是进程中的一个实体(组成单元),是系统进程调度的最小单元。一个进程至少具有一个线程&#xff0c;如果进程仅有一个线程&#xff0c;该线程就代表进程本身。把代表进程本身的线程称为主线程&#xff0c;一个进程…

Vue3 + TS + MapboxGL.js 三维地图开发项目

文章目录 1. 安装依赖 2. 新建 Map 组件(components/MapView.vue) 3. 在页面中使用(views/Home.vue) 4. 效果说明 1. 安装依赖 npm install mapbox-gl @types/mapbox-gl --save⚠️ 注意:需要去 Mapbox 官网,申请一个 access token。 package.json {"name":…