5种方法完成二叉树后序遍历(Java实现)

这篇博客介绍了5种不同的非递归方法来完成二叉树的后序遍历,包括递归思路、先序遍历逆序、双栈法、记录末次输出以及另一种记录末次输出的方法。每种方法都有详细的解释和Java代码实现,适合LeetCode等算法题的练习。

[1] 代码最少-----递归

递归的思想非常简单,宏观上来讲,就是"先访问做孩子,再访问右孩子,最后访问自己";限定好递归结束条件即可.

    public List<Integer> postorderTraversal(TreeNode root) {
        List<Integer> res = new ArrayList<>();
        if(root == null)
            return res;
        res.addAll(postorderTraversal(root.left));
        res.addAll(postorderTraversal(root.right));
        res.add(root.val);
        return res;
    }

[2] 非递归之----先序遍历的逆序

这种方法思想非常有趣,实际上就是首先做了类似先序遍历的"根右左"遍历,然后将之逆序存储或者输出,就得到了"左右根"的遍历顺序.

    public List<Integer> postorderTraversal(TreeNode root) {
        List<Integer> res = new ArrayList<>();
        if(root == null)
            return res;
        LinkedList<TreeNode> stack = new LinkedList<>();
        stack.add(root);
        while(!stack.isEmpty()){
            TreeNode node = stack.pollLast();
            res.add(0, node.val);
            // 先压左后压右,就会先弹出右后弹出左
            if(node.left != null){
                stack.add(node.left);
            }
            if(node.right != null){
                stack.add(node.right);
            }
        }
        return res;
    }

[3] 非递归之----双栈法(添加一个Flag栈)

这种方法的思想是用两个栈,一个栈stack1用来记录遍历过程(存储的元素是访问到的节点),另一个栈stack2用来存储与stack1中元素一一对应的遍历标识Flag.
要理解这种方法,首先要明白在这段代码中每一个节点会被访问三次,第一次访问是去处理节点的左孩子,第二次是去处理右孩子,第三次的时候输出.对一个节点的三次访问如下图
在这里插入图片描述上图中蓝色的线表示对节点的访问过程,可以发现每个节点被蓝色的线路过三次,即每个节点会被访问三次.

遍历标识的作用是用来在程序某一次(实际上是第三次访问到)又访问到这个节点的时候,告诉程序这个节点的左右子节点是否已经都被遍历了,如果是,就可以直接输出这个节点,反之则再次对节点进行一次访问.

具体的工作思想如下:
程序第一次访问到一个树节点时,将之压入stack1,并在stack2中添加一个"LEFT"标记,用来标识接下来要去访问这个节点的左孩子;程序第二次访问到这个节点时,发现stack2中这个节点的标记是"LEFT",就知道只访问了这个节点一次,刚才只处理了它的左孩子,就将stack2中对应的标记置换为"RIGHT",用来表示接下来要去处理这个树节点的右孩子;程序第三次访问到这个节点时,发现这个节点在stack2中对应的标记是"RIGHT",就知道已经两次访问了这个节点,它的左右孩子都被处理完了,直接将它输出,然后将stack1和stack2中对应的栈帧都弹出.
实际操作时,在代码中需要先判断标记是不是"RIGHT"从而决定要不要再进行一次访问(实际上就是要不要进行第三次访问)

    public List<Integer> postorderTraversal(TreeNode root){
        List<Integer> res = new ArrayList<>();
        if(root == null){
            return res;
        }
        // 用两个常量来表示访问标志
        final int LEFT = 1, RIGHT = 2;
        TreeNode cur = root;
        // 这是两个辅助栈,一个用来存元素,一个用来存标志
        Stack<TreeNode> stack1 = new Stack<>();
        Stack<Integer> stack2 = new Stack<>();
        while(cur != null || !stack1.isEmpty()){
            // 这里是第一次访问节点,在stack1和stack2中添加栈帧(stack2中初始添加都是LEFT)
            while(cur != null){
                stack1.push(cur);
                stack2.push(LEFT);
                cur = cur.left;
            }
            // 这里是对节点的一次访问(不能确定是第二次还是第三次),需要做判断,如果标志位是RIGHT就表示已经完成了两次访问,直接输出并弹出栈帧
            while(!stack1.isEmpty() && stack2.peek() == RIGHT){
                TreeNode temp = stack1.pop();
                res.add(temp.val);
                stack2.pop();
            }
            // 如果标志位是LEFT,就表示只访问过该节点一次,即当前是第二次访问,就需要将标志位改为RIGHT,然后去处理该节点的右孩子
            if(!stack1.isEmpty() && stack2.peek() == LEFT){
                stack2.pop();
                stack2.push(RIGHT);
                cur = stack1.peek();
                cur = cur.right;
            }
        }
        return res;
    }

[4] 非递归之----记录末次输出

这种方法可以看作是上一种的优化,与方法[3]相比,用一个变量lastPrintNode来代替了stack2这个栈.

这个方法的思想其实很简单:
用变量lastPrintNode来记录最后一次输出的节点,这样,当cur指针访问的当前节点满足以下两个条件中的一个时,就做输出处理:①该节点没有右孩子;②该节点的右孩子就是lastPrintNode,即刚才已经被输出了

    public List<Integer> postorderTraversal(TreeNode root) {
        TreeNode cur = root;
        TreeNode lastPrintNode = null;
        List<Integer> res = new ArrayList();
        Deque<TreeNode> stack = new ArrayDeque();
        while(!stack.isEmpty() || cur != null)
        {
            while(cur != null)
            {
                stack.push(cur);
                cur = cur.left;
            }
            cur = stack.peek();
            while(!stack.isEmpty() && (cur.right == null || cur.right == lastPrintNode))
            {
                res.add(cur.val);
                lastPrintNode = cur;
                stack.pop();
                cur = stack.peek();
            }
            if(cur != null)
                cur = cur.right;
        }
        return res;
    }

[5] 非递归之----记录末次输出的另一种方法

这种方法与方法[4]很像,也是记录最后一次输出,不同之处在于树节点的压栈方式和出栈输出的条件不同.

在方法[4]中,压栈方式是"无脑一路左子树,左子树走不通了再尝试右子树,然后再无脑一路左子树";出栈条件是"右孩子为空或者右孩子已经被遍历完毕";而在方法[5]中,压栈的方式是重复"根右左"的顺序进行压栈,然后弹出的时候按照"左右根"的顺序进行是否出栈的判断和处理(其实这种方法有点像方法[2]和方法[4]的结合),比较有意思的是这个方法的出栈条件有三个:“节点没有左右孩子,或者节点的右孩子已经被输出,或者节点的左孩子已经被输出”.

如果读者看完了方法[4],那么对于"节点没有左右孩子,或者节点的右孩子已经被输出"两个条件已经有了理解.你可能会迷惑的是第三个条件"节点的左孩子已经被输出".只输出了左孩子的话,怎么可以将之弹出呢?

其实要想明白这个问题,得再琢磨一下这个方法的压栈方式,我们刚才已经说过:是重复"根右左"的顺序进行压栈,然后弹出的时候按照"左右根"的顺序进行是否出栈的判断和处理.问题就在这里,在判断是否要出栈时,我们所访问到的任意一个节点,都是相对于它的子树的根节点,根据"左右根"的出栈顺序,它的左右孩子刚刚一定都被访问过了,在这种情况下,如果它的左孩子是刚刚输出的节点,那就说明----这个节点没有右孩子,输出了左孩子之后就可以输出这个节点.这就是为什么会有"节点的左孩子已经被输出"这个出栈条件的原因咯.这种情况对应到图上就是只有左孩子没有右孩子的节点,如下图所示.
在这里插入图片描述上图中的节点<4>和<7>就是满足"节点的左孩子已经被输出"这一条件而出栈的.

    public List<Integer> postorderTraversal(TreeNode root) {//非递归写法
        List<Integer> res = new ArrayList<Integer>();
        if(root == null)
            return res;
        Stack<TreeNode> stack = new Stack<TreeNode>();
        TreeNode lastPrintNode = null;
        stack.push(root);
        while(!stack.isEmpty()){
            TreeNode cur = stack.peek();            
            if((cur.left == null && cur.right == null) ||
            (lastPrintNode != null && (lastPrintNode == cur.left || lastPrintNode == cur.right))){ 
                            //如果当前结点左右子节点为空或上一个访问的结点为当前结点的子节点时,当前结点出栈
                res.add(cur.val);
                lastPrintNode = cur;
                stack.pop();
            }else{
                if(cur.right != null) stack.push(cur.right); //先将右结点压栈
                if(cur.left != null) stack.push(cur.left);   //再将左结点入栈
            }            
        }
        return res;
    }

[6] 总结

与先序遍历和中序遍历相比,后序遍历的非递归要难一些,这里有4种非递归方法任君选择,以上代码均可以直接复制粘贴到LeetCode里直接进行测试哦!有帮助的话记得点赞.

如果转载请标明出处.

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值