数据结构笔记:二叉树的非递归遍历

二叉树是一种重要的数据结构,在排序、信息编码、表达式转化等方面有着重要的作用。由于本人之前已在此平台中发布了有关二叉树递归遍历算法的文档(链接见下),所以本篇博客本人主要讲述二叉树的非递归遍历算法。
二叉树的递归遍历与相关统计

非递归遍历的基本原理

对递归调用有了初步了解的读者就会知道,任何递归调用无论时间空间复杂度如何,都需要使用到栈这一数据结构。递归的过程是不仅仅是一个函数自身调用自身的循环过程,本质上说,应该是一个回溯之后再还原的过程:回溯的阶段,函数要不断保存每一次递归的信息,以便在之后的还原过程中,一步步通过每一次递归的信息,找到最初的状态,最终求得问题的答案… 可以发现,回溯过程中最后一次保存的递归的信息,往往是还原过程中第一步要读取的信息… 依照这个规律推导下去,就不难发现,递归的实现要用到栈(越晚入栈的递归信息,在还原时越先被访问)。

上面的解释可以说明,非递归遍历二叉树的算法中,一定要用到栈这一数据结构。依照根结点遍历的顺序,元素入栈出栈的顺序会有所不同。

非递归遍历的算法

为了方便算法的设计与分析,本人用二叉链表的方式表示一棵二叉树,仍然用递归的方式,创建一棵二叉树,其对应的代码段如下(C++):

class tNode{
public:
	char		ch;
	tNode		*lchild;
	tNode		*rchild;
};

tNode *initTree(){	
	tNode		*root;
	char		ch;
	scanf("%c", &ch);

	if(ch=='#')
		root = NULL;				//输入‘#’说明此时创建的这个结点是一个空结点
	else{
		root = new tNode;
		root->ch = ch;
		root->lchild = initTree();
		root->rchild = initTree();
	}
	return root;
}

下面就前序、中序和后序遍历的三大模式,给出对应的算法。

前序遍历(DLR)

  1. 先判断二叉树是否为空树,不是空树时,定义一个tNode 的指针node指向根结点;
  2. 打印输出node->ch;
  3. 若node->rchild不为空,则将node->rchild入栈;
  4. node = node->lchild,若node不为空,跳转到2,否则跳转到5;
  5. 退栈直到node->rchild不为空为止,转2,直到栈为空为止。

上面是算法描述看似很复杂,但实际上大的动作无外乎五个:结点值打印输出、右子树入栈、转左子树、退栈、转右子树。其中,第一二三个操作可以在一个循环内进行,而退栈操作,要单独使用一个if-else语句。核心的代码部分,是一个大循环中嵌套一个子循环和if-else的结构。

函数源码:

void DLR(tNode *root){
	
	stack<tNode *> s_tree;
	tNode		*node = root;
	
	while(node || !s_tree.empty()){
		
		while(node){
			cout << node->ch << " ";
			s_tree.push(node);
			node = node->lchild;
		}
		if(!s_tree.empty()){
			node = s_tree.top();
			s_tree.pop();
			node = node->rchild;
		}
	}
}

中序遍历(LDR)

  1. 先判断二叉树是否为空树,不是空树时,定义一个tNode 的指针node指向根结点;
  2. 若node不为空,则node入栈,node=node->lchild;
  3. 若node为空,则退栈直到node->rchild不为空,访问node所指的结点;
  4. node=node->rchild,转2,直到栈空为止。

以上的算法描述和DLR有着相似之处,只是三大步骤的顺序不同罢了,LDR的三大步骤顺序为右子树入栈、结点访问、转左子树、出栈、转右子树。在代码中,应该注意的要点也和DLR一样。

函数源码:

void LDR(tNode *root){
	
	stack<tNode *> s_tree;
	tNode		*node = root;
	
	while(node || !s_tree.empty()){
		while(node){
			s_tree.push(node);
			node = node->lchild;
		}
		if(!s_tree.empty()){
			node = s_tree.top();
			s_tree.pop();
			cout << node->ch << " ";
			node = node->rchild;
		}
	}
}

后序遍历(LRD)
后序遍历的算法在空间复杂度上比DLR与LRD要复杂,主要在于要引入标志栈s_tag,来存储各个结点的访问状态。

  1. 引入一个bool类型的标志栈s_tag,用于存储结点栈中结点的访问信息;
  2. 先判断二叉树是否为空树,不是空树时,定义一个tNode 的指针node指向根结点;
  3. 若node不为空,则node入栈,并在s_tag中入栈一个false,node=node->lchild;
  4. 若node为空,则进入退栈的循环(s_tag和s_node同时退栈),退栈的条件是:node->rchild为空,或node对应在s_tag中的状态为true;停止退栈的条件是:node->rchild为空,且node对应在s_tag中的状态为false,当遇到这种情形时,还要将s_tag中栈顶的元素改为true
  5. node=node->rchild,转3,直到栈空为止。

由于LRD算法涉及两个栈的使用,所以这里再说一下代码的基本结构。
从上面的算法描述,可以将LRD算法分为三个步骤,分别为左子树入栈、s_node与s_tag退栈、修改s_tag栈顶元素、转换到右子树。其中,左子树入栈单独使用一个循环、s_node与s_tag退栈和修改s_tag栈顶元素使用一个循环,这两个循环嵌套到一个大循环里,其中第二个循环内使用if条件判断语句,作为循环结束的条件。

函数源码:

void LRD(tNode *root){
	
	stack<tNode *> s_node;
	tNode			*node = root, *bottom;
	stack<bool>		s_tag;
	
	if(!root);
	else{
		do{
			for(; node; node = node->lchild){
				s_node.push(node);
				s_tag.push(false);
				if(s_node.size() == 1)
					bottom = node;
			}
			
			while(1){
				node = s_node.top();
				if(!node->rchild || s_tag.top()){
					cout << node->ch << " ";
					s_node.pop();
					s_tag.pop();
					if(node == bottom)
						break;
				}
				else{
					s_tag.pop();
					s_tag.push(true);
					break;
				}
			}
			
			node = node->rchild;
			
		}while(!s_node.empty());
	}
}

本人在写这个程序的时候,犯了一个错误,就是第二个循环中少写了一个循环结束的条件(node==bottom),后来本人才弄明白,当最后一个元素(即根结点)出栈后,内层循环未能退出,导致栈空之后又执行了s_node.top()的语句,从而程序发生了异常,所以出错了。这也是本人引入bottom这个变量的理由。

层次遍历(level order)
层次遍历也是一种二叉树的遍历方式,此方式的实现与之前的三种方式有很大区别,不需要使用栈,但是需要使用队列。

  1. 先判断二叉树是否为空树,不是空树时,定义一个tNode 的指针node指向根结点,node入队列;
  2. node指向此时的队首元素,打印输出node的信息;
  3. 此时队列中的队首元素出队;
  4. 若node->lchild不为空,node->lchild入栈;若node->rchild不为空,node->rchild入栈;
  5. 转2,直到队列为空。

上述描述的算法比较简单,这里直接给出源码:

void level_order(tNode *root){
	
	queue<tNode *> qu;
	tNode		*node;
	
	if(root)
		qu.push(root);
	else
		return;
	
	while(!qu.empty()){
		node = qu.front();						//Who will dequeue is the front. 
		cout << node->ch << " ";
		qu.pop();
		if(node->lchild)
			qu.push(node->lchild);
		if(node->rchild)
			qu.push(node->rchild);
	}
}

代码整合

以上给出的只是某些函数的代码,完整的代码如下,此代码可以直接运行得到相应的结果。代码中包含了C++中实现栈和队列的模板库。
代码整合

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值