树的前中后序 迭代写法——完全按照递归思维写。

这篇博客探讨了如何使用迭代方式模拟二叉树的前序、中序和后序遍历,通过手动压栈和出栈来实现递归效果。博主详细解释了迭代过程,强调了栈的使用技巧和防止死循环的关键点,如逆序压栈、标志节点的使用以及避免重复压栈。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

最近在刷算法的时候刷到树的遍历的时候,有一道题引起了我的思考。
Leecode 144. 二叉树的前序遍历,同样还有两道道类似的题:
Leecode 94. 二叉树的中序遍历
Leecode 145. 二叉树的后序遍历
题意很简单,这三道题都是要求使用迭代的方式写得到这三种遍历方式的结果。题目不难,最开始想的是通过迭代进行模拟树的拓扑结构,先根再左再右,为了回到右节点需要一个栈进行存储右节点。(leecode上解题很多,常规的解法肯定不值得我专门写一篇博客来纪录一下)


我在想,如何用迭代和栈的方式去模拟递归。能不能找到一种通用的方式能解决一切迭代模拟递归的问题。因为递归也是压栈出栈嘛。


答案是肯定有的。我们要做的是去手动的模拟压栈出栈。如何模拟呢?请听我细细道来。

先简单说一下递归函数调用吧,证明一下这个方案的可行性。

递归函数调用实际上就是把当前的一些变量以及它的值给保存起来(保护现场)然后再去执行函数调用。当函数调用返回的时候,这时恢复原来的变量及其值。函数调用关系如下图所示
在这里插入图片描述
可见,第一层函数调用实际上是最后才返回的,这种先进后出的逻辑结构其实就是栈。也就是说用栈是完全可以模拟递归的。理论存在,开始实现。

以前序举例吧。
在这里插入图片描述
先来个递归写法:

	void dfs(TreeNode* root,vector<int> &res){
	        if(root==NULL)
	            return ;
	        res.push_back(root->val);       //根
	        dfs(root->left,res);            //左
	        dfs(root->right,res);           //右
	}
    vector<int> preorderTraversal(TreeNode* root) {
       if(root==NULL)
            return {};
        vector<int> res;
        dfs(root,res);
        return res; 
    }

迭代写法:
前面说了,函数调用的时候要先做一个工作——保护现场,也就是压栈,怎么压?也就是把后面没执行的东西压到栈里面去嘛。具体一点,也就是我去遍历左子树的时候,要把右子树的压到栈里面去,因为我左子树遍历完后要去遍历右子树嘛。由于栈是先进后出的结构,所以我们要先压右子树再压左子树,这样出栈的顺序才是先左后右。

    vector<int> preorderTraversal(TreeNode* root) {
        vector<int> res;
        stack<TreeNode*> sta;
        sta.push(root);
        while(!sta.empty()){
            TreeNode* node=sta.top();
            sta.pop();
            if(node==NULL)
                continue;
            res.push_back(node->val);		//由于是前序,直接加入结果集
            
            sta.push(node->right);			//先左后右
            sta.push(node->left);
        }
        return res;
    }

前序很容易理解,也很容易实现,现在来看看后序。

后序的递归写法:

void dfs(TreeNode* root,vector<int> &res){
        if(root==NULL)
            return ;
        dfs(root->left,res);            //左
        dfs(root->right,res);           //右
        res.push_back(root->val);       //根
    }
    vector<int> preorderTraversal(TreeNode* root) {
       if(root==NULL)
            return {};
        vector<int> res;
        dfs(root,res);
        return res; 
    }

(这里插一句,如果你熟悉前中后序的遍历顺序,会发现前序是前左右,后序是左右前,当我前序先遍历右子树再遍历左子树的时候,前序就变成了前右左,这是我把前序的答案给逆序,不就变成左右前了吗。)

我们不搞那些花里胡哨的,还是模拟递归吧。当我们按照前面的理论,通过递归的遍历顺序反向压栈,写着写着才发现后序最后是要回到最开始的结点的啊!!!如果我们把当前结点也压进去,那么不就死循环了吗,无限压自己再出栈自己。这时我们需要一个标志结点(你也可以使用NULL,但是如果是NULL的话就不能把叶子结点的NULL加进去了),来标志我要恢复现场了,这时只能出栈不能再把自己压进去了。 在代码中的表现如下:

vector<int> postorderTraversal(TreeNode* root){
        stack<TreeNode *> sta;
        sta.push(root);
        vector<int> res;
        TreeNode *flag=new TreeNode(0);     //1.一个标志结点,表示要恢复现场了
        while(!sta.empty()){
            TreeNode* node=sta.top();
            sta.pop();
            if(node==NULL)
                continue;
            if(node==flag){                 //5.如果是标志结点,表示恢复现场,也就是去更新res
                TreeNode* ROOT=sta.top();	//6.由于之前pop了一次,也就是把flag结点pop出去了,所以top等于原来的结点。
                sta.pop();
                res.push_back(ROOT->val);
                continue;					//7.注意继续压栈了,直接continue。
            }

            sta.push(node);                 //2.因为要恢复现场,所以把当前结点再次压入栈
            sta.push(flag);                 //3.压入标志结点
            sta.push(node->right);          //4.先右后左
            sta.push(node->left);   
            
        }
        delete flag;						//8. 防止内存泄漏
        return res;
    }

既然标志位我们都解决了,中序岂不是手到擒来?

中序遍历顺序为: 左 根 右
那么迭代实现为:

vector<int> inorderTraversal(TreeNode* root) {
        stack<TreeNode*> sta;
        vector<int> res;
        TreeNode* flag=new TreeNode(0);

        sta.push(root);
        while(!sta.empty()){
            TreeNode *node=sta.top();
            sta.pop();
            if(node==NULL)
                continue;
            if(node==flag){
                TreeNode * ROOT=sta.top();
                sta.pop();
                res.push_back(ROOT->val);
                continue;
            }
            sta.push(node->right);
            sta.push(node);         
            sta.push(flag);
            sta.push(node->left);
        }
        delete flag;
        return res;
    }

看完了这三道题,于是乎除了掌握了数的三种遍历顺序的迭代实现以外,我们还知道了如何用迭代去模拟递归的压栈。总结一下,需要注意的点:

  1. 考虑栈的先进后出,栈的压入顺序为逆序,和递归的实现相反。
  2. 恢复现场的时候需要用一个标志结点来标识,不然会造成死循环。
  3. 当结点为NULL或者flag结点的时候,一定要continue,不然也会造成重复压栈的死循环。
  4. 注意最后删除flag结点,防止内存泄漏。
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值