本文是递归算法系列文的第7篇,依然沿着递归的脉络,介绍了常常运用递归处理问题的一个典型数据结构——二叉树。分析总结了LeetCode中的相关习题在解法上的思路和模板。
本文内容如下:
判定、比较结点或子树
以及 路径[ 和 ]、累计
)由于树的相关问题题目较多,本文介绍第一部分,其余部分后续更新。(又挖了一个坑)
class TreeNode {
int val;
TreeNode left;
TreeNode right;
TreeNode(int x) { val = x; }
}
LC144前序
List<Integer> res = new ArrayList<>();
public List<Integer> preorderTraversal(TreeNode root) {
if(root == null) return res;
res.add(root.val);
preorderTraversal(root.left);
preorderTraversal(root.right);
return res;
}
LC94中序
List<Integer> res = new ArrayList<>();
public List<Integer> inorderTraversal(TreeNode root) {
if(root == null) return res;
inorderTraversal(root.left);
res.add(root.val);
inorderTraversal(root.right);
return res;
}
LC145后序
List<Integer> res = new ArrayList<>();
public List<Integer> postorderTraversal(TreeNode root) {
if(root == null) return res;
postorderTraversal(root.left);
postorderTraversal(root.right);
res.add(root.val);
return res;
}
public List<Integer> preorderTraversal(TreeNode root) {
List<Integer> res = new ArrayList<>();
if(root == null) return res;
Deque<TreeNode> stack = new ArrayDeque<>();
TreeNode p = root;
while(!stack.isEmpty() || p != null){
while(p!= null){ //一口气走到左下最后一个,一边走一边入栈、同时加入结果集
stack.push(p);
res.add(p.val);
p = p.left;
}
p = stack.pop().right; //逐个往上、然后遍历右子树
}
return res;
}
注:前序还有一种写法:这种写法具有结构对称美哦~后序就知道了
public List<Integer> preorderTraversal(TreeNode root) {
List<Integer> res = new ArrayList<>();
if(root == null) return res;
Deque<TreeNode> stack = new ArrayDeque<>();
TreeNode p = root;
stack.push(p); //根节点入栈
while(!stack.isEmpty()){ //当根节点不空时,出栈一个根节点,然后加入res中
p = stack.pop();
res.add(p.val);
if(p.right != null) //加入右子树入栈
stack.push(p.right);
if(p.left != null) //左子树入栈
stack.push(p.left);
}
return res;
}
public List<Integer> inorderTraversal(TreeNode root) {
List<Integer> res = new ArrayList<>();
if(root == null) return res;
Deque<TreeNode> stack = new ArrayDeque<>();
TreeNode p = root;
while(!stack.isEmpty() || p != null){ //一口气走到左下角一边走,一边入栈,
while(p != null){ //但是是在出栈后才加到结果集
stack.push(p);
p = p.left;
}
p = stack.pop();
res.add(p.val);
p = p.right;
}
return res;
}
后序遍历使用了一个小技巧:
public List<Integer> postorderTraversal(TreeNode root) {
List<Integer> res = new ArrayList<>();
if(root == null) return res;
Deque<TreeNode> stack = new ArrayDeque<>();
// Deque temp_res = new ArrayDeque<>();
TreeNode p = root;
stack.push(p);
while(!stack.isEmpty()){
p = stack.pop();
res.add(p.val);
if(p.left != null)
stack.push(p.left);
if(p.right != null)
stack.push(p.right);
}
Collections.reverse(res); //将结果翻转,这一步也可以用栈
return res;
}
以LC102为例:输入二叉树数组,输出层次遍历结果,并且每一层为一个list,整体为二维list
https://leetcode-cn.com/problems/binary-tree-level-order-traversal/
public List<List<Integer>> levelOrder(TreeNode root) {
List<List<Integer>> res = new ArrayList<>();
if(root == null) return res;
Deque<TreeNode> queue = new ArrayDeque<>();
queue.offer(root);
while(!queue.isEmpty()){
List<Integer> item = new ArrayList<>();
for(int i = queue.size() ; i > 0 ; i --){
TreeNode node = queue.poll();
item.add(node.val);
if(node.left!= null) queue.offer(node.left);
if(node.right!= null) queue.offer(node.right);
}
res.add(item);
}
return res;
}
LC中树标签下的前60来道题经过总结,从问题角度,大概如下包括类别:【标红为本次涉及内容,未标红为后续文章内容】
遍历树进行比较(结点、子树)
深度、路径(和)、计数
(修改)构造相关
二叉搜索树相关(可能会运用BST性质来解题)
明显带有层次遍历的暗示
顺序(前继后继)
暂且大概将其分为这些类别
其实这样分类也不是太好,一是有些题的从解法层面有多种,二是有些虽然题虽然属于这一类,但是解法更像另一类。。能力有限还请见谅
LC100相同的树
思路:
判断树相同等价于:对两棵树的每一对应节点左如下判断:
public boolean isSameTree(TreeNode p, TreeNode q) {
if(p == null && q == null) return true;
if(p == null || q == null || p.val != q.val) return false;
return isSameTree(p.left , q.left) && isSameTree(p.right, q.right);
}
LC101对称二叉树
1
/ \
2 2
/ \ / \
3 4 4 3
思路:按照对称条件,对应节点相等(左子树的左子树与右子树的右子树、左子树的右子树与右子树的左子树)
public boolean isSymmetric(TreeNode root) {
if(root == null) return true;
return compare(root.left , root.right);
}
private boolean compare(TreeNode node1, TreeNode node2){
if(node1 == null && node2 == null) return true;
if(node1 == null || node2 == null || node1.val != node2.val)
return false;
return compare(node1.left , node2.right) && compare(node1.right , node2.left);
}
LC110平衡二叉树
思路1
平衡二叉树 等价于下面两个条件都满足:
public boolean isBalanced(TreeNode root){
if (root == null) return true;
return Math.abs(depth(root.left) - depth(root.right))
<=1 && isBalanced(root.left) && isBalanced(root.right);
}
//就是求高度
private int depth(TreeNode node){
if(node == null) return 0;
if(node.left == null && node.right == null)
return 1;
return Math.max(depth(node.left) , depth(node.right)) +1;
}
思路2:
public boolean isBalanced(TreeNode root){
if(root == null) return true;
return checkBalanced(root) != -1;
}
private int checkBalanced(TreeNode node){
if(node == null) return 0;
int left = checkBalanced(node.left);
if(left == -1) return -1;
int right = checkBalanced(node.right);
if(right == -1) return -1;
return Math.abs(left - right) < 2? Math.max(left , right) + 1: -1;
}
572另一个树的子树
思路:
boolean flag = false;
public boolean isSubtree(TreeNode s, TreeNode t) {
if(s == null && t == null) return true;
if(s == null || t == null) return false;
if(s .val == t.val) flag = verty(s, t);
if(flag) return true; //当前节点已经找到了,直接返回truetrue
//当前节点没找到,需要递归去左右节点找
flag = isSubtree(s.left , t) || isSubtree(s.right, t);
return flag;
}
//判断以s和t两个节点为根的两个树是否全等
private boolean verty(TreeNode s, TreeNode t) {
if(s == null && t == null) return true;
if(s == null || t == null) return false;
if(s.val != t.val) return false;
return verty(s.left , t.left) && verty(s.right , t.right);
}
LC671二叉树中第二小的节点
思路:通过题意可以看出,该二叉树的最小值即为根,因此我们只需要找到和根值不同的那个最小值即可,它可能在左子树也可能在右子树。
因此,设计递归函数helper(root, root.val)
分别在左子树和右子树中去寻找大于根的值的结点,返回二者中的较小值
public int findSecondMinimumValue(TreeNode root) {
if(root == null) return -1;
return helper(root, root.val);
}
private int helper(TreeNode root , int min){
if(root == null) return -1;
if(root.val > min) return root.val; //这里表示找到了
int left = helper(root.left , min);
int right = helper(root.right , min);
if(left == -1) return right;
if(right == -1) return left;
return Math.min(left , right);
}
LC104.二叉树的最大深度
思路:该树深度即为左子树和右子树深度大的那个值加1。 对于其左子树和右子树同样如此,因此进行递归求解
public int maxDepth(TreeNode root) {
if(root == null) return 0;
int left_height = maxDepth(root.left);
int right_height = maxDepth(root.right);
return Math.max(left_height, right_height)+1;
}
LC111.二叉树的最小深度
思路:二叉树最小深度即为左子树和右子树中深度小的那个
public int minDepth(TreeNode root) {
if(root == null) return 0;
if(root.left == null && root.right == null)
return 1;
//注意此处,当左或右子树边没有时,需要在另一边加1
if(root.left == null)
return minDepth(root.right) +1;
if(root.right == null)
return minDepth(root.left) +1;
return Math.min(minDepth(root.left) , minDepth(root.right)) +1;
}
LC124二叉树中的最大路径和
思路:求从任意一个节点到另一个节点的最大路径。我们需要在递归函数中需要做两件事:
-10 25 (34)
/ \ / \
9 20 ==》 9(9) 35 (35)
/ \ / \
15 -7 15(15) 0(-7)
int res = Integer.MIN_VALUE;
public int maxPathSum(TreeNode root) {
dfs(root);
return res;
}
private int dfs(TreeNode root){
if(root == null) return 0;
int left = dfs(root.left);
int right = dfs(root.right);
res = Math.max(res , left + root.val + right);
return Math.max(0 , root.val + Math.max(left,right));
}
LC129求根到叶子节点数字之和
思路:在进行递归遍历时,保存一个curr,用于存放目前为止的结果,因此在递归遍历中,对每个结点
int res ;
public int sumNumbers(TreeNode root) {
if(root == null) return 0;
dfs(root , 0);
return res;
}
private void dfs(TreeNode node , int curr){
if(node == null) return;
curr += node.val;
if(node.left == null && node.right == null )
res += curr;
dfs(node.left , curr*10);
dfs(node.right , curr*10);
}
LC112路径总和
思路:判断在树中存在一条和为未指定值的路径,递归遍历,每次将sum减去当前val即可
public boolean hasPathSum(TreeNode root, int sum) {
if(root == null) return false;
if(sum == root.val && root.left == null && root.right == null)
return true;
boolean left = false, right = false;
//if(root.left != null ) 前面已经对root进行判空
left = hasPathSum(root.left , sum - root.val) ;
//if(root.right != null)
right = hasPathSum(root.right , sum - root.val);
return left || right;
}
LC113路径之和2
public List<List<Integer>> pathSum(TreeNode root, int sum) {
List<List<Integer>> res = new ArrayList<>();
if(root == null) return res;
Deque<Integer> item = new ArrayDeque<>();
dfs(root, sum , res ,item);
return res;
}
private void dfs(TreeNode node , int sum , List<List<Integer>> res ,Deque<Integer> item ){
if(node == null ) return;
item.add(node.val);
if(sum == node.val && node.left == null && node.right == null){
res.add(new ArrayList<Integer>(item));
item.removeLast(); //回溯,返回上一个节点
return;
}
dfs(node.left , sum - node.val , res , item);
dfs(node.right , sum - node.val , res, item);
item.removeLast(); //回溯
}
LC437路径总和3
思路:该题在111的基础上扩充了两点:
public int pathSum(TreeNode root, int sum) {
if(root == null ) return 0;
//求以该节点为根的路径个数
int res = findPath(root, sum);
//对每个节点,都作为根进行寻找路径
int left = pathSum(root.left , sum);
int right = pathSum(root.right , sum);
//最后返回:当前节点出发的路径数+左子树所有满足的路径+右子树所有路径
return left + res + right;
}
//求以这个节点为根的路径个数的具体实现
private int findPath(TreeNode node, int sum){
if(node == null) return 0;
int count = sum == node.val ? 1 : 0; //若到达此处时,sum刚好还剩node.val,count计为1
return count + findPath(node.left ,sum - node.val) + findPath(node.right, sum -node.val);
}
LC257二叉树的所有路径
public List<String> binaryTreePaths(TreeNode root) {
List<String> res = new ArrayList<>();
if(root == null) return res;
dfs(root, res , new StringBuilder());
return res;
}
private void dfs(TreeNode root, List<String> res ,StringBuilder stringBuilder){
if(root == null ) return ;
if(root.left == null && root.right == null){
res.add(stringBuilder.toString() + root.val);
}else{
String temStr = root.val + "->";
stringBuilder.append(temStr);
dfs(root.left , res , stringBuilder);
dfs(root.right , res ,stringBuilder);
//回溯
stringBuilder.delete(stringBuilder.length() - temStr.length(),
stringBuilder.length());
}
}
LC687最长同值路径
思路:此题和124,类似,都是在递归同时要干两件事:
int res;
public int longestUnivaluePath(TreeNode root) {
res = 0;
getLength(root);
return res;
}
private int getLength(TreeNode root) {
if(root == null) return 0;
int left = getLength(root.left);//递归求解左子树的结果
int right = getLength(root.right);
int tem_left = 0 , tem_right = 0;
if(root.left != null && root.val == root.left.val)
tem_left += left+1; //左边结果值为左边递归返回的结果加1
if(root.right != null && root.val == root.right.val)
tem_right += right +1;
res = Math.max(res , tem_left +tem_right);
return Math.max(tem_left,tem_right);
}
LC404左子树之和
思路:可以中途传值
public int sumOfLeftLeaves(TreeNode root) {
if(root == null ) return 0;
int sum = 0;
if(root.left != null && root.left.left ==null && root.left.right == null)
sum += root.left.val;
return sum + sumOfLeftLeaves(root.left) + sumOfLeftLeaves(root.right);
}
LC508出现次数最多的子树元素和
思路:
Map<Integer,Integer> hasMap = new HashMap<>();
public int[] findFrequentTreeSum(TreeNode root) {
dfs_starNode(root );
//此时hashMap中存放各个顶点的子树和及其出现的次数
//遍历得到出现最多值的次数
int maxCount = 0;
for(Map.Entry<Integer, Integer> entry : hashMap.entrySet()) {
if(entry.getValue() > maxCount)
maxCount = entry.getValue();
}
//根据次数,再次遍历,得到和
List<Integer> resList = new ArrayList<>();
for(Map.Entry<Integer, Integer> entry : hashMap.entrySet()) {
if(entry.getValue() == maxCount)
resList.add(entry.getKey());
}
//list => int[]
int res[] = new int[resList.size()];
for(int i = 0 ; i < resList.size() ; i++) {
res[i] = resList.get(i);
}
return res;
}
//从每个点存放 其作为根时子树的和
private void dfs_starNode(TreeNode root) {
if(root == null) return ;
int val = dfs_getSum(root);
// resList.add(val);
if(hashMap.containsKey(val))
hashMap.put(val, hashMap.get(val)+1);
else
hashMap.put(val, 1);
dfs_starNode(root.left);
dfs_starNode(root.right);
}
//求和
private int dfs_getSum(TreeNode root) {
// System.out.println("+");
if(root == null) return 0;
return root.val +dfs_getSum(root.left) +dfs_getSum(root.right);
}
LC222.完全二叉树的结点个数
思路:
在countNodes递归函数中,做如下事情:
计算当前结点的左子树层数left
计算当前结点的右子树层数right
若left = right :
则说明就当前结点而言,左子树一定是满的(完全二叉树性质),返回时加上
若left != right
右子树满,返回时加上递归求左子树的结点数
public int countNodes(TreeNode root) {
if(root == null) return 0;
int left= countLevel(root.left);
int right = countLevel(root.right);
if(left == right)
return countNodes(root.right) + ( 1<<left); //左子树满
else
return countNodes(root.left) + (1 << right); //右子树满
}
private int countLevel(TreeNode node) {
int level = 0;
while(node!= null) {
level ++;
node = node.left ; //完全二叉树左子树具有代表性
}
return level;
}
上面的两类题型(比较判定、路径累计)从本质上来说都是属于遍历性问题的,只不过在遍历过程中,比基本遍历的操作更为麻烦(辅助变量、集合列表) 和/或 逻辑更为复杂(左右子树加和、bool运算、回溯、剪枝) 。
那么,以前序遍历的模板为例,我们可以总结归纳很多相类似问题的解题规律和套路(希望如此):
(1)基础遍历模板:
public void preorder(TreeNode root) {
if(root == null) return; //判空条件不可少,因为下面要取root的左右子树,也可能取val
/* 这里每一轮遍历需要做的事情 */
//TODO
preorder(root.left); //递归对其左子树做相同的事情
preorder(root.right); //递归对其右子树做相同的事情
}
在每一个结点的关键处理步骤中:
/* 这里每一轮遍历需要做的事情 */
我们可能用一个变量去累计每个结点的和(已达到我们需要的结果)
也可能会用一个集合去收集,那么这些个集合作为参数在递归过程中传递就好,这一点在前面文章中有详细介绍:
public void dfs(TreeNode root, ArrayList<Integer> item, ... ) {
//下面操作类似,每一轮可能会有添加,移除等试探回溯的操作
注意到这种返回类型是void,其实我们一般会在外部声明好一些变量(累计和、结果集合等),在遍历过程中对其进行改变,因此这种往往作为一些辅助函数,比如:
//LC671二叉树中第二小的节点
public int findSecondMinimumValue(TreeNode root) {
if(root == null) return -1;
return helper(root, root.val);
}
...
//LC127
public List<String> binaryTreePaths(TreeNode root) {
List<String> res = new ArrayList<>(); //外部变量,在递归中进行修改
if(root == null) return res;
dfs(root, res , new StringBuilder()); //主要进行递归的函数
return res;
}
...
(2)带返回值的遍历模板:
有时我们会设计递归函数返回我们所需要的结果,比如:
返回 数值 类型:
往往出现在求某些满足条件的结果,累计(深度、个数等)、路径(和)
返回 布尔 类型:
往往出现在对于结点的判断(比较相等、是否满足条件)
数值型遍历模板如下:
public int dfs(TreeNode root) {
if(root == null) return 0;
int left = dfs(root.left); //递归对root的左子树求取结果
int right = dfs(root.right); //递归对root的右子树求取结果
/* 一些对左右子树的操作、比较、统计等 */
//TODO
return 与left和right有关的操作结果
}
上面那种也可以看做是一股脑走到叶子结点,然后再逐个返回其结果,然后根据返回的结果进行相应的操作。
例如:返回一棵树中所有结点值的和
private int dfs_getSum(TreeNode root) {
if(root == null) return 0;
int left = dfs(root.left);
int right = dfs(root.right);
return root.val +left + right;
}
例如:LC104求二叉树的最大深度:
public int maxDepth(TreeNode root) {
if(root == null) return 0;
int left_height = maxDepth(root.left);
int right_height = maxDepth(root.right);
return Math.max(left_height, right_height)+1; //取左右子树中深的那个,再加1
}
判断:检查是否平衡(通过返回值的绝对值小于等于1来判断):
private int checkBalanced(TreeNode node){
if(node == null) return 0;
int left = checkBalanced(node.left); //递归求左子树
if(left == -1) return -1;
int right = checkBalanced(node.right);
if(right == -1) return -1;
//关键:判断每个结点的左右子树的高度差绝对值是否小于2
//若是,则加上自身的高度(返回:子树高度+1)
//若否:直接返回-1
return Math.abs(left - right) < 2? Math.max(left , right) + 1: -1;
}
bool型遍历模板类似,只是在操作中更多的是比较:
public boolean dfs(TreeNode root){
if(root == null) return false;
/*这里往往有一些满足结果的结束条件,也有可能有一些剪枝条件*/
//TODO
boolean left = false , right = false;
left = dfs(root.left); //递归对左子树进行判断
right = dfs(root.right); //递归对右子树进行判断
return 对左右结果left和right进行操作,这里往往就是bool操作
}
例如:LC112路径总和
判断棵树中是否有一条从根到叶子的路径,使得其结点val的和等于sum。
public boolean hasPathSum(TreeNode root, int sum) {
if(root == null) return false;
if(sum == root.val && root.left == null && root.right == null)
return true;
boolean left = false, right = false;
left = hasPathSum(root.left , sum - root.val) ;
right = hasPathSum(root.right , sum - root.val);
return left || right;
}
当然,也可以是对两棵树之间进行比较
例如:LC100相同的树
public boolean isSameTree(TreeNode p, TreeNode q) {
if(p == null && q == null) return true;
if(p == null || q == null || p.val != q.val) return false;
return isSameTree(p.left , q.left) && isSameTree(p.right, q.right); //递归对p树的左节点和q数的左节点比较,以及p树的右节点和q树的右节点比较,结果取交集
}
以及对称二叉树。
(3)一些复杂或是混合的遍历
这种往往也是由上述两种构成,拆分就好。比如有时我们会进行下列考察:
本文介绍了二叉树问题中以递归遍历为主的习题以及解法思路。重点剖析了相关遍历模板类型和应用。有关递归设计思路以及其他应用可看如下文章: