二叉树非递归版的后序遍历算法
本公众号主要推送关于对算法的思考以及应用的消息。算法思想说来有,分而治之,搜索,动态规划,回溯,贪心等,结合这些思想再去思考如今很火的大数据,云计算和机器学习,是不是也别有一番风味呢? 在这个征程中,免不了读英文博客,paper,书籍等,提升英语阅读能力也至关重要呀,为了满足大家需要,本公众号也推送这方面的消息。
01—你会学到什么?
树的递归遍历算法很容易理解,代码也很精简,但是如果想要从本质上理解二叉树常用的三种遍历方法,还得要思考树的非递归遍历算法。
读完后的收获:
- “”将学到二叉树的后序遍历的非递归版本
- 明白栈这种数据结构该怎么使用
02—讨论的问题是什么?
主要讨论二叉树的非递归版后序遍历该如何实现,包括借助什么样的数据结构,迭代的构思过程等。
03—相关的概念和理论
- 遍历
Traversal 指沿着某条搜索路线,依次对树中每个结点均做一次且仅做一次访问。访问结点所做的操作依赖于具体的应用问题。
- 二叉树组成
二叉树由根结点及左、右子树这三个基本部分组成。
- 后序遍历
Postorder Traversal 访问根结点的操作发生在遍历其左、右子树之后。
04—思考过程
后序遍历是从左子树,再到右子树,最后到根节点的遍历次序。如果借助栈来实现,我们自然想到先推入根节点,再推入右子树,最后推入左子树,然后出栈的顺序便是与推入顺序相反的。
那么这个问题,岂不是非常简单,是的,如果这棵树的左子树仅包括一个节点,右子树也仅仅包括一个节点,就像下图这样,
很显然,我们这样考虑树结构,思维是很局限的,我们要考虑的是节点2下还重复以上结构,节点3当然也是如此,每个节点都有可能重复这种结构,或仅有左右孩子之一,或都没有也就是叶子。
那么,如此递归结构,该如何思考写出非递归算法呢?
A 我们还是坚持从定义出发,不管一颗多么复杂的二叉树,在后序遍历中,一定先遍历的节点都位于节点2这一枝上,如图所示,所以我们会沿着2这一枝往下遍历,一直找,一直入栈,直到找到一个没有左孩子的节点,假设为节点2;
B 接下来要看下节点2有没有右孩子,如果没有,则表明节点2为叶子节点,直接访问这个节点2,然后出栈节点2;
C 如果节点2存在右孩子,如上图所示,那么我们该怎么办呢?我们把这个右孩子看成以其为根的子树,然后重复step A。如图所示,我们假设2下的右孩子是个叶子节点。分析这个过程,我们当然要入栈这个节点,然后观察到它没有左孩子,也没有右孩子,符合上文提到的step B;
D 这个节点出栈后,此时的栈顶元素为节点2,也就是我们下一个想要访问的元素,这是这个算法最难的地方,不是很容易能想到。
此处的访问条件不再是step B中的条件,而是刚才这个节点2的右孩子我们已经访问过了,所以该轮到其父节点2了。
需要用到一个指针存储着上一迭代的访问过的节点。
以上就是后序遍历非递归版的思路。
05—实现代码
这里我们以二叉树为例,讨论二叉树的后序遍历的非递归版实现。
我们先看下二叉树的节点TreeNode的数据结构定义。
节点的数据域的类型定义为泛型 T,含有左、右子树,及一个带有数据域的构造函数。
public class TreeNode<T>
{
public T val { get; set; }
public TreeNode<T> left { get; set; }
public TreeNode<T> right { get; set; }
public TreeNode(T data)
{
val = data;
}
}
二叉树的后序遍历的非递归版实现代码:
public static IList<T> PostorderTraversal<T>(TreeNode<T> root)
{
IList<T> rtn = new List<T>();
var s = new Stack<TreeNode<T>>();
if (root == null) return rtn;
s.Push(root);
TreeNode<T> cur = root;
TreeNode<T> r = null; //标记访问过的右子树
while (s.Count > 0)
{
//延伸到左子树
if (cur != null && cur.left != null)
{
s.Push(cur.left);
cur = cur.left;
}
else
{
cur = s.Peek();
//访问右子树的操作
if (cur.right != null && cur.right != r)
{
cur = cur.right;
s.Push(cur);
}
else //访问节点的操作
{
rtn.Add(s.Pop().val);
r = cur;
cur = null;
}
}
}
return rtn;
}
代码分析
代码中用到了两个非常重要的指针 cur,r 。
cur是每次迭代都会更新的指针,它没有非常明确的语义,它在某步迭代中可能是某个子树上的左节点抑或是右节点,也可能含义是标记着上一步迭代中访问到了一个叶子节点,此时需要从栈顶中拿值了。
r 的语义很明确,它是某次迭代过程中,如果发生了访问节点的操作,那么它指向这个访问过的节点,目的是为了判断是否向右子树展开,如下决定是否伸向向右子树的一个且条件的一部分。
也就是同时满足右子树不为空,并且右子树不等于上一迭代中的节点,然后才伸向右子树。
//访问右子树的操作 if (cur.right != null && cur.right != r) { cur = cur.right; s.Push(cur); }
05—算法评价
非递归版后序遍历算法的时间复杂度为 O(n),空间复杂度为栈所占的内存空间为 O(n)。
06—总结
讨论了二叉树的非递归版后序遍历算法,算法借助栈,相比于前序遍历和中序遍历,它多了一个指针指向上一迭代中访问过的节点,目的是为了判断是否向右子树展开,算法的时间和空间复杂度都为 O(n)。
- Neutron集成ONOS源码分析
- “访问限制”&“代理访问”实验
- OpenDaylight Lithium-SR2 Cluster集群搭建
- Linux | CentOS7下会玩JDK不?你确定?
- Linux | 不懂Linux的码神,不是真正的菜鸟
- 初体验Spring Boot 2支持的HikariCP连接池
- 快来了解JDK10中引入的全新JIT编译器:Graal
- 基于Ryu打造自定义控制器
- Junit 5新特性全集
- 深入了解浏览器的重绘与重排
- 自己动手写区块链(Java版)
- 自己动手写区块链-发起一笔交易(Java版)
- 详解JavaScript跨域问题
- OpenStack Magnum及Liberty新功能简介
- JavaScript 教程
- JavaScript 编辑工具
- JavaScript 与HTML
- JavaScript 与Java
- JavaScript 数据结构
- JavaScript 基本数据类型
- JavaScript 特殊数据类型
- JavaScript 运算符
- JavaScript typeof 运算符
- JavaScript 表达式
- JavaScript 类型转换
- JavaScript 基本语法
- JavaScript 注释
- Javascript 基本处理流程
- Javascript 选择结构
- Javascript if 语句
- Javascript if 语句的嵌套
- Javascript switch 语句
- Javascript 循环结构
- Javascript 循环结构实例
- Javascript 跳转语句
- Javascript 控制语句总结
- Javascript 函数介绍
- Javascript 函数的定义
- Javascript 函数调用
- Javascript 几种特殊的函数
- JavaScript 内置函数简介
- Javascript eval() 函数
- Javascript isFinite() 函数
- Javascript isNaN() 函数
- parseInt() 与 parseFloat()
- escape() 与 unescape()
- Javascript 字符串介绍
- Javascript length属性
- javascript 字符串函数
- Javascript 日期对象简介
- Javascript 日期对象用途
- Date 对象属性和方法
- Javascript 数组是什么
- Javascript 创建数组
- Javascript 数组赋值与取值
- Javascript 数组属性和方法
- Python中利用aiohttp制作异步爬虫及简单应用
- Linux内核设备驱动之系统调用笔记整理
- python3实现名片管理系统
- Linux IO多路复用之epoll网络编程
- 浅谈python在提示符下使用open打开文件失败的原因及解决方法
- Linux内核设备驱动之内核的调试技术笔记整理
- Python检查和同步本地时间(北京时间)的实现方法
- thinkPHP5.1框架使用SemanticUI实现分页功能示例
- python实现名片管理系统
- Python unittest 简单实现参数化的方法
- CentOS7部署Flask(Apache、mod_wsgi、Python36、venv)
- php的instanceof和判断闭包Closure操作示例
- PHP中的自动加载操作实现方法详解
- python 实现语音聊天机器人的示例代码
- Linux应用程序使用写文件调试程序的方法