数据结构与算法分析笔记与总结(java实现)--二叉树3:二叉树按层遍历打印练习

题目:有一棵二叉树,请设计一个算法,按照层次打印这棵二叉树。给定二叉树的根结点root,请返回打印结果,结果按照每一层一个数组进行储存,所有数组的顺序按照层数从上往下,且每一层的数组内元素按照从左往右排列。保证结点数小于等于500。

思路:对于二叉树,除了先序遍历、中序遍历、后序遍历之外,常用的遍历方式还有按层遍历,即一层一层地进行遍历,通常还要求在遍历时携带行的信息,例如一层遍历完后要求打印换行符或者每一层都打印行号。


对于按层遍历的理解:

1.按层遍历属于针对二叉树的宽度优先遍历问题(DFS)

2.对于图的宽度优先遍历,常常使用队列结构来解决问题

3.面试中,按层遍历常常对换行有所要求

其实按层遍历很简单,使用队列就可以很容易的解决问题,队列具有先进先出的特性,先放入的结点会先弹出,后放入的结点后弹出。如果不考虑遍历时换行的问题,那么按层遍历很简单,类似于先序遍历,只是此时使用的额外数据结构是队列而不是栈而已。

①先创建一个队列queue,可以直接使用Queue类库,也可以使用LinkedList链表来实现队列(LinkedList的poll方法用于从头部删除一个对象并返回这个对象,相当于出队;LinkedList的push方法用于在链表的结尾加入一个对象,相当于入队,因此只要使用push和poll方法就可以实现对队列的操作)

②创建一个临时变量cur表示当前正在访问的结点,初始值cur=root

③首先将root入队,然后开始循环过程,先从队列中弹出一个结点poll记作cur,然后进行2个动作:将cur.left放入队列(如果不为空的话),将cur.right放入队列(如果不为空的话);然后开始下一次循环:弹出一个结点poll记作cur……依次进行直到queue.size()==0;


原理:从根结点开始,将根结点放入队列①,之后弹出根结点①,每弹出一个结点就先后将其左右结点②③放入队列中,由于队列先进先出,因此先取出的是②,然后将④⑤放入到队列中,然后从队列中取出一个结点,由于队列先进新出,此时取出的是之前放入的③,再将⑥⑦放入队列……由于从第二层开始总是从左到有放入队列,下一层的结点一定是在当前层的结点之后才放入并且也是从左到右放入的,因此总体顺序必然是从上到下,从左到右的。

即只要记住,只要使用队列来进行弹出和压入操作就可以保证遍历的顺序是按层遍历的。

While(queue.size()!=0){

         Cur=Queue.poll();

         List.add(cur);

         If(cur.left!=null)queue.push(cur.left);

         If(cur.right!=null)queue.push(cur.right); 

}

 

关键是当要求在按层遍历时要输出行的信息,即要求将每一层结点放入到一个单独的集合中,或者要求每遍历一层时就输出一个换行符,或者按照金字塔格式打印。此时在遍历时需要知道当遍历到某个结点时表示这一层结束,即要知道何时到达每一层的最后一个结点。

解决方法是使用2个指针last和nlast,用nlast表示当前正在访问的结点,用last记录下一行的最后一个结点。

①从根结点开始,初始时nlast=null,即可以想象成为在根结点的上面,还没有访问任何一个结点,认为此时的所在行是0行(root的上面一行),令last=root,即表示下一行(第1行)的最后一个结点是root,即nlast=null,last=root(这是规定的,只要初始值规定好,之后就会出现需要的规律);

②将root放入队列后,循环开始,先从队列中取出一个结点cur,并先后将cur.left和cur.right放入队列中;每访问一个结点,就在将该结点放入队列之后将nlast指针移动到当前结点,表示正在访问的结点;

③每次放完之后要进行一个判断,看当前弹出的结点是否等于last。(弹出的结点就是当前正在遍历的结点,千万注意,我们要判断的是从队列中弹出的结点是否到达每层的最后一个结点,即判断正在遍历的结点是否是每一层的最后一个结点,而不是放入队列中的结点是否是是每一层的最后一个结点---因为按层遍历时结点是按照层的顺序从左到右从队列中弹出来的,因此判断的是弹出的结点是否是每一层的最后一个结点)一开始弹出的结点是root,显然成立,当将root.left和root.right放入到队列中之后,nlast指向了结点③,于是令last=nlast,即以结点③作为下一层(第2层)的最后一个结点……之后弹出cur=②,压入④⑤,判断cur!=last,继续弹出cur=③,压入⑥⑦,nlast=⑦,判断cur==last,于是换行,令last=nlast=⑦,……

即总是先弹出一个结点curà压入2个子节点left和right同时更新nlastà判断弹出的结点cur是否等于lastà如果不是,不作处理,继续循环弹出结点;如果是,进行换行操作,同时更新last为nlast。

操作就是这样,需要记住,它的原理是:对于初始值nlast=null,last=root是规定的,必须如此;之后必须是先弹出结点,再压入子节点,再判断cur==last,顺序不能出错。原理:如果弹出的结点是所在层的最后一个结点,那么它的子节点一定是下一层的最后一个结点。这给出了一个递推关系式,如果遍历弹出的结点cur达到最后所在层的最后一个结点last,那么此时nlast必然恰好到达下一层的最后一个结点,于是将last更新为nlast即可。注意,对于弹出结点后,要先放入它的2个子节点再更新last=nlast,否则nlast还没有到达层的最后一个位置。这是一种规律,是基于初始情况:last=null;nlast=root并且先弹出再放入再更新这种固定操作条件下才能保证正确的。

换行操作有多种类型:可以是仅仅输出一个换行符,也可以是将每一层的结点单独存放在一个集合中,或者将每一层的结点单独存放在一个数组中。由于每一层的结点数目不知道,因此最好还是将每一层的结点都单独存放在一个集合中,即构建一个ArrayList<ArrayList<TreeNode>> list,list的元素又是一个ArrayList集合,集合中存放的元素是树的结点。最后返回这个list,再将list里面的list逐个取出表示每一层元素的集合,再将每一个集合中的结点逐一取出代表每一个结点,显然这里需要通过双重遍历来对二维数组进行赋值,之后将其放入数组int[][]返回即可。

常识:Java中队列Queue<>只是一个接口,并不是一个类(Stack<>是一个类可以直接使用方法),因此不能直接使用Queue来作为队列使用,而必须使用Queue接口的实现类,LinkedList<>就实现了Queue<>接口,可以充当队列。此外实现Queue接口的类还有AbstractQueue、ArrayBlockingQueue、ArrayDeque等,因此当我们需要使用队列时,就是使用LinkedList来当做队列,只需要使用里面的poll()方法和add()方法即可。Poll()方法用于从队列的头部弹出一个结点,add()用于在队列的尾部加入一个结点,注意不能使用push(),因为push()表示的是推入堆栈的栈顶,不是加入在队尾。

 

importjava.util.*;

 

/*

publicclass TreeNode {

    int val = 0;

    TreeNode left = null;

    TreeNode right = null;

    public TreeNode(int val) {

        this.val = val;

    }

}*/

//按层遍历:属于图的宽度优先遍历,需要使用队列来辅助完成

publicclass TreePrinter {

    public int[][] printTree(TreeNode root) {

       //①创建一个队列:使用链表LinkedList来实现队列

        LinkedList<TreeNode> queue=newLinkedList<>();

       //创建集合results用来存放遍历过的结点,每一层对应一个集合,整体结果也是一个集合,于是集合里面放集合

       ArrayList<ArrayList<TreeNode>> results=newArrayList<>();

       //②创建临时变量temp表示当前表示当前正在访问的结点

        TreeNode temp=root;

       //③创建遍历指针last表示下一层的最后结点,nlast表示当前now正在访问的结点(不是遍历是访问),注意初始值

        TreeNode last=root;

        TreeNode nlast=null;

       //创建一个集合levelList表示每一层对应的集合,用来存放一层的结点

        ArrayList<TreeNode> levelList=newArrayList<>();

       //③先将根结点放入到队列中

        queue.add(root);

       //④开始循环:弹出结点--记录--放入左右结点--判断是否到层最后

        while(queue.size()!=0){

           //弹出队列,这是正在遍历的结点,放入每一层的集合

            temp=queue.poll();

            levelList.add(temp);

            //压入左右2个子节点,注意要同时移动更新nlast

            if(temp.left!=null){

                queue.add(temp.left);

                nlast=temp.left;

            }

            if(temp.right!=null){

                queue.add(temp.right);

                nlast=temp.right;

            }

            //判断是否到边

            //如果到边则换行,更新

            if(temp==last){

                //表示到边,进行换行操作,同时更新last

                results.add(levelList);

                levelList=newArrayList<>();

                //常识:levelList只是一个变量名称,只是中间桥梁,results通过levelList已经创建了指向集合真正位置的地址

                last=nlast;

            }

        }

       //双重遍历将结果取出放入到二维数组中

        int[][] results2=new int[results.size()][];

        for(int i=0;i<results.size();i++){

                 results2[i]=newint[results.get(i).size()];

            for(intj=0;j<results2[i].length;j++){

               results2[i][j]=results.get(i).get(j).val;

            }

        }

       //返回结果

        return results2;

    }

}

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值