C++ :借助栈完成二叉树的非递归遍历

二叉树的传统访问分为:前序、中序、后序、层序。

其中前三者是递归访问,但是递归是有缺陷的,树太深就会栈溢出。

因此本文我们思考如何使用非递归的方法来完成遍历。

1. 前序遍历   

   要迭代⾮递归实现⼆叉树前序遍历,⾸先还是要借助递归的类似的思想,只是需要把结点存在栈中。
               
思考一下操作系统的栈的物理结构是如何进行递归的:
先进4,4再进2,2再进nullptr,nullptr退栈到2,2进栈到7....
7退出到4,4再进栈到5.......

我们使用数据结构的栈来模仿函数栈帧,将访问分为:

                                        

也就是全部都遵循以下简图访问逻辑:

              

每一个圈可以是一个节点、也可以是一棵树。

可以结合操作系统的栈的工作原理思考为什么这么做。

实现代码:

首先,无论如何我们都要先把根开始的左子树走完。

                   

然后我们严格遵循我们的遍历规则:

1.先访问左路节点

2.访问左路节点的右节点

走出这个循环时,cur已经来到了最左边叶子结点的左(nullptr),  现在应该去访问最左边叶子节点的右了(也就是进入第二步),那么直接从st中取一个出来即可。

class Solution {
public:
    vector<int> preorderTraversal(TreeNode* root) {
        TreeNode* cur = root;
        stack<TreeNode*> st;
        vector<int> ans;

        while(cur || !st.empty()){
            while(cur){
                ans.push_back(cur->val);//此句表示完成当前节点的遍历
                st.push(cur);
                cur=cur->left;
            }
            //此时cur一定以及是nullptr了,我们可以一个一个拿出刚刚入的栈,去访问右节点了。
            TreeNode* top = st.top();
            st.pop();
            //进行第二步,进入左路节点的右子树
            cur = top->right;
        } 
    return ans; 
    }
};

进入栈顶节点的右之后,进入下一轮循环。将这个top->right当作新的一个递归子问题去再度解决,只不过用的是循环语句。

至于是否进入的条件,要么cur现在已经拿到了一个top->right,还要继续访问;要么栈里还有元素,还能让栈pop一次后再获得新的cur,再进入一轮循环。


 2. 中序遍历

    中序遍历和前序遍历的思考极其相似:更改ans的push_back时间即可(也就是改变完成遍历的时间)。

vector<int> inorderTraversal(TreeNode* root) {
        TreeNode* cur = root;
        stack<TreeNode*> st;
        vector<int> ans;

        while(cur || !st.empty()){
            while(cur){
                //ans.push_back(cur->val);
                st.push(cur);
                cur=cur->left;
            }
            //此时cur一定以及是nullptr了,我们可以一个一个拿出刚刚入的栈,去访问右节点了。
            TreeNode* top = st.top();
            ans.push_back(st.top()->val);
            st.pop();

            cur = top->right;
        } 
    return ans; 
    }

“遍历”是一个抽象的概念,我们认为现在的遍历就是:将val加入到vector中去。

但是为了能让一开始的经过的更靠近根的节点入栈,还是需要把他们“经历”一遍。

但是遍历(也就是进入vector)这个动作,我们需要放到后面pop的地方,这样才是中序。


3. 后序遍历 

相对于前序和中序,后序遍历比较麻烦。

大思路不变:任然是将一个树分为左路节点+左路节点的右子树

用刚才的思路观察后序遍历: 

先和中序一样,一路走到2,将所有的节点都压入栈(但是不能push_back到vector中,与中序同理),然后cur已经走到空了,该从栈中取数据了。

但是取到2的时候不能将2弹出去,因为后序需要先遍历右树。所以进入7,并且将7入栈。

当7完成访问之后,此时才能将2弹出。

因此,一个节点能否被push_back进vector然后再弹出栈,取决于该节点的右子树是否已经完成了遍历。“完成了遍历”也应该有两种情况,一种是右树被遍历完了,比如已经遍历了7之后的2;另一种是右树为空,可以直接弹出,比如7。

现在的问题是,怎么判断右子树是否被访问过或者右子树为空呢?

 为空倒是好判断:

根据后序的访问顺序:左子树 右子树 根

对于每一个节点,最后被访问的都是自己(根),如果根的右子树还没被访问,那上一个被访问的一定是左子树的根。如果根的右子树已经被遍历了,那上一个被遍历的数据一定是右子树的根。

使用双指针法判断上一个被遍历的节点是左子树的根还是右子树的根

此时,top更像是顶替了我们预计的cur的作用, 表示了当前希望被遍历的节点。

完整代码:

class Solution {
public:
    vector<int> postorderTraversal(TreeNode* root) {
        vector<int> ans;
        stack<TreeNode*> st;
        TreeNode* cur = root;
        TreeNode* prev = nullptr;

        while(cur!=nullptr||!st.empty()){
            while(cur){
                st.push(cur);
                cur = cur ->left;
            }
            //走到这的时候cur一定已经是空了
            TreeNode* top = st.top();//先取值,但是不要着急把该节点从栈中弹出来
            if(top->right==nullptr || prev == top->right){//此时就可以访问这个节点了。
                ans.push_back(top->val);
                prev = top;
                st.pop(); 
            }else{
                cur = top->right;
            }
        }
        return ans;
    }
};

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值