二叉树的非递归(迭代)统一实现“前中后序遍历”详解

目录


二叉树大家都非常熟悉,二叉树的遍历大家也不会陌生。用递归法来实现二叉树的“前中后序遍历”相对简洁,用几行代码就能实现,并且只需改变代码顺序就能实现3种遍历。与之相反,非递归(迭代)遍历方法实现“前中后序遍历”起来较为复杂,并且不太容易通过简单的交换代码的次序来实现3种遍历,当然,只是不太容易,并不是没有方法。 下面就介绍统一的非递归(迭代)方法来是实现“前中后序遍历”。

这个过程要使用栈的数据结构,这点想来不用多解释。

树节点的定义如下:

template<class T>
class TreeNode
{
public:
	T data;
	TreeNode* leftChild;
	TreeNode* rightChild;
};

首先明确一点:树中的任何一个节点都可以是父节点。每个父节点总是有左右两个子节点(空的子节点暂时也算是一个节点)。做遍历时,按照遍历的方式,依次把父节点和左右两个子节点压入栈中。此处以“后序遍历”为例,那么入栈的顺序就是父节点、右节点、左节点,按照思路,可以写出如下代码:

void BinaryTree<T>::PostOrderIterate(TreeNode<T> * node)
{
	std::stack<TreeNode<T> *> st;//定义一个栈

	TreeNode<T> * curnode = node;
	if (curnode == nullptr) return;
	//将父节点和两个子节点压入栈中,若子节点没有,不用压入栈
	st.push(curnode);
	if (curnode->rightChild) st.push(curnode->rightChild);
	if (curnode->leftChild) st.push(curnode->leftChild);

	while (!st.empty()){
	    //每次top出的栈顶数据可能是父节点或子节点。但是,不管哪种节点,现在都作为一个父节点,那么其入栈顺序要变,所以pop掉,重新入栈
		curnode = st.top();
		st.pop();
		//按遍历顺寻入栈
		st.push(curnode);
		if (curnode->rightChild) st.push(curnode->rightChild);
		if (curnode->leftChild) st.push(curnode->leftChild);
	}
}

写完了上述代码,我们继续分析。现在我们还需要解决处理数据(此处指打印节点)的问题,那么,什么时候处理数据(打印节点)呢?还有上述代码的循环永远结束不了,如下图。‘+’会不停的入栈出栈。
二叉树的非递归(迭代)统一实现“前中后序遍历”详解
分析之前,明确一点:树中的任何一个节点都可以是父节点。但并不总是父节点,有些节点在某些时候还只是左右子节点,只有在节点出栈的时候才可能成为父节点。如上图,以‘-’为例。
(1)‘-’作为‘+’的左节点入栈:
二叉树的非递归(迭代)统一实现“前中后序遍历”详解
(2)‘-’出栈,为左节点出栈:
二叉树的非递归(迭代)统一实现“前中后序遍历”详解

(3)‘-’作为父节点入栈:

二叉树的非递归(迭代)统一实现“前中后序遍历”详解
1)先分析‘+’不断出入栈的问题。这个过程如下:‘+’作为左节点入栈,然后作为左节点出栈,再作为父节点入栈,之后一直是作为父节点不断出入栈。可以发现,当节点作为父节点出栈时,此时表示已经到达分支的最下节点,可以结束循环了。这一点可以类推到右节点。

2)在分析处理数据(打印节点)的问题。先明确一点:节点作为左右子节点的时候是不能打印数据的,因为我们无法知道它是否为最后一个节点,只有作为父节点的时候才能打印数据。一但从栈中弹出的节点是父节点,此时一定处理数据(打印节点)。

综合1)和2),不难发现,我们只需要知道出栈的节点是父节点,就能解决问题。所以,我们只需要在节点入栈时,标记一下入栈节点的属性(父还是子),就可以了,做法是在父节点入栈后,再压入一个标志节点,这样每次取出父节点之前,都是先取出标志节点。

代码如下,注释已详细说明:

void BinaryTree<T>::PostOrderIterate(TreeNode<T> * node)
{
	std::stack<TreeNode<T> *> st;//定义一个栈

	TreeNode<T> * curnode = node;
	TreeNode<T> * tagFatherNode = new TreeNode<T>;//定义一个标志节点,用于标志入栈的父节点

	if (curnode == nullptr) return;
	//将父节点和两个子节点压入栈中,若子节点没有,不用压入栈
	st.push(curnode);
	st.push(tagFatherNode);//标记入栈节点为父节点
	if (curnode->rightChild) st.push(curnode->rightChild);
	if (curnode->leftChild) st.push(curnode->leftChild);

	while (!st.empty()){
	/*若top出的栈顶数据是一个父节点,处理数据(打印节点)并删除节点
	若top出的栈顶数据不是一个父节点,其作为父节点,重新入栈*/
		curnode = st.top();//取出栈顶数据
		st.pop();
		if (curnode == tagFatherNode){//判断出栈的节点是否为父节点
			curnode = st.top();//取出父节点
			std::cout << curnode -> data; //打印
			st.pop();//删除
		}
		else{
			//按遍历顺寻入栈
			st.push(curnode);
			st.push(tagFatherNode);//标记入栈节点为父节点

			if (curnode->rightChild) st.push(curnode->rightChild);
			if (curnode->leftChild) st.push(curnode->leftChild);
		}
	}
	delete tagFatherNode;	
}

进行到这里,后续遍历的非递归(迭代)方法就写出来,当然不仅仅局限于后序遍历,前序遍历和中序遍历只需调整3行代码的顺序就能实现,如下:

前序遍历:

if (curnode->rightChild) st.push(curnode->rightChild);
if (curnode->leftChild) st.push(curnode->leftChild); 

st.push(curnode);
st.push(tagFatherNode);//标记入栈节点为父节点

中序遍历:

if (curnode->rightChild) st.push(curnode->rightChild);

st.push(curnode);
st.push(tagFatherNode);//标记入栈节点为父节点

if (curnode->leftChild) st.push(curnode->leftChild);

这样就写出了和递归遍历方法一样的,只需调整代码顺序,就能实现3种遍历的方法。

总结:这种统一的方法开始会不太好理解,不过没关系,自己多想想,动手写写就好了。注意把握住二叉树的特点:任何节点都可以是父节点,也都可以是左右子节点(根节点处除外),子节点出栈的时候才可能成为父节点;若出栈的就是父节点,处理节点并从栈中删除。

以上就是统一实现3种遍历的方法。如果有疑问,欢迎评论区下方留言;本人水平有限 ,如有错误,也欢迎在评论区下方批评指正。若是喜欢本文,就帮忙点赞吧!

上一篇:寒假刷题打卡第七天 | 链表


下一篇:最小生成树