由二叉树的遍历引出的一系列思考

281 阅读6分钟

受朋友之托,解决一个leetcode算法题。我猜测朋友看我不学无术,也想让我输出一点,故用此“雕虫小技”激励我。在此对@欧浪浪深表感谢。

话不多说,我们先看下题目:

给定一个二叉树,编写一个函数来获取这个树的最大宽度。树的宽度是所有层中的最大宽度。这个二叉树与满二叉树(full binary tree)结构相同,但一些节点为空。

每一层的宽度被定义为两个端点(该层最左和最右的非空节点,两端点间的null节点也计入长度)之间的长度。

示例 1:

输入:

       1
     /   \
    3     2
   / \     \  
  5   3     9 

输出: 4 解释: 最大值出现在树的第 3 层,宽度为 4 (5,3,null,9)。 示例 2:

输入:

      1
     /  
    3    
   / \       
  5   3     

输出: 2 解释: 最大值出现在树的第 3 层,宽度为 2 (5,3)。 示例 3:

输入:

      1
     / \
    3   2 
   /        
  5      

输出: 2 解释: 最大值出现在树的第 2 层,宽度为 2 (3,2)。 示例 4:

输入:

      1
     / \
    3   2
   /     \  
  5       9 
 /         \
6           7

输出: 8 解释: 最大值出现在树的第 4 层,宽度为 8 (6,null,null,null,null,null,null,7)。

耐心看完题目, 很自然的想到,如果我们能将二叉树的每一层的数据表示出来,那么问题就会迎刃而解。那么我们着重的思考“将二叉树的每一层的数据表示出来”这个问题。

首先,将二叉树的数据表示出来这个问题大家应该都有所了解,就是二叉树的遍历问题,这里不做过多赘述,掘金平台上由很多类似的描述。

根据节点之间的关系,我们将二叉树的遍历分为四种:

  1. 前序遍历
  2. 后序遍历
  3. 中序遍历
  4. 层序遍历

很明显,层序遍历从子面意思上就和我们所要探讨的问题:将二叉树的数据表示出来 很类似。那我们就先来尝试着解决二叉树的层序遍历。解决前三种遍历问题,我们很习惯的想到递归的方法,那我们是否能试着用递归方法解决二叉树的层序遍呢?

答案是当然可以,不过请先认真思考两分钟,在你的大脑中简单模拟一下递归过程。

。。。。。。。。。。。。。。。。

好的,时间到了,是不是觉的有点变扭但是又说不出来哪里不对? 好,我来告诉你为什么。

我们仔细想一下,前三种遍历,我们拿前序遍历举例,我们的思想是,先看根节点,输出根节点,如果根节点的左子树存在那么我们将左子树的节点当作根节点作前序遍历,然后前序遍历右子树。

那么我们举个极端的例子,这棵二叉树严重发育不良,根节点的左子树非常长,右子树只有一个节点,

                    1
                   / \
                  2   9
                 / \
                3   8
               /
              4
             /
            5
           /
          6
         /
        7

那么我们通过递归要一直找到节点“7”,在整个左子树没有遍历完成之前,我们是不会管节点“9”的,也就是说我们前序遍历的递归方法是一头“猛子”直接扎到底,(记住这个扎到底,要考)暂时不会管右边的节点是什么。所以我们递归就会用的顺手,也就是说我想前序遍历整棵树,我们先记录根节点,然后统一搞定左子树,然后统一搞定右子树。

那么我们再来思考层序遍历,我们会一头“猛子”直接扎到底,当然不会,我们要保证我们每层都记录好,才能开始记录下一层, 所以说,层序遍历首先要记录根节点,然后要搞定左节点,然后搞定右节点,然后要搞定左节点的下一层,搞定右节点的下一层。所以说用递归是不是很别扭?

此时,我们从这个角度,将二叉树的遍历分为两种,即:

  1. 深度优先算法(前序、中序、后序) 2.  广度优先算法(层序)

我们以题目为例:

       1
     /   \
    3     2
   / \     \  
  5   3     9 

首先这里帮大家总结一个小经验:能用递归的完成的算法,大部分都能用栈解决,因为,递归和栈能对以前的操作进行回溯。

这里我就不详细讲解,如何用非递归(栈)来解决深度优先遍历了,不过我们需要明白的是,深度优先,需要记录根节点,然后一直扎下去,把最先面的数据输出,所以我们用栈(先进后出)来处理深度优先遍历。

接下来我们重点讲解一下广度优先遍历,我们所做的就是想输出第一层的数据,然后输出第二层的数据,按照层次的顺序,所以我们用对列(先进先出)来解决层序遍历的问题。

首先,我们将根节点“1”放入队列,然后每取出一个数,都需要将这个数的左右节点放入对列中

结果:

队列:

1

第二步:取出1:放入1的左右节点

结果:1

对列:

32

第三步:取出3:放入3的左右节点

结果:1,3

对列:

253

第四步:取出2:放入2的左右节点

结果:1,3,2

对列:

539

第五步:取出5:放入5的左右节点

结果:1,3,2,5

对列:

39

第六步:取出3:放入3的左右节点

结果:1,3,2,5,3

对列:

9

第七步:取出9:放入9的左右节点

结果:1,3,2,5,3,9

对列:

对应代码如下:

public List<Integer> levelOrder(TreeNode root) {
        List<Integer> result = new ArrayList<>();
        Queue<TreeNode> queue = new LinkedList<TreeNode>();
        queue.offer(root);
        while(!queue.isEmpty()){
            TreeNode  node = queue.poll();
            result.add(node.val);
            if(node.left != null){
                queue.offer(node.left);
            }
            if(node.right != null){
                queue.offer(node.right);
            }
        }
        return result;
}

输出结果:[1,3,2,5,3,9]

那么我们在此上面增加一点难度,要求按层输出,也就是期望结果是[[1],[3,2],[5,3,9]]该如何实现呢,也很简单,也就是在每层的数据取完之后记录下下一层有多少个数据,取完下一层数据之后再记录下更下一层数据的个数。

对应代码如下:

public List<List<Integer>> levelOrder(TreeNode root) {
        List<List<Integer>> result = new ArrayList<>();
        Queue<TreeNode> queue = new LinkedList<TreeNode>();
        queue.offer(root);
        while(!queue.isEmpty()){
            List<Integer> level = new ArrayList<>();
            int currnetLevelSize = queue.size();
            for(int i = 1;i<=currentLevelSize;i++){
                TreeNode  node = queue.poll();
                level.add(node.val);
                if(node.left != null){
                    queue.offer(node.left);
                }
                if(node.right != null){
                    queue.offer(node.right);
                }
            }
            result.add(level);
        }
        return result;
}

输出结果:[[1],[3,2],[5,3,9]]

那么接下来就是解答我们最初的问题的时候了,如何求二叉树的最大宽度?也就是,我们能有办法记录每一层没个数据的位置,问题就迎刃而解了。 所以我们大可将原树中每个节点的值修改为当前的位置:

也就是将原树修改为如下树:

       1                        0
     /   \                    /   \
    3     2     -->          1     2
   / \     \               /  \     \
  5   3     9             3    4     6

第一步:将根节点变成0,放入对列

结果:最大宽度为1

对列:

0

树修改为:

       0
     /   \
    3     2
   / \     \  
  5   3     9 

第2步:取出对列的头尾,计算出上一层的宽度,(队列最后一个节点的值-队列最前面一个节点的值+1)并将推出队列的左节点修改为根节点值2+1,并放入对列,将根节点的右节点修改为根节点值2+2,并放入对列

结果:最大宽度为2

对列:

12

树修改为:

       0
     /   \
    1     2
   / \     \  
  5   3     9 

第3步:以此类推

结果:最大宽度为4

对列:

346

树修改为:

       0
     /   \
    1     2
   / \     \  
  3   4     6 

对应代码如下:

public int widthOfBinaryTree(TreeNode root) {
        LinkedList<TreeNode> queue = new LinkedList<TreeNode>();
        if(root == null){
            return 0;
        }
        root.val = 0;
        queue.offer(root);
        int result = 0;
        while(!queue.isEmpty()){
            int currnetLevelSize = queue.size();
            int width = queue.getLast().val-queue.getFirst().val+1;
            for(int i = 1;i <= currnetLevelSize;i++){
                TreeNode  node = queue.poll();
                if(node.left != null){
                    queue.offer(node.left);
                    node.left.val = node.val*2+1;
                }
                if(node.right != null){
                    queue.offer(node.right);
                    node.right.val = node.val*2+2;
                }
            }
            result = Math.max(width,result);
            
        }
        return result;
}

输出结果:4

到此,求二叉树的最大宽度问题也就解决了,内容篇幅有点长,主要照顾到许多朋友可能对二叉树的遍历的各种方法了解的不是很透彻,所以花了很长时间介绍。不喜勿喷。最后分享一句话给大家,也是我今天写这篇文章的初衷。

虽有嘉肴,弗食,不知其旨也;虽有至道,弗学,不知其善也。是故学然后知不足,教然后知困。知不足,然后能自反也;知困,然后能自强也。故曰:教学相长也。

                                                                    --《礼记·学记》