二叉树的遍历

二叉树的遍历主要有3种模式,前序,中序和后序。其递归版本,非常简单,大致如下:

void Traversal(TreeNode* root){
    if(!root)
        return;
    // visit root (preorder)
    Traversal(root->left);
    // visit root (inorder)
    Traversal(root->right);
    // visit root (postorder)
}

根据要求不同,分别在对应地方访问节点就可以了。

但是一方面,这种递归版本由于函数调用会影响效率,另一方面面试时很多面试官会让你转化成非递归版本。本来说不难,但是为了给面试官留下好印象,还是要非常熟悉这一套的,要求0失误快速写出来。


非递归版本

由于是递归转非递归,因此肯定会用到栈,即用栈去模拟计算机函数调用的过程。

前序遍历非常容易模拟,因为root先于其左右子树被访问,因此每次从栈顶取到root后直接访问,然后将其右儿子和左儿子依次压入栈中就可以了(注意先压右儿子,因为按照栈是FILO,所以右儿子会被后访问到)。

 vector<int> preorderTraversal(TreeNode *root) {
        vector<int> ans;
        if(root == NULL)
            return ans;

        stack<TreeNode*> st;
        st.emplace(root);

        while(!st.empty()){
            //访问root
            TreeNode * tmp = st.top();
            st.pop();
            ans.emplace_back(tmp->val);

            //先压右儿子
            if(tmp->right != NULL) st.emplace(tmp->right);
            //再压左儿子
            if(tmp->left != NULL) st.emplace(tmp->left);
        }
        return ans;
    }

但是到了中序和后序遍历就稍微麻烦一点了,因为root并不是第一个被访问到,因此当我们从栈顶弹出root时,会遇到一个麻烦,即它是第一次被访问到,还是已经访问完它的左右子树了呢?

如果是第一次被访问到,显然现在我们还不能正式遍历它,而是要先遍历它的子树,那么这个时候我们就需要把它暂时放回到栈中,等遍历完它的子树后,再从栈中把它取出来遍历。 那么如何判断它的左右子树被遍历到,当然我们可以给每个节点加一个变量来标记它是否已经被遍历过;或者利用C++中的pair作为栈的基本元素, 除了在栈中记录节点,同时在栈中记录每个节点进出栈的次数,通过进出栈的次数决定它的左右子树是否被遍历完了。

但是不论哪种方法,都要利用额外的空间,显然这样做虽然能行,但是不够好。当然有更好的解决办法。

下面我们先考虑中序遍历,当我们拿到一个节点root时:

  1. 显然最先遍历的是它的左子树(记为 T1 T1 的根记为 l1 ),然后才回来访问root和root的右子树;
  2. 而当我们第一次访问子树 T1 ,应该先遍历它的左子树(记为 T2 T2 的根记为 l2 ),然后才是 l1 和它的右子树
  3. 以此类推,每次都是先遍历节点的左子树,然后才是节点本身

    那么既然这样,我们拿到一个节点root后先按顺序将它以及 l1,l2,,lh 压入栈中。然后再从栈中取出元素,这时栈顶元素一定是树中最左下角的元素 lh ,此时我们遍历它,然后开始遍历 lh 的右子树就可以了。当访问完 lh 的右子树时,我们再从栈顶弹出元素,这个元素按照我们的压栈顺序一定是 lh 的父节点 lh1 ,而此时由于以 lh 为根的子树恰好刚遍历完,即 lh1 的左子树遍历完了,自然我们就需要遍历 lh1 ,然后再遍历 lh1 的右子树就可以了。

以此类推,这样我们就可以完成中序遍历。

vector<int> inorderTraversal(TreeNode *root) {
        vector<int> ans;

        TreeNode* t = root;
        stack<TreeNode* > st;
        while(t != NULL || !st.empty()){
            //如果t不空,将t左边的后代(即l1,l2...)依次压入栈中
            while( t != NULL){
                st.emplace(t);
                t = t->left;
            }
            if(!st.empty()){
                //此时栈顶元素的左子树一定遍历完了,那么我们开始遍历栈顶元素
                t = st.top();
                st.pop();
                ans.emplace_back(t->val);

                //然后开始遍历栈顶元素的右子树
                t = t->right;
            }
        }
        return ans;
    }

最后是后序遍历,还是用压栈的方法,但是比上面简单但又巧妙很多。由于后序遍历的特点是节点本身最后被访问到,这样就给了我们一个简单的方法判断某个节点root的左右子树是否被遍历到:

因为我们想遍历root时,一定是其右子树被遍历完的时候。而遍历右子树时,最后遍历到的一定是右子树的根,即root的右儿子。此时我们只需要一个变量pre, 用来记录之前遍历的那个节点,。然后和root的右儿子做一下比较,如果两者相同,说明右子树已经遍历完,这时我们直接遍历root就可以了。当然遇到root没有右儿子时,说明右子树为空,这样我们就要拿pre和root的左儿子比较。如果root连左儿子都没有,那么说明它是叶子节点,直接访问就可以了。

vector<int> postorderTraversal(TreeNode *root) {
        vector<int> ans;
        if(root == NULL)
            return ans;

        //pre表示之前遍历到的那个节点
        TreeNode *pre = NULL, *tmp;

        stack<TreeNode* >st;
        st.emplace(root);

        while(!st.empty()){
            tmp = st.top();
            //如果pre等于tmp的右儿子;或者没有右儿子的情况下,pre等于其左儿子,或者甚至连左儿子都没有。说明tmp的左右子树已经访问完了,那么我们开始访问tmp,并更新pre
            if( (tmp->right != NULL && tmp->right == pre) ||
                (tmp->right == NULL && (tmp->left == pre ||
                tmp->left == NULL)) ){

                pre = tmp;
                st.pop();
                ans.emplace_back(tmp->val);
            }
            //否则将tmp的右儿子和左儿子加入栈中
            else{
                if(tmp->right != NULL) st.emplace(tmp->right);
                if(tmp->left != NULL) st.emplace(tmp->left);
            }
        }
        return ans;
    }

除了上面这些方法,还有其他用栈模拟的,这里不再冗述。不过需要指的提一下的是,如果允许遍历时修改节点指针,只要保证遍历完之后能恢复原状,那么可以不用栈就完成遍历,即O(1)的空间,具体的方法就是Morris Traversal,有兴趣的同学自己去看看吧,这种方法能够在空间要求极为严格的情况下,完成二叉树的搜索。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值