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

二叉树的前中后序列遍历,递归调用自身的方式写出来的代码非常简洁,一目了然。

二叉树这种结构,个人认为天然的就是递归思想的载体。数学中的分形理论,有着类似的魅力。

递归思想很多大牛都有自己的解释,不再去做过多的解读,简单说起来,就是对于一个复杂的问题,将其分解成若干个简单的问题,对这若干个简单问题,规定相同的方法加以解决,并对问题的最终解决条件加以确立,避免无限的递归调用。

递归和循环的不同之处在于,递归是有去也有回,有向前的递归调用,也有向问题发起地点的回溯。

1、二叉树前中后序遍历的递归写法(C++)。

先定义数据结构。

// 节点example,你也可以定义自己的,这里方便展示用这样的结构
struct TreeNode
{
	char data;
	TreeNode *left, *right;
	bool flag; // 用于非递归后序遍历编码
	TreeNode(char ch): data(ch), left(nullptr), right(nullptr), flag(0) { }
};

typedef TreeNode *BTree;

前序:先访问根节点,再访问左儿子,再访问右儿子。

中序:先访问左儿子,再回到访问根节点,再访问右儿子。

后序:先访问左儿子,再访问右儿子,再访问根节点。

void PreOrder(BTree root)
{
    if (!root) return;          //若根节点空,不做任何事情
    printf("%c ", root->data); // 打印访问到的每个节点
    PreOrder(root->left);      // 访问其左儿子
    PreOrder(root->right);     // 访问其右儿子      
}

void InOrder(BTree root) {
	if (!root) return;
	InOrder(root->left);
	printf("%c ", root->data);
	InOrder(root->right);
}

void PostOrder(BTree root) {
	if (!root) return;
	PostOrder(root->left);
	PostOrder(root->right);
	printf("%c ", root->data);
}

我们发现,无论是前序,中序,还是后序,从节点的经历顺序上,都是先从根开始,然后到左子树,最后到右子树。对于每个节点,都是这样,可以运用一样的视角加以解释。前、中、后序遍历,不同点仅仅在于,打印节点的时机不同。这一点在上面的代码中可以十分明显的看出来。

2、二叉树前中后序遍历的非递归写法(C++)

我们知道,递归方法,存在调用栈溢出的风险。有没有办法去对此进行控制?当然是有的,那就是运用非递归的方法。

其实说是非递归,用的也是递归思想,不过不是调用自身,所以不会产生调用栈的问题。

怎么运用递归的思想,但是又避免调用栈呢,我们就得使用一个栈容器来模拟递归过程。

栈天然的可以用来模拟递归过程,还记得我们说递归是有去,还得回溯吗?也就是说,起点也是出口,栈也是如此,入栈的元素,还得原路返回才能出来。

我们再以此分析二叉树的三种遍历。

前面说到二叉树的访问顺序,其实本质来看,是一贯的,都是根->左->右,只不过访问节点内容的时机不同,前序就是到了根立即访问,根是什么呢,每个节点都是他的左右儿子的根,所以按照根->左->右的顺序,到一个节点,打印一个节点的内容。这里面有一个隐含的问题得注意到,这里所谓的根->左->右,其实是“根->左->根->右->根”。这里面的左和右泛指左子树、右子树,显然,左子树和右子树可能有一个节点、多个节点、没有节点(空)。

中序又是什么呢,还是按照根->左->根->右->根的顺序,这个时候访问节点的时机要做个调整,我们到了一个节点先不着急访问其内容,先一直往左,往左走不了了,肯定要回来的(递归回溯,由左->根),回到根,再从根往右。我们打印节点内容的时机,就是这个左->根的时候,把根打印出来,这个时候再往右走,如果右边为空,那么往上回溯,刚才经历的过程,其实是把更上一层的根的“左”遍历了一遍,这个时候又要回到更上一层的根,把其打印出来,再往它的右边走,如此类推。

后序遍历的大致过程,可以参照上文。

这里啰里啰嗦说一大堆,主要是想说,对于每个二叉树的节点,都存在这么个过程,也就是根->左->根->右->根。每个节点都是根,同时又可能是左儿子或者右儿子。

具体过程:

// 前序遍历非递归方法
void preOrderTraverse(BTree t) {
	if (!t) return;

	stack<TreeNode *> vstack;
	TreeNode *curr, *top;
	curr = t;
	while (curr || !vstack.empty()) {
		while (curr) {
			if (curr == t)
				printf("%c", curr->data);
			else
				printf(" %c", curr->data);
			vstack.push(curr);
			curr = curr->left;
		}
		top = vstack.top();
		vstack.pop();
		curr = top->right;
	}
}
// 中序遍历非递归
void inOrderTraverse(BTree root) {
	if (!root) return;

	stack<TreeNode *> vstack;
	TreeNode *curr, *top;
	curr = root;
	bool flag = 0;
	while (curr || !vstack.empty()) {
		while (curr) {
			vstack.push(curr);
			curr = curr->left;
		}
		top = vstack.top();
		vstack.pop();
		if (!flag) {
			flag = 1;
			printf("%c", top->data);
		}
		else
			printf(" %c", top->data);
		curr = top->right;
	}
}
// 后序遍历非递归方法
void postOrderTraverse(BTree root) {
	if (!root) return;

	stack<TreeNode *> vstack;
	TreeNode *curr, *top;
	curr = root;
	bool temp_flag = false;
	while (curr || !vstack.empty()) {
		while (curr) {
			vstack.push(curr);
			curr = curr->left;
		}
		top = vstack.top();
		if (!top->flag) {
			top->flag = 1;
			curr = top->right;
		}
		else {
			if (!temp_flag) {
				temp_flag = 1;
				printf("%c", top->data);
			}
			else
				printf(" %c", top->data);
			vstack.pop();
			if (!vstack.empty()) 
				top = vstack.top();
			if (!top->flag) {
				curr = top->right;
			    top->flag = 1;
			}
		}
	}
}

个人认为的几个原则:

1、对于节点的入栈顺序来说,严格遵循根-左-右的原则,所以出栈的时候就能保证按照反方向回溯。

2、对于出栈的时机和打印的时机,根据遍历方式的不同有所不同

3、对于前序而言,每个节点都作为根节点,入栈即打印其内容,再到左儿子,对左儿子应用同样的访问和打印原则,再对右儿子运用同样的访问和打印原则。

4、对于中序而言,每个节点作为根节点,先要访问其左儿子,再打印其内容,具体表现在,往左访问,一直压栈,当左为空,此时栈顶元素就是往左访问后回溯的根,对于中序而言,就在此时打印栈顶元素,打印过了就出栈,接着访问其右儿子,重复前面的过程。

5、对于后序,由于必须先访问完左边和右边,才能打印根的内容,所以得设置一个flag记录该节点的状态,如果flag状态是0,代表这个节点还没访问过它的右边,flag状态是1,代表这个节点的左右儿子都已经访问过了,下次循环再碰到这个节点,就可以打印出栈了。

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值