二叉树的递归遍历与非递归改写

之前写的二叉树的非递归遍历,几乎都用了previsou指针和current指针,判断这两个指针的相对位置,决定此时的动作。

后来在算法导论的二叉搜索树章节中看到,其实有两种办法写非递归遍历。一种是用栈去模拟递归的情况,另一种则用判断两个指针是否相等来做。

我的显然是后者。虽然我也用到了栈,但那是因为我的二叉树节点没有父节点指针的属性。

如果用栈去模拟递归遍历的话,是不需要判断两个指针是否相等的。

下面依次分析前序、中序和后序遍历。

首先给出二叉树的节点定义。

typedef struct BNode
{
	int data;
	struct BNode *left;
	struct BNode *right;
} BNode;
然后给出递归的前序遍历和中序遍历:

void preorderRec(BNode *t)
{
	if(!t)
	{
		return;
	}
	else
	{
		cout << t->data << ends;
		preorderRec(t->left);
		preorderRec(t->right);
	}
}
void inorderRec(BNode *t)
{
	if(!t)
	{
		return;
	}
	else
	{
		inorderRec(t->left);
		cout << t->data << ends;
		inorderRec(t->right);
	}
}
递归函数在形式上都非常简单。

下面讨论非递归实现前序和中序遍历。

一般来说,用非递归来模拟递归过程,都是用栈来模拟递归的函数栈来实现的。

下面给出前序和中序的非递归函数。虽然仔细看也能看懂,但后面接着会有一些问题和改进。

void preorder(BNode *t)
{
	if(!t)
	{
		return;
	}
	else
	{
		stack<BNode*> stk;
		while(t || stk.size() != 0)
		{
			while(t)
			{
				stk.push(t);
				cout << t->data << ends;
				t = t->left;
			}
			t = stk.top();
			stk.pop();
			t = t->right;
		}
	}
}
void inorder(BNode *t)
{
	if(!t)
	{
		return;
	}
	else
	{
		stack<BNode*> stk;
		while(t || stk.size() != 0)
		{
			while(t)
			{
				stk.push(t);
				t = t->left;
			}
			t = stk.top();
			cout << t->data << ends;
			stk.pop();
			t = t->right;
		}
	}
}

在前序和中序的非递归遍历函数中,入栈的情况是很好理解的,每次把左孩子入栈,直到左孩子为空,对应了递归调用中的preorder(t->left)或inorder(t->left)。

那么什么时候出栈,为什么会出栈?其实看上去没有那么直观。这个问题后面会讲。


现在不妨给出后序遍历的递归函数过程:

void postorderRec(BNode *t)
{
	if(!t)
	{
		return;
	}
	else
	{
		postorderRec(t->left);
		postorderRec(t->right);
		cout << t->data << ends;
	}
}
在这里,暂时看不出后序和前/中序的递归遍历有什么区别。

但是,要用栈去模拟后序遍历的递归调用过程,却是非常麻烦的。这是因为后序遍历的递归过程中包含了两次递归调用。

那么为什么前序和中序貌似很轻易就转换过来了呢?它们不也是两次递归调用吗?

实际上,仔细观察的话,前序和中序遍历有一个特点,就是输出语句的位置,一个在两次递归的前面,另一个在两次递归之间——它们的共性是,函数在递归返回时的最后一条语句是一个递归调用!

这种情况,就可以把递归调用进行尾递归优化,把两次递归调用改为一次递归,并将外面套上一层循环结构。

尾递归优化后的前/中序递归遍历函数:

void preorderRec(BNode *t)
{
	while(t)
	{
		cout << t->data << ends;
		preorderRec(t->left);
		t = t->right;
	}
}
void inorderRec(BNode *t)
{
	while(t)
	{
		inorderRec(t->left);
		cout << t->data << ends;
		t = t->right;
	}
}
这样一来,我们只需要用栈去模拟第一个递归调用就可以了。原本的第二个递归调用被优化成了循环。

所以,前/中序遍历的递归函数可以很容易转成用栈模拟的循环结构。

而后序遍历则要复杂一些。


我本想用两个栈去模拟两次递归调用,可即使用两个栈,那么在两个栈中还要考虑何时从第一个栈中弹出元素,何时从第二个栈中弹出元素,很麻烦,不一定能实现。

在网上看了看别人的代码(主要是自己懒),又自己稍加改动,写了下面的代码:

typedef struct BNodeTag
{
	BNodeTag(BNode *t):node(t), isRightRec(false){}
	BNode *node;
	bool isRightRec;
}BNodeTag;
void postorder(BNode *t)
{
	if(!t)
	{
		return;
	}
	else
	{
		stack<BNodeTag> stk;
		while(t || stk.size() != 0)
		{
			while(t)
			{
				stk.push(BNodeTag(t));
				t = t->left;
			}
			if(false == stk.top().isRightRec)	// 从左递归返回
			{
				stk.top().isRightRec = true;
				t = stk.top().node->right;
			}
			else	// 从右递归返回
			{
				cout << stk.top().node->data << ends;
				stk.pop();
			}
		}
	}
}
仍然用一个栈去模拟递归。

但因为有两次递归调用,所以重定义了栈的元素,使其多了一个标签符号,表示这次栈的弹出是从左递归返回还是右递归返回。

注意到在if(false == stk.top().isRightRec)的语句块中,实际上应该从栈中弹出栈顶,修改isRightRec元素,再压入栈。但由于效果相同,所以就直接修改了栈顶元素的属性。

另外,注意到,若进入了从右递归返回的else语句块,执行完后,下次循环时,是一定不会到while(t)的循环语句块中的。因为从右递归返回之后,表示当前节点的子节点已经遍历完毕,所以会不断返回,直到从左递归返回时,再次压栈,变量t才有了新的意义。


二叉树遍历充分说明了用栈去模拟递归的很强的技巧性。

之前一直想用非递归改写汉诺塔问题的递归算法,苦于汉诺塔中的两个递归调用想不清楚。经过二叉树遍历的分析之后,发现了尾递归优化来简化非递归改写的规律。有空的时候可以实现一下汉诺塔的非递归算法。


最后,给出本文代码的整体实现。

#include <iostream>
#include <stack>
using namespace std;

typedef struct BNode
{
	int data;
	struct BNode *left;
	struct BNode *right;
} BNode;

void preorderRec(BNode *t)
{
	while(t)
	{
		cout << t->data << ends;
		preorderRec(t->left);
		t = t->right;
	}
}
/*
void preorderRec(BNode *t)
{
	if(!t)
	{
		return;
	}
	else
	{
		cout << t->data << ends;
		preorderRec(t->left);
		preorderRec(t->right);
	}
}*/

void preorder(BNode *t)
{
	if(!t)
	{
		return;
	}
	else
	{
		stack<BNode*> stk;
		while(t || stk.size() != 0)
		{
			while(t)
			{
				stk.push(t);
				cout << t->data << ends;
				t = t->left;
			}
			t = stk.top();
			stk.pop();
			t = t->right;
		}
	}
}

void inorderRec(BNode *t)
{
	if(!t)
	{
		return;
	}
	else
	{
		inorderRec(t->left);
		cout << t->data << ends;
		inorderRec(t->right);
	}
}
/*void inorderRec(BNode *t)
{
	while(t)
	{
		inorderRec(t->left);
		cout << t->data << ends;
		t = t->right;
	}
}
*/
void inorder(BNode *t)
{
	if(!t)
	{
		return;
	}
	else
	{
		stack<BNode*> stk;
		while(t || stk.size() != 0)
		{
			while(t)
			{
				stk.push(t);
				t = t->left;
			}
			t = stk.top();
			cout << t->data << ends;
			stk.pop();
			t = t->right;
		}
	}
}

void postorderRec(BNode *t)
{
	if(!t)
	{
		return;
	}
	else
	{
		postorderRec(t->left);
		postorderRec(t->right);
		cout << t->data << ends;
	}
}

typedef struct BNodeTag
{
	BNodeTag(BNode *t):node(t), isRightRec(false){}
	BNode *node;
	bool isRightRec;
}BNodeTag;
void postorder(BNode *t)
{
	if(!t)
	{
		return;
	}
	else
	{
		stack<BNodeTag> stk;
		while(t || stk.size() != 0)
		{
			while(t)
			{
				stk.push(BNodeTag(t));
				t = t->left;
			}
			if(false == stk.top().isRightRec)	// 从左递归返回
			{
				stk.top().isRightRec = true;
				t = stk.top().node->right;
			}
			else	// 从右递归返回
			{
				cout << stk.top().node->data << ends;
				stk.pop();
			}
		}
	}
}

int main()
{			   
	BNode a1 = {1, NULL, NULL};
	BNode a2 = {2, NULL, NULL};
	BNode a3 = {3, NULL, NULL};
	BNode a4 = {4, NULL, NULL};
	BNode a5 = {5, NULL, NULL};
	BNode a6 = {6, NULL, NULL};
	BNode a7 = {7, NULL, NULL};
	a1.left = &a2;
	a1.right = &a3;
	a2.left = &a4;
	a2.right = &a5;
	a3.left = &a6;
	a3.right = &a7;
	preorderRec(&a1);
	cout << endl;
	preorder(&a1);
	cout << endl;
	cout << endl;
	inorderRec(&a1);
	cout << endl;
	inorder(&a1);
	cout << endl;
	cout << endl;
	postorderRec(&a1);
	cout << endl;
	postorder(&a1);
	cout << endl;
	return 0;
}






  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值