【算法】二叉树的非递归遍历的简洁写法/迭代器实现/O(1)空间复杂度的Morris遍历

事情的起因是这样的,一大早突然想到C++的STL里set是由红黑树实现的,那set遍历的时候iterator是怎么实现的呢,自己想了个不算太好的算法,于是就想着去网上找找,看到一个人说就是把二叉树的非递归遍历给改改就好了,于是我就想着LeetCode上有前中后序二叉树的非递归遍历这三道题,之前做过,现在再拿来做一遍好了。但是发现我的做法跟网上主流的做法都不一样,但是感觉我自己的做法更好理解一点,于是特此拿出来写篇文章

相关的LeetCode题有如下几道:

二叉树的非递归遍历的大一统解法

之前,记得学二叉树的时候,老师讲的递归改成非递归遍历,后序遍历最为麻烦 ,需要写好多。但是这次做下来感觉都还挺简单的。我自己的理解就是把原本程序递归中需要自动压栈的过程,给改成人手工压栈了,所以压栈的顺序参考递归的时候压栈顺序就好。
前序遍历:

    vector<int> preorderTraversal(TreeNode* root) {
        vector<int> res;
        if(root==NULL)
            return res;
        stack<TreeNode*> s;
        s.push(root);
        while(!s.empty()){
            TreeNode* tmp = s.top();
            s.pop();
            res.push_back(tmp->val);
            if(tmp->right)      //一定要注意这里是先压right节点。
                s.push(tmp->right);
            if(tmp->left)
                s.push(tmp->left);
        }
        return res;
    }

可以看到用前序遍历二叉树的方法,在用递归的时候写,是先读这个节点本身,然后再去遍历左子节点,再遍历右子节点,所以递归压栈的时候,是将右子节点压在最底下的,其上面是继续遍历的左子节点。

中序遍历:

typedef pair<TreeNode*,bool> Tb
    vector<int> inorderTraversal(TreeNode* root) {
        vector<int> res;
        if(root==NULL)
            return res;
        stack<Tb> s;
        s.push(Tb(root,false));
        while(!s.empty()){
            auto tmp = s.top();
            s.pop();
            if(tmp.second == false){
                if(tmp.first->right)
                    s.push(Tb(tmp.first->right,false));
                s.push(Tb(tmp.first,true));
                if(tmp.first->left)
                    s.push(Tb(tmp.first->left,false));                
            }else{
                res.push_back(tmp.first->val);
            }
        }
        return res;
    }

这里面的思想是这样的,考虑到中序遍历的递归形式,所以等于需要进到一个节点的栈中两次——第一次直接进到这个节点的左子节点,第二次读取这个节点本身,并进到右子节点。
所以用一个bool变量记录其之前时候是否已经进入过了。
碰到一个新节点先将其压入栈中,标记为没有进过。等到第一次从栈中弹出这个节点,标记改为进过这个节点的左节点了,再压入栈中,然后压入这个节点的左节点。等再弹出这个节点的时候,就是中序遍历的读取部分了,读取节点的值,并将其右子节点压入栈中。
后序遍历:

typedef pair<TreeNode*,bool> Tb;

    vector<int> postorderTraversal(TreeNode* root) {
        vector<int> res;
        if(root==NULL)
            return res;
        stack<Tb> s;
        s.push(Tb(root,false));
        while(!s.empty()){
            auto tmp = s.top();
            s.pop();
            if(tmp.second == false){
                s.push(Tb(tmp.first,true));
                if(tmp.first->right)
                    s.push(Tb(tmp.first->right,false));
                if(tmp.first->left)
                    s.push(Tb(tmp.first->left,false));
            }else{
                    res.push_back(tmp.first->val);
            }
        }
        return res;
    }

可以看到后序遍历和中序遍历本质上一样的,只是中间压栈的时候有区别。可以看到这两种方法,甚至这三种方法都是来自于对于递归压栈的模拟。
举个下图的例子:

void f(){
    f(A);
    f(B);
    f(C);
}

如果递归的顺序如上面代码所示,那么改成非递归的形式,压栈的顺序就要倒着来:

void f(){
    stack.push(C);
    stack.push(B);
    stack.push(A);
}

因为是压在最下面的最后运行,最上面的最早运行。
所以中序遍历的时候先压右节点,再压当前节点,最后再压左节点。
后序遍历的时候,先压当前节点,再压右节点,最后压左节点。
其实前序也是一样,先压右节点,再压左节点,最后压当前节点,但是因为紧接着当前节点就又要被弹出所以,就省略了最后一步。

后来发现之前已经有个人的想法跟我类似了,详见下面这篇文章。

更简单的非递归遍历二叉树的方法


二叉树的迭代器实现

为什么说二叉树的中序遍历迭代器实现就是一个中序遍历的变种呢?因为迭代器只要每步操作都前进到一下步原本需要输出的节点上面就行。所以

class BSTIterator {
public:
    typedef pair<TreeNode*,bool> Tb;
    stack<Tb> s;
    BSTIterator(TreeNode *root) {
        if(root == NULL)
            return;
        s.push(Tb(root,false));
        pushAll();     //模块复用
    }

    /** @return whether we have a next smallest number */
    bool hasNext() {
        return !s.empty();
    }

    /** @return the next smallest number */
    int next() {
        Tb tmp = s.top();
        s.pop();
        pushAll();
        return tmp.first->val;
    }
private:
    void pushAll(){
        while(!s.empty()&&s.top().second == false){
            Tb tmp = s.top();
            s.pop();
            if(tmp.first->right)
                s.push(Tb(tmp.first->right,false));
            s.push(Tb(tmp.first,true));
            if(tmp.first->left)
                s.push(Tb(tmp.first->left,false));
        }
    }
};

最后到底STL里iterator是如何实现的呢

到这里其实问题还没有解决呢,C++的STL里是究竟是如何实现神奇的iterator呢,其既能++又能–,而且在set里面又新插入了元素,++,–还能保持正确。
先是在Github上找了个人的自己写的set练手程序它是底层加了个数组,即把其中序遍历的结果存到了数组之中。但是这样做的话,set插入元素的复杂度就会提高到 O(n) 得不偿失。
然后我又去找了STL中set的源码,发现其直接调用了更为底层的rb_tree,所以有跑去看rb_tree的源码发现其树中,每个节点还有一个父节点,所以其++操作就可以如下:

        rb_tree_iterator<Tree, IsConst>& operator++(void) noexcept  
        {  
            if (node->right != nullptr)  
            {  
                node = node->right;  
                while (node->left != nullptr)  
                    node = node->left;  
            }  
            else  
            {  
                node_pointer ptr = node->parent;  
                while (node == ptr->right)  
                {  
                    node = ptr;  
                    ptr = ptr->parent;  
                }  
                if (node->right != ptr)  
                    node = ptr;  
            }  
            return *this;  
        }  

所以set里iterator单次的++操作时间复杂度可能为 O(logn) 但是均摊意义上的复杂度还是 O(1)

空间复杂度为 O(1) Morris 遍历

需要用到 Morris 遍历的是LeetCode上的这道题
99. Recover Binary Search Tree
题目中要求用 O(1) 的空间复杂度去修复一颗BST

参考资料

二叉树迭代器算法这篇文章就是一个经典的教科书中的实现方式,也还行,一开始我还以为我的方法不能改成iterator的形式,就想采用这种呢。

9.3.1. General design concerns for a tree iterator
这篇文章实现的是一个前序迭代器,我的思路主要就是从这篇文章中来的。

另外还有一个 O(1) 空间 O(n) 时间复杂度的遍历二叉树方法:
Morris Traversal方法遍历二叉树(非递归,不用栈,O(1)空间)
可以学习一下

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值