二叉树前序、中序、后序遍历非递归写法的透彻解析
前言
在前两篇文章二叉树和二叉搜索树中已经涉及到了二叉树的三种遍历。递归写法,只要理解思想,几行代码。可是非递归写法却很不容易。这里特地总结下,透彻解析它们的非递归写法。其中,中序遍历的非递归写法最简单,后序遍历最难。我们的讨论基础是这样的:
- //Binary Tree Node
- typedef struct node
- {
- int data;
- struct node* lchild; //左孩子
- struct node* rchild; //右孩子
- }BTNode;
首先,有一点是明确的:非递归写法一定会用到栈,这个应该不用太多的解释。我们先看中序遍历:
中序遍历
分析
中序遍历的递归定义:先左子树,后根节点,再右子树。如何写非递归代码呢?一句话:让代码跟着思维走。我们的思维是什么?思维就是中序遍历的路径。假设,你面前有一棵二叉树,现要求你写出它的中序遍历序列。如果你对中序遍历理解透彻的话,你肯定先找到左子树的最下边的节点。那么下面的代码就是理所当然的:
中序代码段(i)
- BTNode* p = root; //p指向树根
- stack<BTNode*> s; //STL中的栈
- //一直遍历到左子树最下边,边遍历边保存根节点到栈中
- while (p)
- {
- s.push(p);
- p = p->lchild;
- }
保存一路走过的根节点的理由是:中序遍历的需要,遍历完左子树后,需要借助根节点进入右子树。代码走到这里,指针p为空,此时无非两种情况:
说明:
- 上图中只给出了必要的节点和边,其它的边和节点与讨论无关,不必画出。
- 你可能认为图a中最近保存节点算不得是根节点。如果你看过树、二叉树基础,使用扩充二叉树的概念,就可以解释。总之,不用纠结这个没有意义问题。
- 整个二叉树只有一个根节点的情况可以划到图a。
- p = s.top();
- s.pop();
- cout << p->data;
我们的思维接着走,两图情形不同得区别对待:
- p = s.top();
- s.pop();
- cout << p->data;
- p = s.top();
- s.pop();
- cout << p->data;
- p = s.top();
- s.pop();
- cout << p->data;
- p = p->rchild;
- p = s.top();
- s.pop();
- cout << p->data;
- p = p->rchild;
中序代码段(ii)
- p = s.top();
- s.pop();
- cout << p->data;
- p = p->rchild;
- BTNode* p = root;
- stack<BTNode*> s;
- while (!s.empty() || p)
- {
- //代码段(i)一直遍历到左子树最下边,边遍历边保存根节点到栈中
- while (p)
- {
- s.push(p);
- p = p->lchild;
- }
- //代码段(ii)当p为空时,说明已经到达左子树最下边,这时需要出栈了
- if (!s.empty())
- {
- p = s.top();
- s.pop();
- cout << setw(4) << p->data;
- //进入右子树,开始新的一轮左子树遍历(这是递归的自我实现)
- p = p->rchild;
- }
- }
仔细想想,上述代码是不是根据我们的思维走向而写出来的呢?再加上边界条件的检测,中序遍历非递归形式的完整代码是这样的:
中序遍历代码一
- //中序遍历
- void InOrderWithoutRecursion1(BTNode* root)
- {
- //空树
- if (root == NULL)
- return;
- //树非空
- BTNode* p = root;
- stack<BTNode*> s;
- while (!s.empty() || p)
- {
- //一直遍历到左子树最下边,边遍历边保存根节点到栈中
- while (p)
- {
- s.push(p);
- p = p->lchild;
- }
- //当p为空时,说明已经到达左子树最下边,这时需要出栈了
- if (!s.empty())
- {
- p = s.top();
- s.pop();
- cout << setw(4) << p->data;
- //进入右子树,开始新的一轮左子树遍历(这是递归的自我实现)
- p = p->rchild;
- }
- }
- }
恭喜你,你已经完成了中序遍历非递归形式的代码了。回顾一下难吗?
中序遍历代码二
- //中序遍历
- void InOrderWithoutRecursion2(BTNode* root)
- {
- //空树
- if (root == NULL)
- return;
- //树非空
- BTNode* p = root;
- stack<BTNode*> s;
- while (!s.empty() || p)
- {
- if (p)
- {
- s.push(p);
- p = p->lchild;
- }
- else
- {
- p = s.top();
- s.pop();
- cout << setw(4) << p->data;
- p = p->rchild;
- }
- }
- }
前序遍历
分析
前序代码段(i)
- //边遍历边打印,并存入栈中,以后需要借助这些根节点(不要怀疑这种说法哦)进入右子树
- while (p)
- {
- cout << setw(4) << p->data;
- s.push(p);
- p = p->lchild;
- }
接下来就是:出栈,根据栈顶节点进入右子树。
前序代码段(ii)
- //当p为空时,说明根和左子树都遍历完了,该进入右子树了
- if (!s.empty())
- {
- p = s.top();
- s.pop();
- p = p->rchild;
- }
同样地,代码段(i)(ii)构成了一次完整的循环体。至此,不难写出完整的前序遍历的非递归写法。
前序遍历代码一
- void PreOrderWithoutRecursion1(BTNode* root)
- {
- if (root == NULL)
- return;
- BTNode* p = root;
- stack<BTNode*> s;
- while (!s.empty() || p)
- {
- //边遍历边打印,并存入栈中,以后需要借助这些根节点(不要怀疑这种说法哦)进入右子树
- while (p)
- {
- cout << setw(4) << p->data;
- s.push(p);
- p = p->lchild;
- }
- //当p为空时,说明根和左子树都遍历完了,该进入右子树了
- if (!s.empty())
- {
- p = s.top();
- s.pop();
- p = p->rchild;
- }
- }
- cout << endl;
- }
下面给出,本质是一样的另一段代码:
前序遍历代码二
- //前序遍历
- void PreOrderWithoutRecursion2(BTNode* root)
- {
- if (root == NULL)
- return;
- BTNode* p = root;
- stack<BTNode*> s;
- while (!s.empty() || p)
- {
- if (p)
- {
- cout << setw(4) << p->data;
- s.push(p);
- p = p->lchild;
- }
- else
- {
- p = s.top();
- s.pop();
- p = p->rchild;
- }
- }
- cout << endl;
- }
在二叉树中使用的是这样的写法,略有差别,本质上也是一样的:
前序遍历代码三
- void PreOrderWithoutRecursion3(BTNode* root)
- {
- if (root == NULL)
- return;
- stack<BTNode*> s;
- BTNode* p = root;
- s.push(root);
- while (!s.empty()) //循环结束条件与前两种不一样
- {
- //这句表明p在循环中总是非空的
- cout << setw(4) << p->data;
- /*
- 栈的特点:先进后出
- 先被访问的根节点的右子树后被访问
- */
- if (p->rchild)
- s.push(p->rchild);
- if (p->lchild)
- p = p->lchild;
- else
- {//左子树访问完了,访问右子树
- p = s.top();
- s.pop();
- }
- }
- cout << endl;
- }
后序遍历
分析
后序遍历代码一
- //后序遍历
- void PostOrderWithoutRecursion(BTNode* root)
- {
- if (root == NULL)
- return;
- stack<BTNode*> s;
- //pCur:当前访问节点,pLastVisit:上次访问节点
- BTNode* pCur, *pLastVisit;
- //pCur = root;
- pCur = root;
- pLastVisit = NULL;
- //先把pCur移动到左子树最下边
- while (pCur)
- {
- s.push(pCur);
- pCur = pCur->lchild;
- }
- while (!s.empty())
- {
- //走到这里,pCur都是空,并已经遍历到左子树底端(看成扩充二叉树,则空,亦是某棵树的左孩子)
- pCur = s.top();
- s.pop();
- //一个根节点被访问的前提是:无右子树或右子树已被访问过
- if (pCur->rchild == NULL || pCur->rchild == pLastVisit)
- {
- cout << setw(4) << pCur->data;
- //修改最近被访问的节点
- pLastVisit = pCur;
- }
- /*这里的else语句可换成带条件的else if:
- else if (pCur->lchild == pLastVisit)//若左子树刚被访问过,则需先进入右子树(根节点需再次入栈)
- 因为:上面的条件没通过就一定是下面的条件满足。仔细想想!
- */
- else
- {
- //根节点再次入栈
- s.push(pCur);
- //进入右子树,且可肯定右子树一定不为空
- pCur = pCur->rchild;
- while (pCur)
- {
- s.push(pCur);
- pCur = pCur->lchild;
- }
- }
- }
- cout << endl;
- }
后序遍历代码二
- //定义枚举类型:Tag
- enum Tag{left,right};
- //自定义新的类型,把二叉树节点和标记封装在一起
- typedef struct
- {
- BTNode* node;
- Tag tag;
- }TagNode;
- //后序遍历
- void PostOrderWithoutRecursion2(BTNode* root)
- {
- if (root == NULL)
- return;
- stack<TagNode> s;
- TagNode tagnode;
- BTNode* p = root;
- while (!s.empty() || p)
- {
- while (p)
- {
- tagnode.node = p;
- //该节点的左子树被访问过
- tagnode.tag = Tag::left;
- s.push(tagnode);
- p = p->lchild;
- }
- tagnode = s.top();
- s.pop();
- //左子树被访问过,则还需进入右子树
- if (tagnode.tag == Tag::left)
- {
- //置换标记
- tagnode.tag = Tag::right;
- //再次入栈
- s.push(tagnode);
- p = tagnode.node;
- //进入右子树
- p = p->rchild;
- }
- else//右子树已被访问过,则可访问当前节点
- {
- cout << setw(4) << (tagnode.node)->data;
- //置空,再次出栈(这一步是理解的难点)
- p = NULL;
- }
- }
- cout << endl;
- }<span style="font-family: 'Courier New'; "> </span>
总结
- 所有的节点都可看做是父节点(叶子节点可看做是两个孩子为空的父节点)。
- 把同一算法的代码对比着看。在差异中往往可看到算法的本质。
- 根据自己的理解,尝试修改代码。写出自己理解下的代码。写成了,那就是真的掌握了。
转载请注明出处,本文地址:http://blog.csdn.net/zhangxiangdavaid/article/details/37115355
二叉树前序、中序、后序遍历非递归写法的透彻解析相关推荐
- 二叉树的前、中、后、层次非递归遍历(js)
有如下二叉树 遍历: // 前序遍历, head-left-rightfunction HLR (tree) {const stack = [], res = []if (tree) stack.p ...
- 二叉树的前序、中序、后序遍历(递归、非递归写法)
文章目录 一.什么是二叉树? 二.二叉树的基本概念 三.二叉树的三种遍历方式 1.前序遍历(preordertraversal) 1.中序遍历(inordertraversal) 1.后序遍历(pos ...
- 【二叉树Java】二叉树遍历前序中序后序遍历的非递归写法
本文主要介绍二叉树前序中序后序遍历的非递归写法 在探讨如何写出二叉树的前序中序后序遍历代码之前,我们先来明确一个问题,前序中序后序遍历根据什么区分? 二叉树的前序中序后序遍历,是相较根节点说的.最先遍 ...
- 2021 - 10 -7 ! 二叉树的前序、中序、后序遍历 非递归思路(迭代版本)
//! 前序遍历的非递归版本,精髓在于用栈模拟递归的过程,但实际进栈出栈顺序与递归并不一样, //! 比较抽象,建议画图理解,比较重要 void BinarySearchTreesZH::preord ...
- 4.二叉树的先序、中序以及后序遍历的递归写法与非递归写法(LeetCode第94、144、145题)
一.递归法 这次我们要好好谈一谈递归,为什么很多同学看递归算法都是"一看就会,一写就废". 主要是对递归不成体系,没有方法论,每次写递归算法 ,都是靠玄学来写代码,代码能不能编过都 ...
- 二叉树的遍历:先序 中序 后序遍历的递归与非递归实现及层序遍历
二叉树的定义:一种基本的数据结构,是一种每个节点的儿子数目都不多于2的树 树节点的定义如下: // 树(节点)定义 struct TreeNode {int data; // 值TreeNode* l ...
- java中二叉树_Java工程师面试1000题224-递归非递归实现二叉树前、中、后序遍历...
224.使用递归和非递归实现二叉树的前.中.后序遍历 使用递归来实现二叉树的前.中.后序遍历比较简单,直接给出代码,我们重点讨论非递归的实现. class Node { public int valu ...
- 二叉树的深度(前序 中序 后序 递归非递归搜素)、广度、搜索 C++
a b c 使用 1 2 3 表示 /* 描述:二叉树的深度(前序 中序 后序 递归非递归搜素).广度.搜索 作者:jz 日期:20140819 */ #include<stdio.h> ...
- C++实现二叉树 前、中、后序遍历(递归与非递归)非递归实现过程最简洁版本
本文并非我所写,是复制的该链接中的内容: 最近学习二叉树,想编程实现递归和非递归的实现方式: 递归的方式就不说了,因为大家的递归程序都一样:但是对于非递归的实现方式, 根据这几天的查阅资料已看到差不多 ...
最新文章
- U-Boot常用命令
- Blazor+Dapr+K8s微服务之基于WSL安装K8s集群并部署微服务
- AcWing 1087. 修剪草坪28
- COM, COM+ and .NET 的区别
- python信号量怎么用_Python3.X 线程中信号量的使用方法示例
- 模幂运算问题,使用朴素算法和重复-平方算法(快速幂+C#计算程序运行时间)
- 运维,你是选择25k的996还是18k的八小时工作日?
- java静态成员静态代码块初始化顺序
- jdbc.postgresql源码分析
- 值得推荐的13款可视化软件,快收藏!
- C4D建模宝典R20笔记
- 「软件项目管理」一文详解软件项目进度计划
- mysql-8.0.18免安装版安装过程
- 处理猪舌须知小窍门-滋阴润燥好良方-菜椒滑猪舌
- leaflet地图和飞线
- 初始化mysql5.7.18_MySQL5.7.18,初始化完成登录,提示密码过
- Python基础入门自学——18--操作Excel-工作实践需求
- ZipFile 解压多个.zip压缩文件
- IT运维工作到底是做什么的?
- 细粒度图像分类模型(含实战代码)