本节我们来简单了解一下二叉树基本的层序遍历与变换。
0. 最简单的遍历
我们先看最简单的情况,仅仅遍历并输出全部元素,如下:
3
/ \
9 20
/ \
15 7
上面的二叉树应输出结果 [3, 9, 20, 15, 7], 方法上面已经图示了,这里看一下怎么代码实现。先访问根节点,然后将其左右子孩子放到队列里,接着继续出队,出来的元素都将其左右各自孩子放到队列里,直到队列为空了就退出就行了:
List<Integer> simpleLevelOrder(TreeNode root) {
List<Integer> res = new ArrayList<Integer>();
if (root == null) {
return res;
}
LinkedList<TreeNode> queue = new LinkedList<TreeNode>();
//将根节点放入队列中,然后不断遍历队列
queue.add(root);
//有多少元素执行多少次
while (queue.size() > 0) {
//获取当前队列的长度,这个长度相当于 当前这一层的节点个数
TreeNode t = queue.remove();
res.add(t.val);
if (t.left != null) {
queue.add(t.left);
}
if (t.right != null) {
queue.add(t.right);
}
}
return res;
}
从树的结构我们不难发现,一个结点在 一层访问之后,其他孩子都是在下层按照FIFO的顺序处理的,因此队列就是一个缓存的作用。
如果要把每层的元素分开,要怎么做?看下一题:
1. 二叉树的层序遍历
LeetCode102 题目要求:给你一个二叉树,请你返回其按层序遍历得到的节点值。(即逐层地,从左到右访问所有节点)。
我们再观察执行过程图,我们先将根节点放到队列中,然后不断遍历队列。
那我们如何判断某一层的元素访问完了呢?我们可以使用一个size来表示每一层的元素个数,只要出队size就减1,减到0就说明该层元素访问完了。当size变成0之后,这时队列中剩余的元素就是下一层的元素个数,因此重新将size标记为下一层元素个数,继续下一行的数据处理。以上图为例:
首先拿到结点3,标记size为1,其左右结点都不为空,则将其自身出队,让其左右结点都入队。此时size--为0,进入下一层,9,20为第二层的所有元素,此时size = 2。
继续,将9从队列中拿走,size--变成1,并将其孩子8,13入列。之后再将20出队并将其孩子15,17入队,size--为0,此时的四个元素8,13,15,17就是第三层的元素。
代码如下:
public static List<List<Integer>> level102Order(TreeNode root) {
if (root == null) {
return new ArrayList<List<Integer>>();
}
List<List<Integer>> res = new ArrayList<List<Integer>>();
LinkedList<TreeNode> queue = new LinkedList<TreeNode>();
//将根节点放入队列中,然后不断遍历队列
queue.add(root);
while (queue.size() > 0) {
//获取当前队列的长度,这个长度相当于 当前这一层的节点个数
int size = queue.size();
ArrayList<Integer> tmp = new ArrayList<Integer>();
//将队列中的元素都拿出来(也就是获取这一层的节点),放到临时list中
//如果节点的左/右子树不为空,也放入队列中
for (int i = 0; i < size; ++i) {
TreeNode t = queue.remove();
tmp.add(t.val);
if (t.left != null) {
queue.add(t.left);
}
if (t.right != null) {
queue.add(t.right);
}
}
//将临时list加入最终返回结果中
res.add(tmp);
}
return res;
}
2. 层序遍历-自底向上
LeetCode 107.给定一个二叉树,返回其节点值自底向上的层序遍历。(即按从叶子节点所在层到根节点所在的层,逐层从左向右遍历)。例如给定的二叉树为:
返回结果为:
[ [15,7], [9,20], [3] ]
如果要求从上到下输出每一层的节点值,做法是很直观的,在遍历完一层节点之后,将存储该层节点值的列表添加到结果列表的尾部。这道题要求从下到上输出每一层的节点值,只要对上述操作稍作修改即可,在遍历完一层节点之后,将存储该层节点值的列表添加到结果列表的头部。
为了降低在结果列表的头部添加一层节点值的列表的时间复杂度,结果列表可以使用链表的结构,在链表头部添加一层节点值的列表的时间复杂度是 O(1)。在 Java 中,由于我们需要返回的 List 是一个接口,这里可以使用链表实现。
public static List<List<Integer>> levelOrderBottom(TreeNode root) {
List<List<Integer>> levelOrder = new LinkedList<>();
if (root == null){
return levelOrder;
}
Queue<TreeNode> queue = new LinkedList<>();
queue.offer(root);
while (!queue.isEmpty()){
int size = queue.size();
LinkedList<Integer> tmp = new LinkedList<>();
for (int i = 0; i < size; i++) {
TreeNode t = queue.poll();
if (t.left != null){
queue.offer(t.left);
}
if (t.right != null){
queue.offer(t.right);
}
tmp.add(t.val);
}
//前面部分与上一题相同,只在每次加入新链表集合时,从levelOrder前面插入
levelOrder.add(0,tmp);
}
return levelOrder;
}
3. 二叉树的锯齿形层序遍历
LeetCode103 题,要求是:给定一个二叉树,返回其节点值的锯齿形层序遍历。(即先从左往右,再从右往左进行下一层遍历,以此类推,层与层之间交替进行)。
例如:给定二叉树 [3,9,20,null,null,15,7]
返回结果:
[ [3], [20,9], [15,7] ]
改题目也是层序遍历的变种,只是最后输出的结果有所变化,要求我们按层数的奇偶来决定每一层的输出顺序。如果当前层数是偶数,从左至右输出当前层的节点值,否则,从右至左输出当前层的节点值。
我们依然可以沿用第 102 题的思想,为了满足题目要求的返回值为「先从左往右,再从右往左」交替输出的锯齿形,可以利用「双端队列」的数据结构来维护当前层节点值输出的顺序。双端队列是一个可以在队列任意一端插入元素的队列。在广度优先搜索遍历当前层节点拓展下一层节点的时候我们仍然从左往右按顺序拓展,但是对当前层节点的存储我们维护一个变量 isOrderLeft 记录是从左至右还是从右至左的:
如果从左至右,我们每次将被遍历到的元素插入至双端队列的末尾。
从右至左,我们每次将被遍历到的元素插入至双端队列的头部。
public static List<List<Integer>> zigzagLevelOrder(TreeNode root) {
List<List<Integer>> ans = new LinkedList<>();
if (root == null) {
return ans;
}
Queue<TreeNode> queue = new LinkedList<>();
//奇数行为false,偶数行为true
boolean isOrderLeft = false;
queue.offer(root);
while (queue.size() > 0) {
int size = queue.size();
Deque<Integer> tmp = new LinkedList<>();
for (int i = 0; i < size; i++) {
TreeNode t = queue.poll();
if (t.left != null) {
queue.add(t.left);
}
if (t.right != null) {
queue.add(t.right);
}
if (isOrderLeft){
tmp.offerFirst(t.val);//偶数行则元素添加到链表头
} else {
tmp.offer(t.val);//奇数行元素添加到链表尾
}
}
isOrderLeft = !isOrderLeft;
ans.add(new LinkedList<Integer>(tmp));
}
return ans;
}