一文讲清二叉树前中后序遍历递归改循环,准备面试不再焦虑!

秋招季节又快来临了,不管是笔试还是面试白板编程,二叉树前中后序遍历是非常常见的热身题,递归写法想必同学们都是信手拈来。但是面试官看你写得这么熟,小伙骨骼精奇,是块扣腚的好材料,必须好好考察,下面问题就来了。递过有什么局限性?blablabla...嗯,你说得很有道理,那就用循环重写一遍吧。这?...很多同学就卡壳了。今天这篇文章就是来解决这个问题的,一次性把问题讲透。

关于堆栈

首先我们复习一下什么是堆栈,堆栈的原则是先进后出。第一个进去的元素总是最后一个出来,最后一个进去的元素总是第一个出来。实际生活中我们的很多行为都利用了堆栈。我们来举个实际例子:比如你正在吃饭

  1. 发现作业没写完,开始写作业。任务堆栈:[吃饭]
  2. 作业写到一半,口渴了去喝水。任务堆栈:[吃饭,作业]
  3. 喝水路上看到小猫,去撸一把。任务堆栈:[吃饭,作业,喝水]
  4. 撸完猫了,继续去喝水。任务堆栈:[吃饭,作业]
  5. 喝完水了,开始写作业。任务堆栈:[吃饭]
  6. 作业写完,开始吃饭。任务堆栈:[]
  7. 饭吃完了,该干啥干啥。

从上面例子可以看出,当我们正在进行一个任务被打断时,我们把正在进行的任务放到堆栈中,处理完手里的任务后,看看堆栈里有没有之前做到一半的,每次都把最近的拿出来继续处理。其实递归的函数调用也是这个原理。下面我们就开始逐步讲解如何把递归的遍历改写成循环。

所谓前中后序遍历区别在与根节点的位置在哪里:

  1. 前序:根节点 - 左子树遍历展开 - 右子树遍历展开
  2. 中序:左子树遍历展开 - 根节点 - 右子树遍历展开
  3. 后序:左子树遍历展开 - 右子树遍历展开 - 根节点

在开始编写代码之前我们会手工模拟走一遍。(x)代表原来树里值为x的节点,左右子树指向不变;x代表值为x的节点,左右子树指向null,是复制出来的节点。

前序遍历

  • 根节点放入堆栈。堆栈:[(1)]
  • 弹出栈顶元素1,值添加到结果数组,因为先处理左子树,再处理右子树,所以注意入栈顺序。结果:[1],堆栈:[(3), (2)]
  • 弹出栈顶元素2,值添加到结果数组,再处理该节点的子树。结果:[1, 2],堆栈:[(3), (5), (4)]
  • 弹出栈顶元素4,值添加到结果数组,子树为空,不入栈。结果:[1, 2, 4],堆栈:[(3), (5)]
  • 弹出栈顶元素5,值添加到结果数组,左子树为空,只需要右子树入栈。结果:[1, 2, 4, 5],堆栈:[(3), (8)]
  • 弹出栈顶元素8,值添加到结果数组,子树为空,不入栈。结果:[1, 2, 4, 5, 8],堆栈:[(3)]
  • 弹出栈顶元素3,值添加到结果数组,再处理该节点的子树。结果:[1, 2, 4, 5, 8, 3],堆栈:[(7), (6)]
  • 弹出栈顶元素6,值添加到结果数组,子树为空,不入栈。结果:[1, 2, 4, 5, 8, 3, 6],堆栈:[(7)]
  • 弹出栈顶元素7,值添加到结果数组,子树为空,不入栈。结果:[1, 2, 4, 5, 8, 3, 6, 7],堆栈:[]
  • 堆栈为空,结束。

所以很容易把以上的步骤翻译成代码:在LintCode验证代码是否正确。

public List<Integer> preorderTraversal(TreeNode root) {
    List<Integer> ret = new ArrayList<>();
    if (root != null) {
        Stack<TreeNode> stack = new Stack<>();
        stack.push(root); // 初始化堆栈
        while (!stack.isEmpty()) {
            TreeNode node = stack.pop(); // 弹出栈顶元素处理
            ret.add(node.val);
            if (node.right != null) {
                stack.push(node.right); // 右子树不为空先入栈,然后再判断左子树。
            }
            if (node.left != null) {
                stack.push(node.left);
            }
        }
    }
    return ret;
}

中序遍历

中序遍历要注意了,先遍历左子树,再显示根节点,最后遍历右子树。凭感觉入栈顺序应该是:右子树,根节点,左子树。注意,这里根节点入栈的时候只需要一个值,所以需要复制节点,将左右子树置为空。因为堆栈里每个元素是node,如果左右子树不为空就代表这个节点不是叶子节点不能直接输出,而是要根据遍历顺序先处理完左右子树。

  • 根节点放入堆栈。堆栈:[(1)]
  • 弹出栈顶元素1,因为左右子树不为空,根据遍历顺序先处理完左子树再当前节点,再处理右子树,所以要注意入栈顺序。注意:因为只有左右子树都为空的节点才能输出,所以复制节点,左右子树置为空,然后再入栈。结果:[],堆栈:[(3), 1, (2)]
  • 弹出栈顶元素2,按遍历顺序压栈。结果:[],堆栈:[(3), 1, (5), 2, (4)]
  • 弹出栈顶元素4,因为左右子树为空,所以直接输出。结果:[4],堆栈:[(3), 1, (5), 2]
  • 弹出栈顶元素2,因为左右子树为空,所以直接输出。结果:[4, 2],堆栈:[(3), 1, (5)]
  • 弹出栈顶元素5,因为左子树为空,所以直接输出,然后再右子树入栈。结果:[4, 2, 5],堆栈:[(3), 1, (8)]
  • 弹出栈顶元素8,因为左右子树为空,所以直接输出。结果:[4, 2, 5, 8],堆栈:[(3), 1]
  • 弹出栈顶元素1,因为左右子树为空,所以直接输出。结果:[4, 2, 5, 8, 1],堆栈:[(3)]
  • 弹出栈顶元素3,因为左右子树不为空,按遍历顺序压栈。结果:[4, 2, 5, 8, 1],堆栈:[(7), 3, (6)]
  • 弹出栈顶元素6,因为左右子树为空,所以直接输出。[4, 2, 5, 8, 1, 6],堆栈:[(7), 3]
  • 弹出栈顶元素3,因为左右子树为空,所以直接输出。[4, 2, 5, 8, 1, 6, 3],堆栈:[(7)]
  • 弹出栈顶元素7,因为左右子树为空,所以直接输出。[4, 2, 5, 8, 1, 6, 3, 7],堆栈:[]
  • 堆栈为空,结束。

总结一下,翻译成代码步骤如下:

  • 根节点入栈
  • 如果堆栈不为空
    • 弹出栈顶元素
    • 如果右子树不为空,入栈。
    • 判断左子树是否为空
      • 是!新建一个节点,复制当前节点的值,左右子树置为空,入栈,然后左子树入栈。
      • 否!直接输出当前值。

LintCode验证代码是否正确。

public List<Integer> inorderTraversal(TreeNode root) {
    List<Integer> ret = new ArrayList<>();
    if (root != null) {
        Stack<TreeNode> stack = new Stack<>();
        stack.push(root);
        while (!stack.isEmpty()) {
            TreeNode node = stack.pop();
            if (node.right != null) {
                stack.push(node.right);
            }
            if (node.left == null) {
                ret.add(node.val);
            } else {
                TreeNode tmp = new TreeNode(node.val); // 构造函数默认左右子树为null
                stack.push(tmp);
                stack.push(node.left);
            }
        }
    }
    return ret;
}

后序遍历

既然中序遍历搞定了,后续遍历就是根据中序遍历的代码照葫芦画瓢了。简单说就是如果该节点的左右子树不全为空的话,复制当前节点的值入栈,然后如果右子树不为空,入栈,左子树不为空,入栈。为了方便大家理解,具体步骤还是不辞辛苦再写一遍吧。

  • 根节点放入堆栈。堆栈:[(1)]
  • 弹出栈顶元素1,因为左右子树不全为空,所以按照顺序入栈。结果:[],堆栈:[1, (3), (2)]
  • 弹出栈顶元素2,因为左右子树不全为空,所以按照顺序入栈。结果:[],堆栈:[1, (3), 2, (5), (4)](注意:这里节点2是复制出来的,左右子树都置为空。)
  • 弹出栈顶元素4,因为左右子树为空,所以直接输出。结果:[4],堆栈:[1, (3), 2, (5)]
  • 弹出栈顶元素5,因为右子树不为空,所以按照顺序入栈。结果:[4],堆栈:[1, (3), 2, 5, (8)]
  • 弹出栈顶元素8,因为左右子树为空,所以直接输出。结果:[4, 8],堆栈:[1, (3), 2, 5]
  • 弹出栈顶元素5,因为左右子树为空,所以直接输出。结果:[4, 8, 5],堆栈:[1, (3), 2]
  • 弹出栈顶元素2,因为左右子树为空,所以直接输出。结果:[4, 8, 5, 2],堆栈:[1, (3)]
  • 弹出栈顶元素2,因为左右子树不全为空,所以按照顺序入栈。结果:[4, 8, 5, 2],堆栈:[1, 3, (7), (6)]
  • 弹出栈顶元素5,因为左右子树为空,所以直接输出。结果:[4, 8, 5, 2, 6],堆栈:[1, 3, (7)]
  • 弹出栈顶元素7,因为左右子树为空,所以直接输出。结果:[4, 8, 5, 2, 6, 7],堆栈:[1, 3]
  • 弹出栈顶元素3,因为左右子树为空,所以直接输出。结果:[4, 8, 5, 2, 6, 7, 3],堆栈:[1]
  • 弹出栈顶元素1,因为左右子树为空,所以直接输出。结果:[4, 8, 5, 2, 6, 7, 3, 1],堆栈:[]
  • 堆栈为空,结束。

总结一下,翻译成代码步骤如下:

  • 根节点入栈
  • 如果堆栈不为空
    • 弹出栈顶元素
    • 判断左右子树是否都为空
      • 是!如果右子树不为空,入栈;如果左子树不为空,入栈。
      • 否!直接输出当前值。

LintCode验证代码是否正确。

public List<Integer> postorderTraversal(TreeNode root) {
    List<Integer> ret = new ArrayList<>();
    if (root != null) {
        Stack<TreeNode> stack = new Stack<>();
        stack.push(root);
        while (!stack.isEmpty()) {
            TreeNode node = stack.pop();
            if ((node.left == null) && (node.right == null)) {
                ret.add(node.val);
            } else {
                TreeNode tmp = new TreeNode(node.val);
                stack.push(tmp);
                if (node.right != null) {
                    stack.push(node.right);
                }
                if (node.left != null) {
                    stack.push(node.left);
                }
            }
        }
    }
    return ret;
}

最后,推荐一个画二叉树的在线工具:http://mshang.ca/syntree/。自己输入表达式,[]代表一个空子树。最简单的一个结构表达如下:[1 [2] [3]]。文章里的例子可以用[1 [2 [4] [5 [] [8]]] [3 [6] [7]]]产生。

猜你喜欢

转载自blog.csdn.net/panda_lin/article/details/108230003