二叉树的非递归遍历

本文详细介绍了如何实现二叉树的非递归遍历,特别是后序遍历,通过模拟系统栈的方法将递归算法转化为非递归。文章提供了前序、中序、后序遍历的非递归实现,并总结了模拟系统栈的关键步骤。

二叉树的遍历有三种经典方式:前序遍历、中序遍历、后序遍历。

递归遍历

递归的写法非常简单:

前序遍历

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 中断当前函数执行,开始执行该子函数。

后记:其实,这里的模拟系统栈功能还不够全面,比如子函数的返回值怎么模拟,调用子函数时参数的值传递与引用传递如何模拟。(因为二叉树的遍历问题里不要这些所以这里没有提及,大家可以自行思考一哈~)

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值