二叉树序言、为了、经过非递归措辞预订透彻的分析

前言

前两篇文章二叉树二叉搜索树中已经涉及到了二叉树的三种遍历。递归写法,仅仅要理解思想,几行代码。但是非递归写法却非常不easy。这里特地总结下,透彻解析它们的非递归写法。当中。中序遍历的非递归写法最简单,后序遍历最难。我们的讨论基础是这种:

//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为空,此时无非两种情况:

说明:

  1. 上图中仅仅给出了必要的节点和边,其他的边和节点与讨论无关,不必画出。
  2. 你可能觉得图a中近期保存节点算不得是根节点。假设你看过树、二叉树基础,使用扩充二叉树的概念,就能够解释。

    总之,不用纠结这个没有意义问题。

  3. 整个二叉树仅仅有一个根节点的情况能够划到图a。

细致想想,二叉树的左子树,最下边是不是上图两种情况?无论如何,此时都要出栈。并訪问该节点。这个节点就是中序序列的第一个节点。

依据我们的思维,代码应该是这样:

p = s.top();
s.pop();
cout << p->data;

我们的思维接着走,两图情形不同得差别对待:

1.图a中訪问的是一个左孩子。按中序遍历顺序,接下来应訪问它的根节点。也就是图a中的还有一个节点。高兴的是它已被保存在栈中。我们仅仅需这种代码和上一步一样的代码:

p = s.top();
s.pop();
cout << p->data;

左孩子和根都訪问完了。接着就是右孩子了,对吧。接下来仅仅需一句代码:p=p->rchild;在右子树中,又会新一轮的代码段(i)、代码段(ii)……直到栈空且p空。

2.再看图b。因为没有左孩子,根节点就是中序序列中第一个,然后直接是进入右子树:p=p->rchild;在右子树中。又会新一轮的代码段(i)、代码段(ii)……直到栈空且p空。

思维到这里,似乎非常不清晰,真的要区分吗?依据图a接下来的代码段(ii)这种:

p = s.top();
s.pop();
cout << p->data;
p = s.top();
s.pop();
cout << p->data;
p = p->rchild;

依据图b。代码段(ii)又是这种:

p = s.top();
s.pop();
cout << p->data;
p = p->rchild;

我们可小结下:遍历过程是个循环。而且按代码段(i)、代码段(ii)构成一次循环体。循环直到栈空且p空为止。

不同的处理方法非常让人抓狂,可统一处理吗?真的是能够的!回想扩充二叉树,是不是每一个节点都能够看成是根节点呢?那么,代码仅仅需统一写成图b的这种形式。

也就是说代码段(ii)统一是这种:

中序代码段(ii)

p = s.top();
s.pop();
cout << p->data;
p = p->rchild;

口说无凭,得经的过理论检验。

图a的代码段(ii)也可写成图b的理由是:由于是叶子节点,p=-=p->rchild;之后p肯定为空。

为空,还需经过新一轮的代码段(i)吗?显然不需。

(由于不满足循环条件)那就直接进入代码段(ii)。看!

最后还是一样的吧。

还是连续出栈两次。

看到这里。要细致想想哦。相信你一定会明确的。

这时写出遍历循环体就不难了:

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):

前序代码段(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;
}

以下给出还有一种思路下的代码。

它的想法是:给每一个节点附加一个标记(left,right)。假设该节点的左子树已被訪问过则置标记为left;若右子树被訪问过,则置标记为right。

显然,仅仅有当节点的标记位是right时,才可訪问该节点;否则,必须先进入它的右子树。

具体细节看代码中的凝视。

后序遍历代码二

//定义枚举类型: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>

总结

思维和代码之间总是有巨大的鸿沟。

一般是思维正确,清楚,但却不易写出正确的代码。

要想越过这鸿沟,仅仅有多尝试、多借鉴,别无它法。

下面几点是理解上述代码的关键:

  1. 全部的节点都可看做是父节点(叶子节点可看做是两个孩子为空的父节点)。
  2. 把同一算法的代码对照着看。

    在差异中往往可看到算法的本质。

  3. 依据自己的理解,尝试改动代码。

    写出自己理解下的代码。

    写成了。那就是真的掌握了。

转载请注明出处,本文地址:http://blog.csdn.net/zhangxiangdavaid/article/details/37115355

专栏文件夹:

版权声明:本文博客原创文章,转载,转载请注明出处。

时间: 2024-08-06 10:10:36

二叉树序言、为了、经过非递归措辞预订透彻的分析的相关文章

二叉树前序、中序、后序遍历非递归写法的透彻解析

前言 在前两篇文章二叉树和二叉搜索树中已经涉及到了二叉树的三种遍历.递归写法,只要理解思想,几行代码.可是非递归写法却很不容易.这里特地总结下,透彻解析它们的非递归写法.其中,中序遍历的非递归写法最简单,后序遍历最难.我们的讨论基础是这样的: //Binary Tree Node typedef struct node { int data; struct node* lchild; //左孩子 struct node* rchild; //右孩子 }BTNode; 首先,有一点是明确的:非递归

【数据结构与算法】二叉树深度遍历(非递归)

据说这个笔试面试的时候非常easy考到,所以写到这里. 图示 代码实现 /** * 源代码名称:TreeIteratorNoRecursion.java * 日期:2014-08-23 * 程序功能:二叉树深度遍历(非递归) * 版权:[email protected] * 作者:A2BGeek */ import java.util.Stack; public class TreeIteratorNoRecursion { class TreeNode<T> { private T mNod

第4章第3节 二叉树的基本操作(非递归实现)

二叉树的非递归遍历 上一节二叉树的递归遍历中简单介绍了二叉树的递归遍历的实现方式,本节主要介绍二叉树的非递归遍历实现,继续引用上节的例子来说明下. 一.先序遍历 二叉树先序遍历的访问顺序为:根结点->左孩子->右孩子.简单的说,对于任意一个结点,均可以看作是根结点,直接对其访问,如果访问完成后,左孩子不为空,则可以将该左孩子再次看成一个新的根结点,那么就又回到开始,访问根结点,访问左孩子,如果左孩子为空时,访问它的右孩子.对于一般程序而言,递归程序转为非递归程序需要引入栈这个数据结构,可以参考

二叉树中序遍历非递归写法

中序遍历比前序要稍微复杂些,我也先用手写理出思路 代码写的和书上的一比...感觉麻烦了好多,但毕竟是自己理的思路不容易忘,所以还是贴自己的 void inOrder_stack(BiTree T){ printf("\n非递归中序遍历结果:\n"); initStack(&sqStack); BiTree p=T,l;//l用于保存上次的输出 push(&sqStack,p); while(!stackEmpty(sqStack)){ getTop(&sqSta

二叉树的遍历(非递归方式)

前序非递归遍历(借用栈结构): ①将根节点入栈: ②判栈空,获取栈顶元素输出: ③判断右子树是否为空,再判断左子树是否为空,在回至②执行. void PreOrder(BinTree bt) { stack<BinTree> astack; BinTreeNode * p; astack.push(bt); while(!astack.empty()) { p=astack.top(); astack.pop(); cout<<p->data<<" &q

二叉树的三种非递归遍历方式

1.先序遍历 1 void PreorderTraversal(BinTree BT) 2 { 3 BinTree T; 4 std::stack<BinTree> BtStack; 5 T = BT; 6 while (T || !BtStack.empty()) 7 { 8 while (T) 9 { 10 BtStack.push(T); 11 printf("%c ", T->Data); 12 T = T->Left; 13 } 14 T = BtSt

二叉树四种遍历非递归

1 void levelOrder(Bitree* root){ 2 queue<Node*> nodeQueue; 3 Node* pointer=root; 4 if(pointer){ 5 nodeQueue.push(pointer); 6 } 7 while(!nodeQueue.empty()){ 8 pointer=nodeQueue.front(); 9 visit(pointer); 10 nodeQueue.pop(); 11 if(pointer->left)nod

二叉树的中序非递归遍历思想

#include<stdio.h> #include<stdlib.h> #define OK 1 #define  ERROR 0 typedef struct node { int data; struct node *lchild; struct node *rchild; } Node,Tree; /* www.quzhuanpan.com 解释全来自去转盘网,转载请告知 */ typedef Node *ElemType; typedef Tree *AnoElemTyp

二叉树的建立,非递归前序、中序、后续遍历

算法网上很多,这里只是我手写的可执行的代码,备份用. #include <iostream> #include <vector> #include<queue> using namespace std; struct node {     char element; struct node * left; struct node * right; //struct node * parent; node (char a) { element = a; } }; stru