二叉树层序遍历 II 102
给你一个二叉树,请你返回其按 层序遍历 得到的节点值。 (即逐层地,从左到右访问所有节点)。
// Definition for a binary tree node.
public class TreeNode {
int val;
TreeNode left;
TreeNode right;
TreeNode() {}
TreeNode(int val) { this.val = val; }
TreeNode(int val, TreeNode left, TreeNode right) {
this.val = val;
this.left = left;
this.right = right;
}
}
示例:
二叉树:[3,9,20,null,null,15,7]
,
3 / \ 9 20 / \ 15 7
返回其层序遍历结果:
[ [3], [9,20], [15,7] ]
方法:广度优先搜索
最朴素的方法是用一个二元组 (node, level) 来表示状态,它表示某个节点和它所在的层数,每个新进队列的节点的 level 值都是父亲节点的 level 值加一。最后根据每个点的 level 对点进行分类,分类的时候我们可以利用哈希表,维护一个以 level 为键,对应节点值组成的数组为值,广度优先搜索结束以后按键 level 从小到大取出所有值,组成答案返回即可。
如何优化空间开销:如何不用哈希映射,并且只用一个变量 node 表示状态,实现这个功能呢?
- 首先根元素入队
- 当队列不为空的时候
- 求当前队列的长度 size
- 依次从队列中取 size 个元素进行拓展,然后进入下一次迭代
它和普通广度优先搜索的区别在于,普通广度优先搜索每次只取一个元素拓展,而这里每次取 size 个元素。在上述过程中的第 i 次迭代就得到了二叉树的第 i 层的 size 个元素。
证明该方式的合理性:观察这个算法,可以归纳出这样的循环不变式:第 i 次迭代前,队列中的所有元素就是第 i 层的所有元素,并且按照从左向右的顺序排列。证明它的三条性质(可以理解成数学归纳法):
- 初始化:i = 1 的时候,队列里面只有 root,是唯一的层数为 1 的元素,因为只有一个元素,所以也显然满足「从左向右排列」;
- 保持:如果 i = k 时性质成立,即第 k 轮中出队 sizeK 的元素是第 k 层的所有元素,并且顺序从左到右。因为对树进行广度优先搜索的时候由低 k 层的点拓展出的点一定也只能是 k + 1 层的点,并且 k + 1 层的点只能由第 k 层的点拓展到,所以由这sizeK个点能拓展到下一层所有的sizeK+1个点。又因为队列的先进先出(FIFO)特性,既然第 k 层的点的出队顺序是从左向右,那么第 k + 1 层也一定是从左向右。至此,已经可以通过数学归纳法证明循环不变式的正确性。
- 终止:因为该循环不变式是正确的,所以按照这个方法迭代之后每次迭代得到的也就是当前层的层次遍历结果。证毕。
复杂度分析
记树上所有节点的个数为 n。
- 时间复杂度:每个点进队出队各一次,故渐进时间复杂度为 O(n)。
- 空间复杂度:队列中元素的个数不超过 n 个,故渐进空间复杂度为 O(n)。
class Solution {
public List<List<Integer>> levelOrder(TreeNode root) {
// 声明返回值的存储空间
List<List<Integer>> leverOrder = new LinkedList<List<Integer>>();
// 如果节点为空,则直接返回
if(root == null){
return leverOrder;
}
// 层序遍历的节点存储空间 BFS
Queue<TreeNode> queue = new LinkedList<TreeNode>();
queue.offer(root);
// 节点还未访问完全访问
while(!queue.isEmpty()){
// 存储每层节点的 val
List<Integer> curLevel = new ArrayList<Integer>();
int size = queue.size();
// 对当前层的元素进行访问
for(int i = 0; i < size; i ++){
TreeNode node = queue.poll();
curLevel.add(node.val);
// 查看当前元素的左右是否为空,否则加入队列以进行下一层的遍历
TreeNode left = node.left;
TreeNode right = node.right;
if(left != null){
queue.offer(left);
}
if(right != null){
queue.offer(right);
}
}
// 在遍历完一层节点之后,将存储该层节点值的列表添加到结果列表的头部
leverOrder.add(curLevel);
}
return leverOrder;
}
}
二叉树层序遍历 II 107
给定一个二叉树,返回其节点值自底向上的层序遍历。 (即按从叶子节点所在层到根节点所在的层,逐层从左向右遍历)
给定二叉树 [3,9,20,null,null,15,7]
,
3 / \ 9 20 / \ 15 7
返回其自底向上的层序遍历为:
[ [15,7], [9,20], [3] ]
方法:广度优先搜索
树的层次遍历可以使用广度优先搜索实现。从根节点开始搜索,每次遍历同一层的全部节点,使用一个列表存储该层的节点值。
如果要求从上到下输出每一层的节点值,做法是很直观的,在遍历完一层节点之后,将存储该层节点值的列表添加到结果列表的尾部。—— 最原始的层序遍历
这道题要求从下到上输出每一层的节点值,只要对上述操作稍作修改即可:在遍历完一层节点之后,将存储该层节点值的列表添加到结果列表的头部。
为了降低在结果列表的头部添加一层节点值的列表的时间复杂度,结果列表可以使用链表的结构,在链表头部添加一层节点值的列表的时间复杂度是 O(1)。
在 Java 中,由于需要返回的 List 是一个接口,这里可以使用链表实现;而 C++ 或 Python 中,需要返回一个 vector 或 list,它不方便在头部插入元素(会增加时间开销),所以可以先用尾部插入的方法得到从上到下的层次遍历列表,然后再进行反转。
复杂度分析
- 时间复杂度:O(n),其中 n 是二叉树中的节点个数。每个节点访问一次,结果列表使用链表的结构时,在结果列表头部添加一层节点值的列表的时间复杂度是 O(1),因此总时间复杂度是 O(n)。
- 空间复杂度:O(n),其中 n 是二叉树中的节点个数。空间复杂度取决于队列开销,队列中的节点个数不会超过 n。
class Solution {
public List<List<Integer>> levelOrderBottom(TreeNode root) {
// 声明返回值的存储空间
List<List<Integer>> levelOrder = new LinkedList<List<Integer>>();
// 如果节点为空,则直接返回
if(root == null){
return levelOrder;
}
// 层序遍历的节点存储空间 BFS
Queue<TreeNode> queue = new LinkedList<TreeNode>();
queue.offer(root);
// 节点还未访问完全访问
while(!queue.isEmpty()){
// 存储每层节点的 val
List<Integer> curLevel = new ArrayList<Integer>();
int size = queue.size();
// 对当前层的元素进行访问
for(int i = 0; i < size; i ++){
TreeNode node = queue.poll();
curLevel.add(node.val);
// 查看当前元素的左右是否为空,否则加入队列以进行下一层的遍历
TreeNode left = node.left;
TreeNode right = node.right;
if(left != null){
queue.offer(left);
}
if(right != null){
queue.offer(right);
}
}
// 在遍历完一层节点之后,将存储该层节点值的列表添加到结果列表的头部
levelOrder.add(0, curLevel);
}
return levelOrder;
}
}
class Solution {
public List<List<Integer>> levelOrderBottom(TreeNode root) {
// 声明返回值的存储空间
List<List<Integer>> levelOrder = new LinkedList<List<Integer>>();
// 如果节点为空,则直接返回
if(root == null){
return levelOrder;
}
// 层序遍历的节点存储空间 BFS
Queue<TreeNode> queue = new LinkedList<TreeNode>();
queue.offer(root);
// 节点还未访问完全访问
while(!queue.isEmpty()){
// 存储每层节点的 val
List<Integer> curLevel = new ArrayList<Integer>();
int size = queue.size();
// 对当前层的元素进行访问
for(int i = 0; i < size; i ++){
TreeNode node = queue.poll();
curLevel.add(node.val);
// 查看当前元素的左右是否为空,否则加入队列以进行下一层的遍历
TreeNode left = node.left;
TreeNode right = node.right;
if(left != null){
queue.offer(left);
}
if(right != null){
queue.offer(right);
}
}
// 直接进行自顶向下的遍历
levelOrder.add(curLevel);
}
// 对最后的结果进行翻转返回
Collections.reverse(levelOrder);
return levelOrder;
}
}