1 二叉树遍历
二叉树遍历有前中后序遍历三种方式,每种方式的递归和迭代遍历都是必须掌握的。
1.1 递归
二叉树的递归遍历比较统一,前中后序的遍历代码基本相同,只是交换了前后顺序。
//前序递归
//输入输出
List<Integer> res = new ArrayList<>();
preOrder(res, root);
return res;
//递归代码
private void preOrder(TreeNode node, List<> res) {
if(node == null) return;
//该处体现前序,中间节点先加入结果集,就是前序
//中序和后序就是将该处代码顺序交换
res.add(node.val);
preOrder(node.left);
preOrder(node.right);
}
1.2 迭代
迭代法的迭代过程是借助栈实现的。 迭代法的前序遍历和后序遍历基本类似,中序遍历则有所不同。
- 前序遍历与后序遍历。值得注意的是,每次从栈中弹出,需要用一个TreeNode来接住弹出的节点。
//1.前序迭代
public List<Integer> preOrdre(TreeNode root) {
List<Integer> res = new ArrayList<>();
if(root == null) return null;
//对于栈,需要注意的地方是,不能将null加进去
Stack<TreeNode> stack = new Stack<>();
stack.push(root);
while(!stack.isEmpty()) {
//前序迭代,首先处理中间的元素
TreeNode node = stack.pop();
res.add(node.val);
//对于左右的元素,应该是先处理最左的元素,然后处理最左元素对于的右元素
//所以入栈顺序应该是先入右边,再如左边,弹出就是上面的顺序了
if(node.right != null) stack.push(node.right);
if(node.left != null) stack.push(node.left);
}
}
//2.后序迭代
//后序迭代的思路是,以中-右-左的方式迭代,反转成左-右-中的形式
public List<Integer> postOrder(TreeNode root) {
List<Integer> res = new ArrayList<>();
if(root == null) return null;
Stack<TreeNode> stack = new Stack<>();
stack.push(root);
while(!stack.isEmpty()) {
TreeNode node = stack.pop();
res.add(node.val);
if(node.left != null) stack.push(node.left);
if(node.right != null) stack.push(node.right);
}
Collections.reverse(res);
}
2.中序遍历:中序遍历为什么和前面两者不同?原因在于中序遍历的访问顺序与处理顺序是不同的,而前面两者中是相同的。所以中序遍历要借助指针来访问节点,而栈来处理元素。
- 遍历过程中,每个节点都会做为小二叉树的中节点和左右节点。当作为中节点时,是在遍历过程中的,不加入结果集;作为左右节点时,加入结果集。
//3.中序
public List<Integer> postOrder(TreeNode root) {
List<Integer> res = new ArrayList<>();
if(root == null) return null;
Stack<TreeNode> stack = new Stack<>();
//这里与前面相反的地方,是因为root并不是第一个弹出处理的节点
TreeNode cur = root;
//对于该判断条件,有以下4种情况。有三种是成立的,所以选择与
//1.cur != null stack.Empty 这是刚进入迭代,还未向栈中添加元素
//2.cur != null !stack.Empty 这是迭代过程中
//3.cur == null !stack.Empty 这是迭代到叶子节点
//4.cur == null stack.Empty 迭代到叶子节点,而且栈中处理完了,表明遍历完这棵树了
while(cur != null || !stack.isEmpty()) {
//遍历过程中,cur != null 只有两种身份,左子节点或者是右子节点
//cur != null 的处理就是入栈,遍历
if(cur != null) {
stack.push(cur);
cur = cur.left;
}
//cur == null 的情况就是遍历完一条路径,将会从左子节点或者右子节点跳回到父节点,即中间的点;而在此时,是作为上一个小二叉树的左子节点
else {
cur = stack.pop();
res.add(cur);
cur = cur.right;
}
}
}
1.3 统一迭代法
风格统一的前中后序迭代法。主要思路是空指针标记法,用空指针标记要加入结果集的元素,即在小二叉树中作为左右节点时的元素。
//1.中序遍历统一迭代法
public List<Integer> postOrder(TreeNode root) {
List<Integer> res = new ArrayList<>();
if(root == null) return null;
Stack<TreeNode> stack = new Stack<>();
stack.push(root);
while(!stack.Empty()) {
TreeNode node = stack.peek();
//栈顶元素 != null 表明是作为遍历过程节点,那么要加入其左右子节点
if(node != null) {
stack.pop();
if(node.right != null) stack.push(node.right);
stack.push(node);
stack.push(null);//遍历使命介绍,下次就加入结果集
if(node.left != null) stack.push(node.left);
}
else {
stack.pop();
node = stack.pop();
res.add(node.val);
}
}
return res;
}
相比于中序遍历,前序和后序遍历的区别就在于:遍历过程中入栈的顺序,其差异如下:
//2.前序遍历
if(node != null) {
stack.pop();
if(node.right != null) stack.push(node.right);
if(node.left != null) stack.push(node.left);
stack.push(node);
stack.push(null);
}
1.4 层序遍历
前面的所有遍历方式本质上都是深度优先遍历,即DFS。而层序遍历是BFS,广度优先遍历,这里会使用到的数据结构是队列。
class Solution {
public List<List<Integer>> resList = new ArrayList<List<Integer>>();
public List<List<Integer>> levelOrder(TreeNode root) {
//checkFun01(root,0);
checkFun02(root);
return resList;
}
//1.递归实现层序遍历
//传入参数:deep
private void checkFun01(TreeNode node, Integer deep) {
//终止条件
if(node == null) return;
deep += 1;//从上往下调用,每次传入的都是上一层的deep
if(res.size() < deep) {
res.add(new ArrayList<>());
}
res.get(deep-1).add(node.val);
//传递给左右子节点
checkFun01(node.left, deep);
checkFun01(node.right, deep);
}
//2.迭代实现层序遍历
//队列实现,每层从左到右依次进队,处理每个节点时,每个节点的左右子节点也依次进队
private void checkFunc02(TreeNode node) {
if(node == null) return;
Queue<TreeNode> que = new LinkedList<TreeNode>();
que.offer(node);
while(!que.isEmpty()) {
List<Integer> itemList = new ArrayList<>();
int len = que.size();//获得当前层的长度
while(len > 0) {
TreeNode tmp = que.poll();
itemList.add(tmp.val);
if(tmp.left != null) que.offer(tmp.left);
if(tmp.right != null) que.offer(tmp.right);
len -= 1;
}
res.add(itemList);
}
}
2 构造二叉树
2.1 根据前中后序构造二叉树
2.1.1 前中后序数组分析
无论是何种顺序的数组,将最上层的节点去除后,左右子树在数组中也是左右分离的。这是分析这类题目的基础。
- 中序:1)最上层节点在数组中间,该节点的左右分别为左右子树。2)每个左右子树的序列的最后一位都为最右节点。
- 后序:1)每个独立二叉树序列中,最后一位为顶层节点(遍历时的当前节点)。
- 前序:与后序相反。
2.1.2 前序和中序构造二叉树
//前序与中序构造二叉树
//用Map实现查找操作
//采用左闭右开
Map<Integer, Integer> inMap;
public TreeNode buildTree(int[] preorder, int[] inorder) {
if(preorder == null) return null;
inMap = new HashMap<>();
int n = inorder.length;
for(int i = 0; i < n; i++) {
inMap.put(inorder[i], i);
}
return findNode(preorder, inorder, 0, n, 0,n);
}
private TreeNode findNode(int[] preorder, int[] inorder, int inBegin, int inEnd, int preBegin, int preEnd) {
if(inBegin >= inEnd && preBegin >= preEnd) return null;
int index = inMap.get(preorder[preBegin]);
int len = index - inBegin;
//len表示左右子树的长度
//指针加上len就是右开的右边界的值
TreeNode node = new TreeNode(preorder[preBegin]);
node.left = findNode(preorder, inorder, inBegin, index, preBegin + 1, preBegin + len + 1);
node.right = findNode(preorder, inorder, index + 1, inEnd, preBegin + len + 1, preEnd);
return node;
}
2.2 根据层序遍历构造二叉树
2.2.1 层序遍历数组分析
此处考虑有null值的数组,整体满足层序的顺序。同时缺少的左右子节点也通过null值标识。
上图所示数组为[1,2,3,null,null,4,5]。可以得到:
- 除了根节点外,其他节点都是左右成对出现,遍历时可以成对遍历。
- 没有子节点的情况,通过不加入队列实现。
2.2.2 构造二叉树
//层序遍历构造二叉树
public TreeNode deserialize(Integer[] data) {
Queue<TreeNode> queue = new LinkedList<>();
//层序遍历使用队列来实现:
//队列中只进不为null的值,入队以下层元素的身份入队
//出队以上层节点的身份出队
TreeNode root = new TreeNode(data[0]);
queue.add(root);
int i = 1;
//指针 i:
//1.每次出队,指针都指向的是出队节点的左子节点
//2.每个节点,指针都要向后移两位
while(!queue.isEmpty()) {
TreeNode node = queue.poll();
if(val[i] != null) {
node.left = new TreeNode(val[i]);
queue.add(node.left);
}
node += 1;
if(val[i] != null) {
node.right = new TreeNode(val[i]);
queue.add(node.right);
}
}
return root;
}
3 二叉树混合其他题型
3.1 二叉树 + 前缀和 + 哈希表
LeeCode原题链接:leetcode.cn/problems/pa…
3.1.1 题目要求及解析
- 题目要求:求出二叉树中节点之和为targetSum的路径数目。路径不需要从根节点开始,也不需要在叶子节点结束,但是路径方向必须是向下的
- 解析:遍历二叉树的过程中构造前缀和表。该表中的前后任意两值相减可以得到任意路径的和。
根据以上公式可得对于遍历到的节点,只需要寻找前序前缀和中是否有preSum - targetSum的值
3.1.2 代码实现
//路径总和:前缀和 + 哈希表
class Solution {
private int ans;
public int pathSum(TreeNode root, int targetSum) {
Map<Long, Integer> preSum = new HashMap<>();//哈希表
preSum.put(0L, 1);
long s = 0;//前缀和
dfs(root, targetSum, s, preSum);
return ans;
}
private void dfs(TreeNode root, int targetSum, long s, Map<Long, Integer> preSum) {
//前序DFS遍历,每遍历一个节点更新前缀和
if(root == null) return;
s += root.val;//更新前缀和
ans += preSum.getOrDefault(s - targetSum, 0);//更新结果,getOrDefault:有就返回,没有就返回默认值
preSum.merge(s, 1, Integer::sum);//更新前缀和表,merge:修改哈希表中元素,有就sum,没有就设为1。
dfs(root.left, targetSum, s, preSum);
dfs(root.right, targetSum, s, preSum);
preSum.merge(s, -1, Integer::sum);//回溯,恢复现场
}
}
3.2 二叉树 + 动归
LeeCode链接:leetcode.cn/problems/un…
3.2.1 题目要求
- 题目要求:求出从1到n的n个节点组成的二叉树一共有多少种。
- 解析:n个节点的二叉树的种类可由n个节点分别做根节点组成。
3.2.2 代码实现
//二叉树种类:动规
public int numTrees(int n) {
int[] dp = new int[n+1];
dp[0] = 1;
dp[1] = 1;
for(int i = 2; i <= n; i++) {
for(int j = 1; j <= i; j++) {
dp[i] += dp[j-1] * dp[i-j];
//上述代码实现
}
}
return dp[n];
}
Hot100 二叉树题型整理
| 分类 | 题号 | 题目 | 解法 |
|---|---|---|---|
| 遍历类型(1) | 236 | 二叉树公共祖先 | DFS,后序递归遍历,找左右子树是否有节点 |
| 遍历类型(1) | 124 | 二叉树最大路径和 | 中序递归DFS,每个节点判断其子树有无最大路径,然后将更长的一半子树返给上层 |
| 遍历类型(1) | 543 | 二叉树直径 | 与124类似 |
| 遍历类型(1) | 538 | 把二叉树转换为累加树 | DFS递归,右中左 |
| 遍历类型(1) | 114 | 把二叉树转换为链表(先序遍历顺序相同) | DFS递归,将每个节点的右边接为左边、左边的末尾接到右边 |
| 遍历类型(1) | 617 | 合并二叉树 | DFS前序 |
| 遍历类型(1) | 104 | 二叉树的最大深度 | DFS后序,从底层一个个加上来 |
| 遍历类型(1) | 101 | 对称二叉树 | DFS对称遍历,(左左、右右) |
| 遍历类型(1) | 98 | 验证二叉搜索树 | 中序遍历,按顺序比较max |
| 构造类型(2) | 297 | 二叉树序列化与反序列化 | 编码:层序遍历,存储。解码:层序遍历数组构造二叉树。均使用队列 |
| 构造类型(2) | 105 | 前序与中序遍历序列构造二叉树 | 未提及具体解法 |
| 混合类型(3) | 96 | 不同的二叉搜索树 | 动态规划 |
| 混合类型(3) | 437 | 路径总和 | 前缀和 + 哈希表,前序递归遍历,遍历从根节点开始的所有路径。后面的前缀和 - 目标和 == 前面的前缀和,就找到了一个可行路径 |