美文网首页二叉树
二叉树的迭代遍历

二叉树的迭代遍历

作者: 铜炉 | 来源:发表于2021-01-05 22:28 被阅读0次

前言

前面讲过,二叉树是具有天然的递归结构的,那么为什么要迭代遍历呢?是不是多此一举?
其实这个问题可以思考一下,递归的好处是什么,坏处又是什么?
对我个人而言

优势:代码优雅,可读性强。
劣势:空间复杂度高

那么迭代能解决啥问题内?
首先要清楚的是递归的空间复杂度是怎么产生的,因为调用栈的增多,每次递归调用自身都会开辟一个新的栈空间,所以空间复杂度为O(n)。如果采用迭代的形式,可以避免的是调用栈的空间,通过增加多个指针,建立遍历的前后节点关联关系,就可以做到将空间复杂度降为O(1),这就是Morris遍历了。

Morris遍历也是一种迭代遍历二叉树的方式,今天我们先从基本的迭代入手,作为Morris的前奏~

我之前再刷leecode的时候注意到有些时候题目会拓展让你思考:"能否用迭代形式完成本题?",可能也是我对递归爱得深沉,很多时候我要憋好久才能从递归这个思路转化为迭代的思路,但是时间久了,发现这样每次都耗时好久转化思路也不是个办法,就想着找一个通用的方法论,能不能迅速帮助自己完成这件事。

首先先分析递归和迭代的不同。
递归的判断是要找到终点,迭代也是一样,迭代的本质是到达最后一个元素就停下来,这个最后一个元素就是终点。
此外,递归每次的调用自身都会有一个开辟一个新的栈空间,但是迭代没有,所以整理下关键点。

递归的终点是子问题的终点,迭代的终点是最后一个元素。
递归是天然消耗调用栈空间的,迭代没有额外的空间。

接下来我们要做的事情,就是用迭代的方式模拟递归的调用,也就是用迭代的方式吧递归的调用给表达出来。这里,我们先尝试处理一下二叉树的前序遍历。

public void traversal(TreeNode node) {
        if(node == null) {
            return;
        }
        // 前序遍历
        System.out.println(node.val);
        traversal(node.left);
        traversal(node.right);
    }

上面是二叉树的前序遍历,我们的迭代遍历就从模拟递归开始,

1、构造一个栈,来模拟递归的调用栈。
2、确定迭代的条件。

观察递归的遍历可得,两种形式会让当前栈的递归调用停止

1、node==null
2、当前栈的调用栈为空。

那么以此,我们先把迭代的模板写出来

public void traversal(TreeNode node) {
        Stack<TreeNode> stack = new Stack<>();
        /**
         * node与栈不同时为空,迭代就继续进行
         */
        while (node != null || !stack.empty()) {
            
        }
    }

接下来前序遍历的路径,简单来说就是:

记录当前节点->往左走,走一步记录一步->走不动了,回退一个->往右走一步->记录当前节点->往左走,走一步记录一步->走不动了,回退一个......

如此循环,所以根据这个思路,我们吧步骤拆分一下

1、记录当前节点(压栈)
2、往左走,走一步记录一步(不断移动指针到左子树,并压栈)
3、走不动了,回退一个(出栈)
4、往右走一步(移动指针到右子树)

然后,看代码

public List<Integer> preorderTraversal(TreeNode root) {
        List<Integer> res = new ArrayList<Integer>();
        if (root == null) {
            return res;
        }
        // 辅助栈
        Stack<TreeNode> stack = new Stack<>();
        TreeNode node = root;
        // 当前node与栈不同时为空就继续迭代
        while (!stack.isEmpty() || node != null) {
            // 前序遍历,当前节点不为空,记录当前节点,并压栈
            if (node!=null) {
                res.add(node.val);
                stack.add(node);
            }
            // 一直往左走,走一步,压一次栈,知道走不动
            while (node != null && node.left!=null) {
                node = node.left;
                res.add(node.val);
                stack.push(node);
            }
            // 回退一步
            node = stack.pop();
            // 往右走一步
            node = node.right;
        }
        return res;
    }

同理,分析一下中序遍历,中序遍历的路径,就是

把当前节点压栈->往左走,走一步压一次栈>走不动了,回退一个->记录当前节点->往右走一步->把当前节点压栈->往左走,走一步压一次栈->走不动了,回退一个......

继续拆分一下步骤

0、把当前节点压栈
1、往左走,走一步压一次栈(不断移动指针到左子树,并压栈)
2、走不动了,回退一个(出栈)
3、记录当前节点
4、往右走一步(移动指针到右子树)

public List<Integer> inorderTraversal(TreeNode root) {
        List<Integer> res = new ArrayList<Integer>();
        if (root == null) {
            return res;
        }
        // 辅助栈
        Stack<TreeNode> stack = new Stack<>();
        TreeNode node = root;
        // 当前node与栈不同时为空就继续迭代
        while (!stack.isEmpty() || node != null) {
            // 把当前节点压栈
            if(node !=null) {
                stack.push(node);
            }
            // 一直往左走,走一步,压一次栈,直到走不动
            while (node != null && node.left!=null) {
                node = node.left;
                stack.push(node);
            }
            // 回退一步
            node = stack.pop();
            //记录当前节点
            res.add(node.val);
            // 往右走一步
            node = node.right;
        }
        return res;
    }

最后是后序遍历,这个稍微复杂点,但是也不怕,还是先分析一下后序的调用路径。

压入当前节点->往左走,走一步压一次栈->走不动了,回退一个->往右走一步

到这里的路径和前序中序很相似,区别在于往右走这一步的点,此时有两种情况
1、节点为空,弹栈,记录节点
2、节点有值,继续向左走
这里可以注意到,在后序遍历中,有两次弹栈,一次是向左走走到头的的弹栈,一次是往右走到空节点的弹栈,然而只有从右节点的弹栈才可以记录节点信息,基于此,我们需要一个额外的指针,来判断当前是否是右节点回退的弹栈,代码如下

    public List<Integer> postorderTraversal(TreeNode root) {
        List<Integer> res = new ArrayList<Integer>();
        if (root == null) {
            return res;
        }
        // 辅助栈
        Stack<TreeNode> stack = new Stack<>();
        TreeNode node = root;
        TreeNode prev = null;
        // 当前node与栈不同时为空就继续迭代
        while (!stack.isEmpty() || node != null) {
            // 把当前节点压栈
            if (node != null) {
                stack.push(node);
            }
            // 一直往左走,走一步,压一次栈,直到走不动
            while (node != null && node.left != null) {
                node = node.left;
                stack.push(node);
            }
            // 回退一步
            node = stack.pop();
            // 判断右节点是否为空 || 判断是否是右节点回退
            if (node.right == null || node.right == prev) {
                // 记录当前节点
                res.add(node.val);
                // 记录当前节点,为了判断下次回退是否是右节点回退
                prev = node;
                //因为执行到这里,当前的node左右子树都已经遍历完成,将node置为空,下一次进入循环直接继续弹栈
                node = null;
            } else {
                // 右节点不为空,且是从左节点回退,右子树还未遍历,往右走一步,并将当前节点压栈
                stack.push(node);
                node = node.right;
            }

        }
        return res;
    }

到这里,迭代的前中后序遍历也梳理完成,代码的写法上主要是为了模拟递归的调用,还有可以优化的空间,不过这样的一种思路,可以便与以后将其他的递归调用转化为迭代。

在写完写篇文章之后,我们可以进行空间复杂度为O(1)的Morris遍历了。

相关文章

  • 算法-二叉树算法总结

    二叉树算法总结 1 二叉树的遍历 1.1 前序遍历 递归 迭代 1.2 中序遍历 递归 迭代 1.3 后序遍历 递...

  • 二叉树算法基础

    二叉树节点 1 前序遍历 1.1 递归遍历 1.2 迭代遍历 2 中序遍历 2.1 递归遍历 2.2迭代遍历 3 ...

  • 07-13:二叉树review1

    二叉树review1: 1、二叉树结构 1)二叉树的遍历 0)递归/迭代实现 前/中/后序遍历 递归 迭代 层次遍...

  • 二叉树的前序、中序、后序遍历迭代实现

    二叉树的前序、中序、后序遍历迭代实现 二叉树的前序遍历,迭代实现 根-左-右 思路:1、 借用栈的结构2、 先...

  • 3.有关二叉树的算法

    1.分层遍历二叉树:宽度优先遍历 2.分层遍历应用,按层打印二叉树 3.前序遍历二叉树 4.前序遍历,迭代 5.中...

  • 算法精选题总结之二叉树类

    1.二叉树的中序遍历中序遍历的迭代方法中序遍历的递归方法2.二叉树前序遍历3.二叉树后续遍历4.二叉树的最近公共祖...

  • LeetCode 二叉树的中序遍历

    二叉树的中序遍历 二叉树的中序遍历 顺序其实就是 左 中 右用递归的解法来解: 迭代解法:

  • 二叉树的Morris遍历

    前言 前面已经进行过二叉树的递归遍历和迭代遍历,在前两种遍历中,二叉树遍历的空间复杂度都是O(n),这是因为不管是...

  • 二叉树遍历方法大全(包含莫里斯遍历)

    前言 二叉树的遍历可能大家都比较熟悉了,这篇文章主要介绍了三种二叉树的遍历方法——递归、迭代和莫里斯遍历,他们各自...

  • Leetcode 144 二叉树的前序遍历

    二叉树的前序遍历 题目 给定一个二叉树,返回它的 前序 遍历。 示例: 进阶: 递归算法很简单,你可以通过迭代算法...

网友评论

    本文标题:二叉树的迭代遍历

    本文链接:https://www.haomeiwen.com/subject/hrgvoktx.html