二叉树遍历-----前序后序迭代遍历的新思路

leetcode二叉树遍历

前序遍历:https://leetcode.com/problems/binary-tree-preorder-traversal/

中序遍历:https://leetcode.com/problems/binary-tree-inorder-traversal/

后序遍历:https://leetcode.com/problems/binary-tree-postorder-traversal/

二叉树遍历的迭代算法,网上有很多。这里记录几种特别的遍历方法。

前序遍历:

前序遍历的普通算法,思路比较简单,很多书里面迭代方法的基本都采用这种方法,用堆栈来模拟递归的思路。

class Solution {
public:
    vector<int> preorderTraversal(TreeNode* root) {
        vector<int> ret;
        stack<TreeNode*> stackTree;
        if(root == NULL)
            return ret;
        TreeNode* curr = root;
        while(1){
            while(curr){
                stackTree.push(curr);
                ret.push_back(curr->val);
                curr = curr->left;
            }
            if(stackTree.empty())
                return ret;
            curr = stackTree.top();
            stackTree.pop();
            curr = curr->right;
        }
        
    }
};

后来发现一种算法,解题的算法实现非常的简单。

先看另一种算法的代码:

class Solution {
public:
    vector<int> preorderTraversal(TreeNode* root) {
        vector<int> ret;
        if(root == NULL)
            return ret;
        stack<TreeNode*> stackTree;
        stackTree.push(root);
        TreeNode* cur = NULL;
        while(!stackTree.empty()){
            cur = stackTree.top();
            stackTree.pop();
            ret.push_back(cur->val);
            if(cur->right !=NULL)
                stackTree.push(cur->right);
            if(cur->left != NULL)
                stackTree.push(cur->left);
        }
        return ret;
    }
};

要理解这个算法,首先要理解下面的这句语句:

stackTree.push(cur->right);

这句语句,不能把它简单想成是把右节点入栈了,而是要想象成,整个右子树入栈了,这样对于理解整个算法可能会有一定的帮助。

整个算法的思想就是,先把整棵树(根节点)入栈,然后进行循环。在循环里面,用curr这个变量来表示子树的根节点。所以按照前序的思想,首先输出根节点,然后再在栈里面存入右子树,然后是左子树。然后下一次迭代的时候,出栈的就是左子树的根节点,左子树继续循环,再栈里面存入它的右子树和左子树,然后继续,直到左子树全部输出了,然后右子树的根节点出栈,在输出右子树……直到把整棵树输出。

算法具体操作就是先输出根节点,然后输出左子树,遍历完左子树,然后再输出右子树,遍历右子树。输出子树的过程采用迭代的方法完成。

这种算法有别于上面的算法,这种算法,感觉是使用迭代的方法,实现了递归的思想。而且实现起来特别的简单。就是先载入根节点,然后载入右子树的根节点,在载入左子树的根节点。然后进行迭代。

如果不是很明白,自己随便找个例子来看看,可能会清楚一点的。


中序遍历:

中序遍历,采用的方法就是和上面的前序遍历的第一种方法,只是把输出的位置换一下,这个好像暂时没有什么特别的方法。

代码:

class Solution {
public:
    vector<int> inorderTraversal(TreeNode* root) {
        stack<TreeNode*> stackNode;
        vector<int> inOrder;
        while(1)
        {
            while(root)
            {
                stackNode.push(root);
                root = root->left;
            }
            
            if(stackNode.empty())
                return inOrder;
            root = stackNode.top();
            stackNode.pop();
            inOrder.push_back(root->val);
            root = root->right;
        }
        
    }
};

后序遍历:

后序遍历是三种遍历里面最麻烦的,因为普通的方法,要记录节点是否是第二次访问,这种方法,这里不多说。下面有两种特别的方法,可以不用对节点做记录。

方法一:

第一种方法,是采用后序遍历和前序遍历的逆序关系。

前序遍历,遍历的顺序是中左右,而后续遍历,遍历的顺序是左右中,那把后续遍历翻一下序,就是中右左,和前序遍历基本是一样的,只是先访问右子树,再访问左子树。所以实现后序遍历的一种方法就是,先实现中右左的遍历,然后把遍历的结果反序。得到的就是后序遍历了。

代码如下:

class Solution {
public:
    vector<int> postorderTraversal(TreeNode* root) {
        vector<int> ret;
        if(root == NULL)
            return ret;
        stack<TreeNode*> stackTree;
        stackTree.push(root);
        TreeNode* cur = NULL;
        while(!stackTree.empty()){
            cur = stackTree.top();
            stackTree.pop();
            ret.push_back(cur->val);
            if(cur->left !=NULL)
                stackTree.push(cur->left);
            if(cur->right != NULL)
                stackTree.push(cur->right);
        }
        reverse(ret.begin(), ret.end());
        return ret;
    }
};
观察一下,就可以发现,这个代码基本和上面的前序遍历是一模一样的,只是把子树入栈的顺序换了一下。由原来的先入右子树,再入左子树换成了先入左子树,再入右子树,实现中右左的遍历,然后对遍历的结果进行翻转就可以了。


方法二:

上面的代码有点取巧,遍历的过程并不是真正意义上的后序遍历,只是遍历的输出结果和后序遍历一样的罢了。

第二种方法,是实现正真的后序遍历。

先给出代码实现:

class Solution {
public:
    vector<int> postorderTraversal(TreeNode* root) {
        stack<TreeNode*> stackTree;
        vector<int> ret;
        if(root == NULL)
            return ret;
        TreeNode* pre = root;
        TreeNode* curr;
        stackTree.push(root);
        while(!stackTree.empty()){
            curr = stackTree.top();
            if(curr->left && pre != curr->left && pre != curr->right)
                stackTree.push(curr->left);
            else if(curr->right && pre != curr->right)
                stackTree.push(curr->right);
            else{
                ret.push_back(curr->val);
                pre =curr;
                stackTree.pop();
            }
            
        }
        return ret;
    }
    
};
在这个方法里面,采用一个新的数据pre,来记录给出结果的上一个遍历输出的节点。是输出的节点,中间过程节点,没有输出的不会被赋予pre。而数据curr则是表示现在指向的节点。

首先,给pre赋值root。考虑中间过程,pre和curr的关系有一下几种:

1.pre既不是curr的左节点,又不是curr的右节点,此时就表示,中间节点还没有遍历左右节点,所以要把左节点入栈。

2.pre不是curr的右节点,当情况1不成立的时候,情况2代表pre为中间节点的左节点或者左节点为空,接下来需要遍历中间节点的右节点,所以把右节点入栈。

3.pre是curr的右节点,此时代表,中间节点的左右节点都已经遍历过了,可以输出中间节点了。


这里有一个关键的地方就是,每次迭代开始给curr赋值的时候,只能取出栈顶元素,但是不能删除栈顶元素,因为这只是栈顶元素的第一次访问,还要进行第二次访问,才能删除栈顶元素。所以栈顶元素的删除是要在情况3下,中间元素输出后,才能删除栈顶元素的。

这个方法,要注意2个地方:

1.pre的初值不能使null,对于这个方法来说,最好的办法就是给他赋值为root。因为如果给pre赋值null的话,那如果输入是[1,2].那右子树就是空的,算法会误以为pre所指向的地址是右子树,所以会直接输出root。理论上来说,pre代表的是上一个遍历输出的节点。所以,pre的初值要满足一个条件,即pre刚开始的时候,不能和树里面的节点相同,即对树里面的所有节点,一定要满足下面的条件:

pre != curr->left && pre != curr->right, curr表示树的所有节点。

所以pre可以使root,root明显满足上面的条件。

2.            else if(curr->right && pre != curr->right)可不可以用else if(curr->right && pre == curr->left)来代替?

这句代码对表示上面三种情况的情况2,:在情况1不成立的条件下,pre不是curr的右节点。

情况2和pre是curr的左节点是不等价的。因为curr的左节点可能是空的,此时,也是满足情况2的,但是不满足pre是curr的左节点,所以上面两句话是不等价的。

那 else if(curr->right && pre != curr->right) 可不可以用   else if(curr->right && (pre == curr->left || curr->left == NULL))来代替?

同样不行,后面虽然把left是否是空考虑进去了,但是还是不能。因为,当左子树是空时,右子树不为空,这个条件始终成立,整个迭代就会在这里形成一个死循环。所以,情况二只能用源代码中的语句来表示,这是这个算法中特别要注意的地方。


总结:

对于后序遍历,第一种方法,实现起来比较简单,但确定就是他遍历的过程不是真正的后序遍历,只是给出了后序遍历的结果。

第二种方法过程采用了后序遍历,但是代码的书写中,要注意的点比较多,特别是第二点,针对第二种情况,要载入右节点的情况,这个地方特别容易出错,乍看有很多不同的写法,但是实际上,应该只有这一种写法,其他很多写法都是错误的,至少罗列的两种都是错的。





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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值