二叉树的遍历有三种经典方式:前序遍历、中序遍历、后序遍历。
递归遍历
递归的写法非常简单:
前序遍历
void preorder_traverse(tree_node* root) {
if (root == nullptr)
return;
this->preorder.push_back(root->value);
preorder_traverse(root->left);
preorder_traverse(root->right);
}
中序遍历
void inorder_traverse(tree_node* root) {
if (root == nullptr)
return;
inorder_traverse(root->left);
this->inorder.push_back(root->value);
inorder_traverse(root->right);
}
后序遍历
void postorder_traverse(tree_node* root) {
if (root == nullptr)
return;
postorder_traverse(root->left);
postorder_traverse(root->right);
this->postorder.push_back(root->value);
}
非递归的深度优先遍历
二叉树的非递归遍历是一个经典问题,有教科书式的完美解法,但相信很多人和我一样:书打开,“嗯。。马冬梅”,等到要用的时候(比如说面试),“马什么梅来着。。?”。递归的算法很容易记住,试想,如果我们能够根据递归的算法写出非递归的算法,这个问题不就迎刃而解了么~现在,这个问题转化成了——给定任意一个递归算法,如何写出其非递归版本?其实,递归也是函数调用的一种,只不过递归调用的是这个函数本身。让我们来回忆一下操作系统是如何处理函数调用的:
当发生函数调用时,系统暂存当前的系统状态(将当前系统状态压栈),开始执行子函数,待子函数执行完毕后,再从系统栈顶读取之前保存的系统状态,继续主函数的执行。
这为我们模拟系统栈递归函数非递归化提供了一些思路。下面,我们就二叉树遍历中最难非递归化的后序遍历为例,来说明如何模拟系统栈将该递归算法非递归化。再看一眼后序遍历的递归算法:
1 void postorder_traverse(tree_node* root) {
2 if (root == nullptr)
3 return;
4 postorder_traverse(root->left);
/* 这里发生了一次递归调用,我们需要保存当前的运行状态。
这个“运行状态”究竟包含哪些信息呢?
1. 数据:等下到第5行时我们还需要用到 root->right,所以现在应该把 root 压栈存起来;
2. 命令:等执行完第4行的调用后返回现在这个函数中时,我们需要知道下面该执行哪行代码了,
所以这里应该把“第5行”存下来,表示第4行的调用执行完了后该执行第5行代码了 */
5 postorder_traverse(root->right);
/* 程序运行到这里,表明第4行的调用已经结束,应该恢复上次的运行状态,
然而这里又发生了一次递归调用,我们需要再次保存当前的运行状态:
1. 数据:等下第6行要用 root->value, 所以把 root 压栈存起来,
2. 命令:“第6行”,表示等第5行的调用结束了该执行第6行 */
6 this->postorder.push_back(root->value);
/* 第5行的调用结束,取出上次保存的运行状态,继续程序执行*/
7 }
从上面的分析可以看出,程序的“运行状态”应该包含“数据”和“命令”这两种信息。所以我们所模拟的系统栈的应该能保存这两种信息,这里为了实现方便,我们使用两个栈:
void post_traverse(tree_node* root) {
stack<tree_node*> vars; //存“数据”的栈
stack<string> states; //存“命令”的栈
}
这两个栈应该怎么用呢?
root = vars.top(); //取出当前需要处理的数据
vars.pop();
string state = states.top(); //取出当前应该执行的命令
states.pop();
根据 state 指示的代码行数对数据干点啥;
/* 遇到函数调用时 */
vars.push(该保存的数据);
states.push(这次调用结束后该执行的命令);
上面的伪代码表明了如何用栈处理调用,但如何开始一个函数的执行,以及如何结束一个函数呢
/* 系统不断从栈中取出数据和要执行的代码行数,开始执行当前函数,直至系统栈为空,此时表明函数(包括其调用的所有子函数),已经执行完毕。*/
1 while (!vars.empty()) {
2 root = vars.top(); //取数据
3 vars.pop();
4 string state = states.top(); //取要执行的命令
5 states.pop();
6 根据 state 指示的代码行数对数据干点啥;
/* 遇到函数调用时 */
7 vars.push(该保存的数据);
8 states.push(这次调用结束后该执行的命令);
9 vars.push(子函数需要用到的数据); /* 想想这里为什么要将子函数需要用的数据和命令压栈?*/
10 states.push(子函数开始执行的命令);
11 continue;
/* 中断当前函数的执行,开始下一轮循环(即子函数的执行):
在下一轮循环开始时,我们取出刚压入栈的子函数数据和命令,开始执行子函数。 */
}
经过上面的分析,我们发现应该在子函数的入口处也标记一个命令,这样才能顺利的开始执行子函数。当然,对于递归函数来说,子函数还是它自己。再来看一下递归算法中需要标记的代码行数。
1 void postorder_traverse(tree_node* root) {
2 if (root == nullptr) //entrance_1:函数入口
3 return;
4 postorder_traverse(root->left);
5 postorder_traverse(root->right); //entrance_2: 第4行调用返回后的继续执行的入口
6 this->postorder.push_back(root->value); // entrance_3: 第5行调用返回后的继续执行的入口
7 }
现在,经过上面的一通分析,我们终于可以写出后序遍历的非递归版本了:
后序遍历(非递归)
void postorder_traverse2(tree_node *root)
{
stack<tree_node*> vars; //数据栈
stack<string> states; //命令栈
vars.push(root); //preorder_traverse() 开始执行时需要的数据
states.push("entrance_1"); //preorder_traverse() 开始执行时的命令
while (!vars.empty()) {
root = vars.top(); //取数据
vars.pop();
string state = states.top(); //取命令
states.pop();
if (state == "entrance_1") { //现在该执行 preorder_traverse() 的第2行代码了
if (root == nullptr) { //对着 preorder_traverse() 的第2行代码,开抄
continue;
}
/* 这里对应 preorder_traverse() 的第4行代码 */
vars.push(root); //保存数据
states.push("entrance_2"); //保存命令:待会调用结束该从 entrance_2 执行代码了
vars.push(root->left); //为 preorder_traverse() 的第4行调用准备数据
states.push("entrance_1"); //为 preorder_traverse() 的第4行调用准备命令
continue; //开始执行 preorder_traverse() 的第4行的调用吧~
}
if (state == "entrance_2") { //现在该执行 preorder_traverse() 的第5行代码了
vars.push(root); //保存数据
states.push("entrance_3"); //保存命令:待会调用结束该从 entrance_2 执行代码了
vars.push(root->right); //为 preorder_traverse() 的第5行调用准备数据
states.push("entrance_1"); //为 preorder_traverse() 的第5行调用准备命令
continue; //开始执行 preorder_traverse() 的第4行的调用吧~
}
if (state == "entrance_3") { //现在该执行 preorder_traverse() 的第6行代码了
this->postorder.push_back(root->value); //开始抄 preorder_traverse() 的代码
}
} //这个反括号就是 preorder_traverse() 第7行那个反括号。
}
这种方式虽然没有教科书上的非递归方法那么精妙绝伦,但形式整齐,便于记忆。更重要的是,当我们掌握了模拟系统栈的方法后,以后不论是后序遍历、前序遍历还是别的任何递归算法,我们都能通过这种方式“无痛”地写出对应的非递归算法。
下面应用这种方法给出前序遍历和中序遍历的非递归版本:
前序遍历(非递归)
void preorder_traverse2(tree_node* root) {
stack<tree_node*> vars;
stack<string> states;
vars.push(root);
states.push("entrance_1");
while (!vars.empty()) {
root = vars.top();
vars.pop();
string state = states.top();
states.pop();
if (state == "entrance_1") {
if (root == nullptr) {
continue;
}
this->preorder.push_back(root->value);
vars.push(root);
states.push("entrance_2");
vars.push(root->left);
states.push("entrance_1");
continue;
}
if (state == "entrance_2") {
vars.push(root->right);
states.push("entrance_1");
}
}
}
中序遍历(非递归)
void inorder_traverse2(tree_node* root) {
stack<tree_node*> vars;
stack<string> states;
vars.push(root);
states.push("entrance_1");
while (!vars.empty()) {
root = vars.top();
vars.pop();
string state = states.top();
states.pop();
if (state == "entrance_1") {
if (root == nullptr) {
continue;
}
vars.push(root);
states.push("entrance_2");
vars.push(root->left);
states.push("entrance_1");
continue;
}
if (state == "entrance_2") {
this->inorder.push_back(root->value);
vars.push(root->right);
states.push("entrance_1");
}
}
}
最后,对模拟系统栈的方法做一个总结:
-
设置两个栈,一个用来存储数据,一个用来存储命令(要执行的代码行数)
-
设置一个循环,不断地从数据栈和命令栈读取数据和命令,开始(继续)当前函数的执行。当栈为空时,表明该函数(以及其所有调用的子函数)执行完毕。
-
发生函数调用时:
1、保存现场(调用结束后需要用到的数据、调用结束后需要执行的代码行数)
2、准备调用函数的数据和命令。
3、使用 continue 中断当前函数执行,开始执行该子函数。
后记:其实,这里的模拟系统栈功能还不够全面,比如子函数的返回值怎么模拟,调用子函数时参数的值传递与引用传递如何模拟。(因为二叉树的遍历问题里不要这些所以这里没有提及,大家可以自行思考一哈~)
本文详细介绍了如何实现二叉树的非递归遍历,特别是后序遍历,通过模拟系统栈的方法将递归算法转化为非递归。文章提供了前序、中序、后序遍历的非递归实现,并总结了模拟系统栈的关键步骤。
3635

被折叠的 条评论
为什么被折叠?



