二叉树层序遍历,万变不离其宗

在上一篇文章中,我们讲述了二叉树深度优先遍历的三种方式:前序、中序、后序遍历。今天我们来讲解二叉树的广度优先遍历(层序遍历)以及其变式:自下而上遍历、锯齿形遍历......

所谓层次遍历,就是从根节点开始,先访问根节点下面一层的全部元素之后,再访问之后层次的元素,类似于金字塔一样一层层访问。

image.png

 

如图,上面这张图的层序遍历序列就是5,4,6,1,2,7,8

一.如何实现最基本的层序遍历?(从上往下,从左往右)

最基本的层序遍历其实很简单,使用队列来存储即可实现。

image.png

 

如图,初始化条件:先将根节点3入队。然后开始循环地将队列中元素出队。每出队一个元素,都将这个元素的子节点入队。然后你就可以发现:这样做的话,出队元素序列就是层序遍历序列,代码如下:

public static List<Integer> simpleLevelOrder(TreeNode root){
    if (root == null){
        return null;
    }
    //1.建一个队列queue(LinkedList实现了Queue接口),建一个列表(ArrayList)存放层序遍历序列
    ArrayList<Integer> res = new ArrayList<>();
    LinkedList<TreeNode> queue = new LinkedList<>();
    queue.add(root);
    while (!queue.isEmpty()){
        //出队队列元素,同时将其值放入ArrayList中;
        //同时,每出队一个元素,就将它的子节点入队,然后继续出队......
        //这样最后ArrayList中存放的就是层序遍历序列
        TreeNode node = queue.remove();
        res.add(node.val);
        if (node.left != null){
            queue.add(node.left);
        }
        if (node.right != null){
            queue.add(node.right);
        }
    }
    return res;

基本的层序遍历不难,但如果要求你将每一层的元素用括号隔开分别表示,又该如何应对呢?

在上面的问题中,若是让你按层分开输出,输出【[3] , [9,20] , [8,13,15,17]】,这时该怎么办呢?这就是经典的按层打印问题,我们一起来看。

 所谓按层打印,其实和上面的层序遍历并没有什么区别,只是需要判断什么时候一层遍历完了。

 如果不提前看过代码的话,是很难想出来的,这里直接奉上代码:

public static List<List<Integer>> levelOrder(TreeNode root){
    if (root == null){
        return null;
    }
    //创建结果集和队列
    ArrayList<List<Integer>> res = new ArrayList<>();
    LinkedList<TreeNode> queue = new LinkedList<>();
    queue.add(root);
    while (!queue.isEmpty()){
        int size = queue.size();//每次循环开始,size都能获取到这层元素的个数 --> 遍历次数
        ArrayList<Integer> list = new ArrayList<>();//list用于存放某一层元素的值
        //记录当前层的元素个数size,然后遍历size次,每遍历到一个元素就出队;
        //遍历size次后这一层也就遍历完成了,队列中剩下的均为下一层的元素。
        for (int i = 0; i < size; i++) {
            //每出队一个元素,都要把它的左右孩子放入队列,和上面一样。
            TreeNode treeNode = queue.remove();
            list.add(treeNode.val);
            if (treeNode.left != null){
                queue.add(treeNode.left);
            }
            if (treeNode.right != null){
                queue.add(treeNode.right);
            }
        }
        res.add(list);
    }
    return res;
}

分析:1.首先可以看到,与前面代码一个很大的不同点就是我设置了变量size。size变量是什么意思呢?最开始队列里只有root根节点一个元素,所以第一轮循环中size == 1,刚好是第一层元素的个数。然后for循环中遍历并出队了size==1次,但因为出队时加入了节点3的左右孩子,所以队列中还剩9和20.所以下一轮循环size赋值为2,刚好是第二层元素的个数。for循环中再出队size次,因为第二层元素等于size个,所以就相当于第二层的元素全部遍历且出队。因为加入了左右孩子,这样队列中剩下的就是第三层的全部元素。size再次赋值为queue.size(),再次出队size次。。。你发现了,size就代表了当前一层的元素个数。

2.所以我们每轮循环通过queue.size()拿到这一层的元素个数,然后根据元素个数就能判断遍历这一层需要出队几次;然后,遍历这一层的元素,将其出队并放在一个List中,每一层都对应一个List。如第二层元素都放在一个List中,为[9,20]。

3.然后每当一层遍历完成时,都把这个保存了该层元素的List再放到一个List中。如第一层是[3],第二层是[9,20],第三层是[8,13,15,17],把这三个List,也就是小结果集都整合放入到一个大List中,

就变为了【[3] , [9,20] , [8,13,15,17]】。

image.png

 二.拓展:那么从下往上遍历,比如输出【[8,13,15,17] , [9,20] , [3]】,要怎么办呢?

你可能发现了,其实每一层遍历的结果并没有改变。 如第一层是[3],第二层是[9,20],第三层是[8,13,15,17],不妨将存放了一层的元素的List称为“小结果列表”,区分于最终返回的List<List>.

那么其实这些题目,都是怎样组织小结果列表和怎样排列小结果列表的变形。

比如上面这个题,小结果列表还是那三个不变,但是在输出时倒序了。所以在加入小结果列表到List<List>时,可以采用栈的形式。可以用Stack,这里选择的是LinkedList<List<Integer>>。每次都将小结果列表放入LinkedList头部,这样最后从头部开始输出时就是倒序。代码如下:

public static List<List<Integer>> levelOrderFromBottom(TreeNode root){
    LinkedList<List<Integer>> res = new LinkedList<>();//最终返回的结果列表
    if (root == null){
        return null;
    }
    LinkedList<TreeNode> queue = new LinkedList<>();
    queue.add(root);
    while (!queue.isEmpty()){
        int size = queue.size();
        ArrayList<Integer> temp = new ArrayList<>();
        for (int i = 0; i < size; i++) {
            TreeNode node = queue.remove();
            temp.add(node.val);
            if (node.left != null){
                queue.add(node.left);
            }
            if (node.right != null){
                queue.add(node.right);
            }
        }//小列表填值完毕
        //把小列表按栈的形式放到大列表LinkedList里面
        res.addFirst(temp);
    }
    return res;
}
/**
 * 注:为什么选择用LinkedList的addFirst方法而不选择ArrayList的add(int index,E element)方法呢?明明后者也可以通过设置参数index为0而将小列表插入到大列表头部呀?
 * --> 这是因为对ArrayList这种数组形式存储,在头部插入元素需要挪动后面所有元素,时间复杂度为On;
 * 而在链表头部插入元素的时间复杂度仅为O1。所以这里选择了链表
 */

三.那么再让你看锯齿形遍历呢?即,先从左往右,下一层从右往左,下一层再从左往右,循环......

如上的树结构图,锯齿遍历就应该是【[3] , [20,9] , [8,13,15,17]】

我们使用一个变量isOrderLeft来存放每一层是从左往右遍历还是从右往左遍历。

看代码:

/**
 *二叉树的锯齿形遍历,即:从上往下遍历,但是奇数层从左往右遍历,偶数层从右往左遍历
 * 思路:每层只有两种遍历方式,因此我们可以选择用一个boolean变量isOrderLeft来记录是否是从左往右遍历
 * 需要注意的是,在遍历过程中,我们将节点放入队列的顺序永远是先放左孩子后放右孩子,
 * 也即是说,队列queue中元素的先后顺序永远是原树从上往下,从左往右的顺序。
 * 所谓的左->右,右->左的遍历方式改变,只是我们存储出队元素的值时对其排列方式进行了调整。
 * 这里也不例外。我们在队列中仍然是从上往下从左往右的原始顺序,只是在将出队元素存储到列表的过程中,如果该层是从左往右遍历就把每次的出队元素加入到小列表尾部;如果该层是从右往左(即为逆序)就把每次的出队元素加入到小列表头部,则一为顺序,一为逆序。
 * 而因为列表是从上往下遍历,所以我们仍然选择使用ArrayList类型的大列表
 * 每一层遍历结束后将isOrderLeft变量赋值为 !isOrderLeft,改变一层中的遍历顺序
 */
public static List<List<Integer>> levelOrderByTeeth(TreeNode root){
    ArrayList<List<Integer>> res = new ArrayList<>();//大结果列表
    if (root == null){
        return null;
    }
    LinkedList<TreeNode> queue = new LinkedList<>();
    queue.add(root);
    boolean isOrderLeft = true;//定义第一层是从左往右遍历
    while (!queue.isEmpty()){
        int size = queue.size();
        LinkedList<Integer> temp = new LinkedList<>();
        for (int i = 0; i < size; i++) {
            TreeNode node = queue.remove();
            if (node.left != null){
                queue.add(node.left);
            }
            if (node.right != null){
                queue.add(node.right);
            }
            //对于出队元素放入小列表中位置的处理,如果isOrderLeft为true则每个元素放入尾部;否则每个元素放入头部
            if (isOrderLeft){
                temp.add(node.val);
            }else {
                temp.addFirst(node.val);
            }
        }
        //每一层的元素遍历完之后,在进入下一层之前,将保存该层元素结果的小列表放入大列表尾部,同时将isOrderLeft改变
        res.add(temp);
        isOrderLeft = !isOrderLeft;
    }
    return res;
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值