在上一篇文章中,我们讲述了二叉树深度优先遍历的三种方式:前序、中序、后序遍历。今天我们来讲解二叉树的广度优先遍历(层序遍历)以及其变式:自下而上遍历、锯齿形遍历......
所谓层次遍历,就是从根节点开始,先访问根节点下面一层的全部元素之后,再访问之后层次的元素,类似于金字塔一样一层层访问。
如图,上面这张图的层序遍历序列就是5,4,6,1,2,7,8
一.如何实现最基本的层序遍历?(从上往下,从左往右)
最基本的层序遍历其实很简单,使用队列来存储即可实现。
如图,初始化条件:先将根节点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]】。
二.拓展:那么从下往上遍历,比如输出【[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; }