对于二叉树三种非递归遍历方式的理解
利用栈实现二叉树的先序,中序,后序遍历的非递归操作
栈是一种先进后出的数据结构,其本质应是记录作用,支撑回溯(即按原路线返回);因此,基于其的二叉树遍历操作深刻的体现了其特性:
若后续的输入和其前面的输出没关系,则可以一直入栈,只能无法再入时才停止入栈,如二叉树遍历;若前面的输出是后面的输出有关系,则不可以这样,如快速排序,其分割位置需要前面的输出支撑,则需要计算和入栈交替进行。
1.先入、后出,只有不能再进入时才能出栈
2.对于栈中的某一元素而言,需要判定其是否满足出栈条件:当回溯至栈中某一点时,后续入栈元素是否和其还有关系。如先中遍历,当回溯至某一点时,后续入栈的点已经和其没关系故出栈,然而后序遍历中其却和其后续入栈的点有关系,因此,做“已经二次访问标记”即可
也就是判断是否还需要其的记忆作用,也就是还需要第三次访问它。因此,回溯时,当不需要其记忆时则让其出栈,否则,对其做再次访问标记,这就是需要对从栈中弹出条件进行设定与判断,比如,先序、中序遍历的符合前者,后序符合后者。
3.设置好遍历结束条件
下面,以代码为例进行说明上述几点理解,请指正
- #include <stdio.h>
- #include <malloc.h>
- #include <stdlib.h>
- #include <queue>
- #include <stack>
- #include <iostream>
- using namespace std;
- typedef struct BiTNode{
- char data;
- BiTNode *lchild, *rchild;
- }BiTNode,*BiTree;
- void CreateBiTree(BiTree &T)//建树,按先序顺序输入节点
- {
- char ch;
- scanf("%c",&ch);
- if(ch==' ')
- {
- T=NULL;
- return;
- }
- else
- {
- T=(BiTree)malloc(sizeof(BiTNode));
- if(!T)
- exit(1);
- T->data=ch;
- CreateBiTree(T->lchild);
- CreateBiTree(T->rchild);
- }
- }
- void InOrderTraverse(BiTree T)//非递归中序遍历
- {
- stack<BiTree> Stack;
- if(!T)
- {
- printf("空树!\n");
- return;
- }
- while(T || !Stack.empty())
- {
- //先入栈:若T为空也即不能入栈时则执行下面的出栈操作
- while(T)
- {
- Stack.push(T);
- T=T->lchild;
- }
- //出栈:需判定出栈或标记
- T=Stack.top();
- Stack.pop();
- printf("%c",T->data);
- //起到链接作用
- T=T->rchild;
- }
- }
- void PreOrderTraverse(BiTree T)//非递归先序遍历
- {
- stack<BiTree> Stack;
- if(!T)
- {
- printf("空树!\n");
- return;
- }
- while(T || !Stack.empty())
- {
- //先入栈:若T为空也即不能入栈时则执行下面的出栈操作
- while(T)
- {
- Stack.push(T);
- printf("%c",T->data);
- T=T->lchild;
- }
- //出栈:需判定出栈或标记
- T=Stack.top();
- Stack.pop();
- //起到连接作用
- T=T->rchild;
- }
- }
- void PostOrderTraverse(BiTree T)//非递归后序遍历,用一个标记标记右子树是否访问过
- {
- int flag[20];
- stack<BiTree> Stack;
- if(!T)
- {
- printf("空树!\n");
- return;
- }
- //先入栈:若T为空也即不能入栈时则执行下面的出栈操作
- while(T)
- {
- Stack.push(T);
- flag[Stack.size()]=0;
- T=T->lchild;
- }
- while(!Stack.empty())
- {
- //判定是否可以出栈
- T=Stack.top();
- while(T->rchild && flag[Stack.size()]==0)
- {
- flag[Stack.size()]=1;
- //回溯点不能出栈,则打标记、并继续入栈
- T=T->rchild;
- while(T)
- {
- Stack.push(T);
- flag[Stack.size()]=0;
- T=T->lchild;
- }
- }
- //此处可以出栈了
- T=Stack.top();
- printf("%c",T->data);
- Stack.pop();
- }
- }
- void main()
- {
- BiTree T;
- CreateBiTree(T);
- PreOrderTraverse(T);
- printf("\n");
- InOrderTraverse(T);
- printf("\n");
- PostOrderTraverse(T);
- printf("\n");
- }
更简单的非递归遍历二叉树的方法
解决二叉树的很多问题的方案都是基于对二叉树的遍历。遍历二叉树的前序,中序,后序三大方法算是计算机科班学生必写代码了。其递归遍历是人人都能信手拈来,可是在手生时写出非递归遍历恐非易事。正因为并非易事,所以网上出现无数的介绍二叉树非递归遍历方法的文章。可是大家需要的真是那些非递归遍历代码和讲述吗?代码早在学数据结构时就看懂了,理解了,可为什么我们一而再再而三地忘记非递归遍历方法,却始终记住了递归遍历方法?
三种递归遍历对遍历的描述,思路非常简洁,最重要的是三种方法完全统一,大大减轻了我们理解的负担。而我们常接触到那三种非递归遍历方法,除了都使用栈,具体实现各有差异,导致了理解的模糊。本文给出了一种统一的三大非递归遍历的实现思想。
三种递归遍历
//前序遍历
void preorder(TreeNode *root, vector<int> &path)
{if(root != NULL){path.push_back(root->val);preorder(root->left, path);preorder(root->right, path);}
}
//中序遍历
void inorder(TreeNode *root, vector<int> &path)
{if(root != NULL){inorder(root->left, path);path.push_back(root->val);inorder(root->right, path);}
}
//后续遍历
void postorder(TreeNode *root, vector<int> &path)
{if(root != NULL){postorder(root->left, path);postorder(root->right, path);path.push_back(root->val);}
}
由上可见,递归的算法实现思路和代码风格非常统一,关于“递归”的理解可见我的《人脑理解递归》。
教科书上的非递归遍历
//非递归前序遍历
void preorderTraversal(TreeNode *root, vector<int> &path)
{stack<TreeNode *> s;TreeNode *p = root;while(p != NULL || !s.empty()){while(p != NULL){path.push_back(p->val);s.push(p);p = p->left;}if(!s.empty()){p = s.top();s.pop();p = p->right;}}
}
//非递归中序遍历
void inorderTraversal(TreeNode *root, vector<int> &path)
{stack<TreeNode *> s;TreeNode *p = root;while(p != NULL || !s.empty()){while(p != NULL){s.push(p);p = p->left;}if(!s.empty()){p = s.top();path.push_back(p->val);s.pop();p = p->right;}}
}
//非递归后序遍历-迭代
void postorderTraversal(TreeNode *root, vector<int> &path)
{stack<TempNode *> s;TreeNode *p = root;TempNode *temp;while(p != NULL || !s.empty()){while(p != NULL) //沿左子树一直往下搜索,直至出现没有左子树的结点{TreeNode *tempNode = new TreeNode;tempNode->btnode = p;tempNode->isFirst = true;s.push(tempNode);p = p->left;}if(!s.empty()){temp = s.top();s.pop();if(temp->isFirst == true) //表示是第一次出现在栈顶{temp->isFirst = false;s.push(temp);p = temp->btnode->right;}else //第二次出现在栈顶{path.push_back(temp->btnode->val);p = NULL;}}}
}
看了上面教科书的三种非递归遍历方法,不难发现,后序遍历的实现的复杂程度明显高于前序遍历和中序遍历,前序遍历和中序遍历看似实现风格一样,但是实际上前者是在指针迭代时访问结点值,后者是在栈顶访问结点值,实现思路也是有本质区别的。而这三种方法最大的缺点就是都使用嵌套循环,大大增加了理解的复杂度。
更简单的非递归遍历二叉树的方法
这里我给出统一的实现思路和代码风格的方法,完成对二叉树的三种非递归遍历。
//更简单的非递归前序遍历
void preorderTraversalNew(TreeNode *root, vector<int> &path)
{stack< pair<TreeNode *, bool> > s;s.push(make_pair(root, false));bool visited;while(!s.empty()){root = s.top().first;visited = s.top().second;s.pop();if(root == NULL)continue;if(visited){path.push_back(root->val);}else{s.push(make_pair(root->right, false));s.push(make_pair(root->left, false));s.push(make_pair(root, true));}}
}
//更简单的非递归中序遍历
void inorderTraversalNew(TreeNode *root, vector<int> &path)
{stack< pair<TreeNode *, bool> > s;s.push(make_pair(root, false));bool visited;while(!s.empty()){root = s.top().first;visited = s.top().second;s.pop();if(root == NULL)continue;if(visited){path.push_back(root->val);}else{s.push(make_pair(root->right, false));s.push(make_pair(root, true));s.push(make_pair(root->left, false));}}
}
//更简单的非递归后序遍历
void postorderTraversalNew(TreeNode *root, vector<int> &path)
{stack< pair<TreeNode *, bool> > s;s.push(make_pair(root, false));bool visited;while(!s.empty()){root = s.top().first;visited = s.top().second;s.pop();if(root == NULL)continue;if(visited){path.push_back(root->val);}else{s.push(make_pair(root, true));s.push(make_pair(root->right, false));s.push(make_pair(root->left, false));}}
}
以上三种遍历实现代码行数一模一样,如同递归遍历一样,只有三行核心代码的先后顺序有区别。为什么能产生这样的效果?下面我将会介绍。
有重合元素的局部有序一定能导致整体有序
这就是我得以统一三种更简单的非递归遍历方法的基本思想:有重合元素的局部有序一定能导致整体有序。
如下这段序列,局部2 3 4
和局部1 2 3
都是有序的,但是不能由此保证整体有序。
而下面这段序列,局部2 3 4
,4 5 6
,6 8 10
都是有序的,而且相邻局部都有一个重合元素,所以保证了序列整体也是有序的。
应用于二叉树
基于这种思想,我就构思三种非递归遍历的统一思想:不管是前序,中序,后序,只要我能保证对每个结点而言,该结点,其左子结点,其右子结点都满足以前序/中序/后序的访问顺序,整个二叉树的这种三结点局部有序一定能保证整体以前序/中序/后序访问,因为相邻的局部必有重合的结点,即一个局部的“根”结点是另外一个局部的“子”结点。
如下图,对二叉树而言,将每个框内结点集都看做一个局部
,那么局部有A
,A B C
,B D E
,D
,E
,C F
,F
,并且可以发现每个结点元素都是相邻的两个局部的重合结点
。发觉这个是非常关键的,因为知道了重合结点,就可以对一个局部排好序后,通过取出一个重合结点过渡到与之相邻的局部进行新的局部排序。我们可以用栈来保证局部的顺序(排在顺序前面的后入栈,排在后面的先入栈,保证这个局部元素出栈的顺序一定正确),然后通过栈顶元素(重合元素)过渡到对新局部的排序,对新局部的排序会导致该重合结点再次入栈,所以当栈顶出现已完成过渡使命的结点时,就可以彻底出栈输出了(而这个输出可以保证该结点在它过渡的那个局部一定就是排在最前面的),而新栈顶元素将会继续完成新局部的过渡。当所有结点都完成了过渡使命时,就全部出栈了,这时我敢保证所有局部元素都是有序出栈,而相邻局部必有重合元素则保证了整体的输出一定是有序的。这种思想的好处是将算法与顺序分离
,定义何种顺序并不影响算法,算法只做这么一件事:将栈顶元素取出,使以此元素为“根”结点的局部有序入栈,但若此前已通过该结点将其局部入栈,则直接出栈输出即可。
从实现的程序中可以看到:三种非递归遍历唯一不同的就是局部入栈的三行代码的先后顺序。所以不管是根->左->右
,左->根->右
,左->右->根
,甚至是根->右->左
,右->根->左
,右->左->根
定义的新顺序,算法实现都无变化,除了改变局部入栈顺序。
值得一提的是,对于前序遍历,大家可能发现取出一个栈顶元素,使其局部前序入栈后,栈顶元素依然是此元素,接着就要出栈输出了,所以使其随局部入栈是没有必要的,其代码就可以简化为下面的形式。
void preorderTraversalNew(TreeNode *root, vector<int> &path)
{stack<TreeNode *> s;s.push(root);while(!s.empty()){root = s.top();s.pop();if(root == NULL){continue;}else{path.push_back(root->val);s.push(root->right);s.push(root->left);}}
}
这就是我要介绍的一种更简单的非递归遍历二叉树的方法。
对于二叉树三种非递归遍历方式的理解相关推荐
- 实现二叉树的三种非递归遍历算法
[问题描述] 编写程序,实现二叉树的三种非递归遍历算法:先序非递归,中序非递归,后序非递归. [输入形式] 输入建树序列. [输出形式] 输出三种遍历序列. [样例输入] A B C # # # # ...
- 刷题:二叉树的非递归遍历方式
二叉树的非递归的遍历方式 上篇博客记录了二叉树的递归遍历方式以及根据二叉树的遍历结果还原二叉树的内容. 本篇博客记录二叉树的非递归的遍历方式. 二叉树的非递归遍历需要借助栈来实现,而且三种遍历的方式的 ...
- 【树】二叉树的两种非递归遍历方法
非递归的遍历需要使用栈保存当前不输出的结点,并且三种遍历顺序步骤有所不同. 中序遍历 1.查看其当前结点是否为空: 若非空则将当前结点入栈,指针指向其左孩子: 若当前结点为空,说明上一个入栈的结点没有 ...
- 二叉树非递归遍历的一点理解
二叉树是我们必须要了解的一个数据结构,针对这个数据结构我们可以衍生出好多知识. 主要是有几种遍历方式,前序遍历,中序遍历,后续遍历,层次遍历. 下面我们根据这个图来详细的说一下这几种非递归遍历的思想与 ...
- 二叉树的前序非递归遍历
二叉树的前序非递归遍历 前面学习过二叉树的前序遍历,使用递归的方式.简单回顾一下: Status PerOrder(BiTree T) {//前序遍历二叉树if (T != NULL) {Visit( ...
- 二叉树学习之非递归遍历
二叉树递归遍历可谓是学过数据结构的同仁都能想一下就能写出来,但在应聘过程我们常常遇到的是写出一个二叉树非递归遍历函数,接着上篇文章写二叉树的非递归遍历,先难后易,一步一步的来. 先上代码: #incl ...
- 二叉树中序非递归遍历
递归是程序设计中强有力的工具.递归函数结构清晰,使程序易读.但递归函数也有不可克服的弱点,时间.空间效率较低,运行代价较高,所以在实际使用中,常希望使用它的迭代版本.为了实现非递归遍历算法,需要一个堆 ...
- 二叉树 中序非递归遍历算法 c++
二叉树的中序非递归算法,详见下 首先,二叉树结点定义 typedef struct BiTNode//二叉树结点结构 {string data;struct BiTNode *lchild,*rchi ...
- 二叉树 2.0 -- 非递归遍历
二叉树递归遍历存在的问题 如果我们的二叉树只有左子树,而且树的高度还很深的时候,这个时候递归调用遍历的时候,栈帧空间开辟的较大,很可能造成栈溢出.但是我们一个程序中,为堆分配的空间要比栈大的多,这个时 ...
最新文章
- 最常用 150 个Linux命令汇总(建议收藏)
- Jmeter-逻辑控制器
- SAP ABAP 平台新的编程模型
- mysql optimize 作用_mysql optimize table
- jQuery Zoom 图片聚焦或者点击放大A plugin to enlarge images on touch, click, or mouseover
- 计算机可以配置端口号吗,如何设置打印机端口,详细教您设置电脑打印机端口...
- MathType编辑手写体
- python如何识别中文_python 判断是否为中文
- 18年一剑!德州心脏研究所研制出磁悬浮心脏,每秒2000转,为心衰患者续命
- 支付宝转账提现相关问题
- AI 成野生动物保护神:没有图像识别算法,考拉可能灭绝!
- 海马玩模拟器 修改host(让hosts生效)
- ctfshow_萌新_萌新隐藏题
- JAVA并发容器-ConcurrentHashMap 1.7和1.8 源码解析
- 百面机器学习和百面深度学习-测试1
- 嵌入式学习(二)——刷机和led实验(看门狗、c语言、icache、重定位、SDRAM)
- Windows设备管理器中的错误代码
- CF外挂界令人发指的垃圾外挂!让你接触不为人知的内幕!
- 跟我一起做一个vue的小项目(九)
- 基于微信旅游小程序系统设计与实现 开题报告