二叉树
今天将带大家完成 道剑指 Offer 的二叉树相关题目。
对于此类题目,都可以运用「DFS」&「BFS」来进行求解,属于 高频 且 简单 类型的题目。
剑指 Offer 32 - III. 从上到下打印二叉树 III
请实现一个函数按照之字形顺序打印二叉树,即第一行按照从左到右的顺序打印,第二层按照从右到左的顺序打印,第三行再按照从左到右的顺序打印,其他行以此类推。
例如:
给定二叉树: [3,9,20,null,null,15,7],
3
/ \
9 20
/ \
15 7
返回其层次遍历结果:
[
[3],
[20,9],
[15,7]
]
提示:
节点总数 <= 1000
迭代 - BFS
这题相比于前两道二叉树打印题目,增加了打印方向的要求。
在 BFS 过程中,入队我们可以仍然采用「左子节点优先入队」进行,而在出队构造答案时,我们则要根据当前所在层数来做判别:对于所在层数为偶数(root 节点在第 层),我们按照「出队添加到尾部」的方式进行;对于所在层数为奇数,我们按照「出队添加到头部」的方式进行。
为支持「从尾部追加元素」和「从头部追加元素」操作,
Java可使用基于链表的LinkedList,而TS可创建定长数组后通过下标赋值。
其中判断当前所在层数,无须引用额外变量,直接根据当前 ans 的元素大小即可。
Java 代码:
class Solution {
public List<List<Integer>> levelOrder(TreeNode root) {
List<List<Integer>> ans = new ArrayList<>();
Deque<TreeNode> d = new ArrayDeque<>();
if (root != null) d.addLast(root);
while (!d.isEmpty()) {
LinkedList<Integer> list = new LinkedList<>();
int sz = d.size(), dirs = ans.size() % 2;
while (sz-- > 0) {
TreeNode t = d.pollFirst();
if (dirs == 0) list.addLast(t.val);
else list.addFirst(t.val);
if (t.left != null) d.addLast(t.left);
if (t.right != null) d.addLast(t.right);
}
ans.add(list);
}
return ans;
}
}
Typescript 代码:
function levelOrder(root: TreeNode | null): number[][] {
const ans = new Array<Array<number>>()
const stk = new Array<TreeNode>()
let he = 0, ta = 0
if (root != null) stk[ta++] = root
while (he < ta) {
const dirs = ans.length % 2 == 0
let sz = ta - he, idx = dirs ? 0 : sz - 1
const temp = new Array<number>(sz)
while (sz-- > 0) {
const t = stk[he++]
temp[idx] = t.val
idx += dirs ? 1 : -1
if (t.left != null) stk[ta++] = t.left
if (t.right != null) stk[ta++] = t.right
}
ans.push(temp)
}
return ans
};
- 时间复杂度:
- 空间复杂度:
递归 - DFS
递归的实现方式与前两题同理。
不过对于 TS 语言来说,由于 DFS 过程中无法知道当前层有多少节点,因此只能在使用「哈希表」记录每层「从左往右」的方向,然后在构造答案时,运用「双指针」来将奇数层的节点进行翻转。
Java 代码:
class Solution {
Map<Integer, LinkedList<Integer>> map = new HashMap<>();
int max = -1;
public List<List<Integer>> levelOrder(TreeNode root) {
dfs(root, 0);
List<List<Integer>> ans = new ArrayList<>();
for (int i = 0; i <= max; i++) ans.add(map.get(i));
return ans;
}
void dfs(TreeNode root, int depth) {
if (root == null) return ;
max = Math.max(max, depth);
dfs(root.left, depth + 1);
LinkedList<Integer> list = map.getOrDefault(depth, new LinkedList<>());
if (depth % 2 == 0) list.addLast(root.val);
else list.addFirst(root.val);
map.put(depth, list);
dfs(root.right, depth + 1);
}
}
TypeScript 代码:
const map: Map<number, Array<number>> = new Map<number, Array<number>> ()
let max = -1
function levelOrder(root: TreeNode | null): number[][] {
map.clear()
max = -1
dfs(root, 0)
const ans = new Array<Array<number>>()
for (let i = 0; i <= max; i++) {
const temp = map.get(i)
if (i % 2 == 1) {
for (let p = 0, q = temp.length - 1; p < q; p++, q--) {
const c = temp[p]
temp[p] = temp[q]
temp[q] = c
}
}
ans.push(temp)
}
return ans
};
function dfs(root: TreeNode | null, depth: number): void {
if (root == null) return
max = Math.max(max, depth)
dfs(root.left, depth + 1)
if (!map.has(depth)) map.set(depth, new Array<number>())
map.get(depth).push(root.val)
dfs(root.right, depth + 1)
}
- 时间复杂度:
- 空间复杂度:
剑指 Offer 34. 二叉树中和为某一值的路径
给你二叉树的根节点 root 和一个整数目标和 targetSum,找出所有 从根节点到叶子节点 路径总和等于给定目标和的路径。
叶子节点 是指没有子节点的节点。
示例 1:
输入:root = [5,4,8,11,null,13,4,7,2,null,null,5,1], targetSum = 22
输出:[[5,4,11,2],[5,8,4,5]]
示例 2:
输入:root = [1,2,3], targetSum = 5
输出:[]
示例 3:
输入:root = [1,2], targetSum = 0
输出:[]
提示:
- 树中节点总数在范围 内
DFS
较为直观的做法是使用 DFS,在 DFS 过程中记录路径以及路径对应的元素和,当出现元素和为 target,且到达了叶子节点,说明找到了一条满足要求的路径,将其加入答案。
使用 DFS 的好处是在记录路径的过程中可以使用「回溯」的方式进行记录及回退,而无须时刻进行路径数组的拷贝。
Java 代码:
class Solution {
List<List<Integer>> ans = new ArrayList<>();
int t;
public List<List<Integer>> pathSum(TreeNode root, int target) {
t = target;
dfs(root, 0, new ArrayList<>());
return ans;
}
void dfs(TreeNode root, int cur, List<Integer> list) {
if (root == null) return ;
list.add(root.val);
if (cur + root.val == t && root.left == null && root.right == null) ans.add(new ArrayList<>(list));
dfs(root.left, cur + root.val, list);
dfs(root.right, cur + root.val, list);
list.remove(list.size() - 1);
}
}
- 时间复杂度:最坏情况所有路径均为合法路径,复杂度为
- 空间复杂度:最坏情况所有路径均为合法路径,复杂度为
BFS
使用 BFS 的话,我们需要封装一个类/结构体 TNode,该结构体存储所对应的原始节点 node,到达 node 所经过的路径 list,以及对应的路径和 tot。
由于 BFS 过程并非按照路径进行(即相邻出队的节点并非在同一路径),因此我们每次创建新的 TNode 对象时,需要对路径进行拷贝操作。
Java 代码:
class Solution {
class Node {
TreeNode node;
List<Integer> list;
int tot;
Node (TreeNode _node, List<Integer> _list, int _tot) {
node = _node; list = new ArrayList<>(_list); tot = _tot;
list.add(node.val); tot += node.val;
}
}
public List<List<Integer>> pathSum(TreeNode root, int target) {
List<List<Integer>> ans = new ArrayList<>();
Deque<Node> d = new ArrayDeque<>();
if (root != null) d.addLast(new Node(root, new ArrayList<>(), 0));
while (!d.isEmpty()) {
Node t = d.pollFirst();
if (t.tot == target && t.node.left == null && t.node.right == null) ans.add(t.list);
if (t.node.left != null) d.addLast(new Node(t.node.left, t.list, t.tot));
if (t.node.right != null) d.addLast(new Node(t.node.right, t.list, t.tot));
}
return ans;
}
}
Typescript 代码:
class TNode {
node: TreeNode
tot: number
list: Array<number>
constructor(node: TreeNode, tot: number, list: Array<number>) {
this.node = node; this.tot = tot; this.list = list.slice();
this.list.push(node.val)
this.tot += node.val
}
}
function pathSum(root: TreeNode | null, target: number): number[][] {
const ans = new Array<Array<number>>()
const stk = new Array<TNode>()
let he = 0, ta = 0
if (root != null) stk[ta++] = new TNode(root, 0, new Array<number>())
while (he < ta) {
const t = stk[he++]
if (t.tot == target && t.node.left == null && t.node.right == null) ans.push(t.list)
if (t.node.left != null) stk[ta++] = new TNode(t.node.left, t.tot, t.list)
if (t.node.right != null) stk[ta++] = new TNode(t.node.right, t.tot, t.list)
}
return ans
};
- 时间复杂度:最坏情况所有路径均为合法路径,复杂度为
- 空间复杂度:最坏情况所有路径均为合法路径,复杂度为
剑指 Offer 32 - I. 从上到下打印二叉树
从上到下打印出二叉树的每个节点,同一层的节点按照从左到右的顺序打印。
例如:
给定二叉树: [3,9,20,null,null,15,7],
3
/ \
9 20
/ \
15 7
返回:
[3,9,20,15,7]
提示:
节点总数 <= 1000
迭代 - BFS
使用「迭代」进行求解是容易的,只需使用常规的 BFS 方法进行层序遍历即可。
Java 代码:
class Solution {
public int[] levelOrder(TreeNode root) {
List<Integer> list = new ArrayList<>();
Deque<TreeNode> d = new ArrayDeque<>();
if (root != null) d.addLast(root);
while (!d.isEmpty()) {
TreeNode t = d.pollFirst();
list.add(t.val);
if (t.left != null) d.addLast(t.left);
if (t.right != null) d.addLast(t.right);
}
int n = list.size();
int[] ans = new int[n];
for (int i = 0; i < n; i++) ans[i] = list.get(i);
return ans;
}
}
TypeScript 代码:
function levelOrder(root: TreeNode | null): number[] {
let he = 0, ta = 0
const ans: number[] = new Array<number>()
const d: TreeNode[] = new Array<TreeNode>()
if (root != null) d[ta++] = root
while (he < ta) {
const t = d[he++]
ans.push(t.val)
if (t.left != null) d[ta++] = t.left
if (t.right != null) d[ta++] = t.right
}
return ans
};
- 时间复杂度:
- 空间复杂度:
递归 - DFS
使用「递归」来进行「层序遍历」虽然不太符合直观印象,但也是可以的。
此时我们需要借助「哈希表」来存储起来每一层的节点情况。
首先我们按照「中序遍历」的方式进行 DFS,同时在 DFS 过程中传递节点所在的深度(root 节点默认在深度最小的第 层),每次处理当前节点时,通过哈希表获取所在层的数组,并将当前节点值追加到数组尾部,同时维护一个最大深度 max,在 DFS 完成后,再使用深度范围 从哈希表中进行构造答案。
Java 代码:
class Solution {
Map<Integer, List<Integer>> map = new HashMap<>();
int max = -1, cnt = 0;
public int[] levelOrder(TreeNode root) {
dfs(root, 0);
int[] ans = new int[cnt];
for (int i = 0, idx = 0; i <= max; i++) {
for (int x : map.get(i)) ans[idx++] = x;
}
return ans;
}
void dfs(TreeNode root, int depth) {
if (root == null) return ;
max = Math.max(max, depth);
cnt++;
dfs(root.left, depth + 1);
List<Integer> list = map.getOrDefault(depth, new ArrayList<Integer>());
list.add(root.val);
map.put(depth, list);
dfs(root.right, depth + 1);
}
}
- 时间复杂度:
- 空间复杂度:
剑指 Offer 32 - II. 从上到下打印二叉树 II
从上到下按层打印二叉树,同一层的节点按从左到右的顺序打印,每一层打印到一行。
例如:
给定二叉树: [3,9,20,null,null,15,7],
3
/ \
9 20
/ \
15 7
返回其层次遍历结果:
[
[3],
[9,20],
[15,7]
]
提示:
节点总数 <= 1000
迭代 - BFS
这道题是 (题解)剑指 Offer 32 - I. 从上到下打印二叉树 的练习版本。
只需要在每次 BFS 拓展时,将完整的层进行取出,存如独立数组后再加入答案。
Java 代码:
class Solution {
public List<List<Integer>> levelOrder(TreeNode root) {
Deque<TreeNode> d = new ArrayDeque<>();
if (root != null) d.addLast(root);
List<List<Integer>> ans = new ArrayList<>();
while (!d.isEmpty()) {
int sz = d.size();
List<Integer> list = new ArrayList<>();
while (sz-- > 0) {
TreeNode t = d.pollFirst();
list.add(t.val);
if (t.left != null) d.addLast(t.left);
if (t.right != null) d.addLast(t.right);
}
ans.add(list);
}
return ans;
}
}
- 时间复杂度:
- 空间复杂度:
递归 - DFS
同理,可以使用「递归」来进行「层序遍历」。
此时我们需要借助「哈希表」来存储起来每一层的节点情况。
首先我们按照「中序遍历」的方式进行 DFS,同时在 DFS 过程中传递节点所在的深度(root 节点默认在深度最小的第 层),每次处理当前节点时,通过哈希表获取所在层的数组,并将当前节点值追加到数组尾部,同时维护一个最大深度 max,在 DFS 完成后,再使用深度范围 从哈希表中进行构造答案。
Java 代码:
class Solution {
Map<Integer, List<Integer>> map = new HashMap<>();
int max = -1;
public List<List<Integer>> levelOrder(TreeNode root) {
dfs(root, 0);
List<List<Integer>> ans = new ArrayList<>();
for (int i = 0; i <= max; i++) ans.add(map.get(i));
return ans;
}
void dfs(TreeNode root, int depth) {
if (root == null) return ;
max = Math.max(max, depth);
dfs(root.left, depth + 1);
List<Integer> list = map.getOrDefault(depth, new ArrayList<>());
list.add(root.val);
map.put(depth, list);
dfs(root.right, depth + 1);
}
}
- 时间复杂度:
- 空间复杂度:
总结
综上,无论是 DFS 还是 BFS 都有高度统一的模板,对于 DFS 而言,重点在于如何设计递归函数,对于 BFS 而言,重点在于设计哪些信息进行入队。
两者无论是效率上还是实现上都是较为类似。