同学们,我把《剑指Offer》中所有“树”标签的习题及思路整理出来了,对照学习效果更佳:
建议参考以下两道题,循序渐进:
剑指Offer32.1——从上到下打印二叉树
剑指Offer32.2——从上到下打印二叉树(二)
这道题我自己写了好久,每次都是卡在某个测试用例,非常崩溃,最后看了大神的解法,觉得豁然开朗,自己的问题在于:没有控制好奇偶层和出队列顺序的关系,导致十分混乱
这么说大家可能难以理解,回到这道题,我们先想一下逆序打印,共有几种方法?
前提:返回值要求是List<List<Integer>>
,因此,每一层都在一个小链表中。
- 第一种:每一层获取到的数字,加入小链表的顺序不同:奇数层
addLast
,偶数层addFirst
(加入头部即实现逆序)。说白了,就是正常加入,出来的时候逆序 - 第二种:不用变量去控制奇偶,因为第一次进入循环一定是奇数层,所以直接逻辑区分奇偶。正常出,加入的时候逆序
- 第三种,每一层获取的数字,正常加入
List
,在加入大List
之前,判断奇偶,看是否反转
上面三种想法,也就是这道题的三种解法,总的来说有以下关键点:
- 首先,层序遍历一定要使用队列
- 第二,要想逆序打印,就需要双端队列
- 第三,要想到集合的
reverse
方法
方法一:加入小集合的时候逆序
跟正常的层序遍历一样,同样需要借助队列的帮助,去循环遍历队列。
这种做法需要注意的是,每一层的遍历结果,同样需要加入到一个双端队列,在加入的时候实现逆序的效果
这样做的好处:相当于解耦操作,不需要双端队列来控制顺序了
代码:
class Solution {
public List<List<Integer>> levelOrder(TreeNode root) {
if(root == null) return new ArrayList<>();
Queue<TreeNode> queue=new LinkedList<>();
queue.add(root);
List<List<Integer>> res=new ArrayList<>();
LinkedList<Integer> list=null;
while(!queue.isEmpty()) {
list=new LinkedList<>();
for(int i=queue.size();i>0;i--) {
TreeNode node=queue.poll();
if(res.size()%2==0) {
list.addLast(node.val);
} else {
list.addFirst(node.val);
}
if(node.left!= null) queue.add(node.left);
if(node.right!= null) queue.add(node.right);
}
res.add(list);
}
return res;
}
}
方法二:无需判断奇偶层,因为刚进去就是奇数层
简单来讲,这种方法主要包含两步!
- 把当前行的节点打印出来!(出队列)
- 把子节点加入队列
难点在于:
需要找到加入和打印的关系,因为上一行加入的下一行就要打印出来,而且是逆序
而诀窍在于:
奇数层从左到右出队列,子节点(先左后右)从左到右加入(addLast)
偶数层从右到左出队列,子节点(先右后左)从右到左加入(addFirst)
这种方法不需要控制变量来控制奇偶,第一次进入循环,就是奇数层,此后通过不断的更新队列中的节点,来继续循环
而每次循环就只包含奇偶两次操作。
注意:这种方法需要双端队列
之前我就错在没有把下层节点加入到队尾(或队首),导致下层节点的值提前输出
代码:
class Solution {
public List<List<Integer>> levelOrder(TreeNode root) {
if(root == null) return new ArrayList<>();
Deque<TreeNode> deque=new LinkedList<>();
deque.addLast(root);
List<List<Integer>> res=new ArrayList<>();
List<Integer> list=null;
while(!deque.isEmpty()) {
list=new ArrayList<>();
for(int i=deque.size();i>0;i--) {
TreeNode node=deque.removeFirst();
list.add(node.val);
if(node.left != null) deque.addLast(node.left);
if(node.right != null) deque.addLast(node.right);
}
res.add(list);
if(deque.isEmpty()) break;
list=new ArrayList<>();
for(int i=deque.size();i>0;i--) {
TreeNode node=deque.removeLast();
list.add(node.val);
if(node.right != null) deque.addFirst(node.right);
if(node.left != null) deque.addFirst(node.left);
}
res.add(list);
}
return res;
}
}
方法三:正常运行,小List加入前判断正反
第三种方法前面只需要正常运行即可,正常出队列,加入队列,只需要在每一层结束前判断,是否需要将List转过来即可
代码:
class Solution {
public List<List<Integer>> levelOrder(TreeNode root) {
if(root == null) return new ArrayList<>();
Queue<TreeNode> queue=new LinkedList<>();
queue.add(root);
List<List<Integer>> res=new ArrayList<>();
List<Integer> list=null;
while(!queue.isEmpty()) {
list=new ArrayList<>();
for(int i=queue.size();i>0;i--) {
TreeNode node=queue.poll();
list.add(node.val);
if(node.left != null) queue.add(node.left);
if(node.right != null) queue.add(node.right);
}
if(res.size()%2!=0) Collections.reverse(list);
res.add(list);
}
return res;
}
}
复杂度分析:
- 时间:O(N),因为都需要遍历每一个节点
- 空间:O(N),因为需要额外队列空间