以下题目来自力扣和牛客,记录学习笔记,更新中~
BM27 按之字形顺序打印二叉树
public class Solution {
public ArrayList<ArrayList<Integer> > Print(TreeNode pRoot) {
// write code here
Deque<TreeNode> deque = new LinkedList<>();
ArrayList<ArrayList<Integer>> res = new ArrayList<>();
if(pRoot == null ) return res;
deque.add(pRoot);
int val = 1;
while(!deque.isEmpty()){
// 打印奇数层
ArrayList tmp = new ArrayList<>();
for(int i = deque.size(); i > 0; i--) {
// 从左向右打印
TreeNode node = deque.removeFirst();
tmp.add(node.val);
// 先左后右加入下层节点
if(node.left != null) deque.addLast(node.left);
if(node.right != null) deque.addLast(node.right);
}
res.add(tmp);
if(deque.isEmpty()) break; // 若为空则提前跳出
// 打印偶数层
tmp = new ArrayList<>();
for(int i = deque.size(); i > 0; i--) {
// 从右向左打印
TreeNode node = deque.removeLast();
tmp.add(node.val);
// 先右后左加入下层节点
if(node.right != null) deque.addFirst(node.right);
if(node.left != null) deque.addFirst(node.left);
}
res.add(tmp);
}
return res;
}
}
import java.util.*;
public class Solution {
// 定义递归函数功能:求出当前结点的
public int maxDepth (TreeNode root) {
// 递归终止
if(root == null) {
return 0;
}
// dfs,先递归左子结点,再递归右子结点,最后求出每一子树的深度的最大值
return Math.max(maxDepth(root.left), maxDepth(root.right)) + 1;
}
}
BM36 判断是不是平衡二叉树
可利用上题中求高度过程中通过判断是否平衡
public class Solution {
public boolean IsBalanced_Solution(TreeNode root) {
return recur(root) != -1;
}
private int recur(TreeNode root){
if(root == null) return 0;
int ldep = recur(root.left);
if(ldep == -1) return -1;
int rdep = recur(root.right);
if(rdep == -1) return -1;
int sub = Math.abs(ldep - rdep);
if(sub > 1)return -1;
return Math.max(ldep,rdep) + 1;
}
}
BM29 二叉树中和为某一值的路径(一)
public class Solution {
public boolean hasPathSum (TreeNode root, int sum) {
// write code here
if(root == null) return false;
sum -= root.val;
if(root.left == null && root.right == null && sum == 0) return true;
return hasPathSum(root.left,sum) || hasPathSum(root.right,sum);
}
}
BM30 二叉搜索树与双向链表
//方法一:已知将二叉搜索树进行中序遍历可以得到由小到大的顺序排列,将中序遍历的结果用数组存储下来,得到的数组是有从小到大顺序的。最后将数组中的结点依次连接即可。
public class Solution {
public TreeNode Convert(TreeNode pRootOfTree) {
if(pRootOfTree == null)return pRootOfTree;
List<TreeNode> list = new ArrayList<>();//定义一个数组,根据中序遍历来存储结点。
recur(list,pRootOfTree);
for(int i = 0;i<list.size() - 1;i++){//根据数组中的顺序将结点连接,注意i的范围。
list.get(i).right = list.get(i+1);
list.get(i+1).left = list.get(i);
}
return list.get(0);//数组的头部存储的是双向链表的第一个结点。
}
public void recur(List<TreeNode> list,TreeNode node){
if(node == null)return;
recur(list,node.left);
list.add(node);
recur(list,node.right);
}
}
//方法二:依旧采取中序遍历。
//(1)使用一个指针preNode指向当前结点root的前继。
//2)对于当前结点root,有root->left要指向前继preNode(中序遍历时,对于当前结点root,其左孩子已经遍历完成了,此时root->left可以被修改。);同时,preNode->right要指向当前结点(当前结点是preNode的后继),此时对于preNode结点,它已经完全加入双向链表。如上图:
public class Solution {
TreeNode preNode = null;//preNode一定是全局变量。
public TreeNode Convert(TreeNode pRootOfTree) {
if (pRootOfTree == null) return pRootOfTree;
TreeNode first = pRootOfTree;
while(first.left != null)//找到双向链表的开头。
first = first.left;
inorder(pRootOfTree);
return first;
}
public void inorder(TreeNode node){
if(node == null) return;
inorder(node.left);
//当前结点中需要进校的调整。
if(preNode != null) preNode.right = node;
node.left = preNode;
preNode = node;//更新preNode,指向当前结点,作为下一个结点的前继。
inorder(node.right);
}
}
BM31 对称的二叉树
具体做法:
step 1:两种方向的前序遍历,同步过程中的当前两个节点,同为空,属于对称的范畴。
step 2:当前两个节点只有一个为空或者节点值不相等,已经不是对称的二叉树了。
step 3:第一个节点的左子树与第二个节点的右子树同步递归对比,第一个节点的右子树与第二个节点的左子树同步递归比较。
public class Solution {
boolean isSymmetrical(TreeNode pRoot) {
return recur(pRoot,pRoot);
}
boolean recur(TreeNode root1, TreeNode root2){
if(root1 == null && root2 == null) return true;//可以两个都为空
//只有一个为空或者节点值不同,必定不对称
if(root1 == null || root2 == null || root1.val != root2.val) return false;
//每层对应的节点进入递归比较
return recur(root1.left,root2.right) && recur(root1.right,root2.left);
}
}
617. 合并二叉树![[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-nSAs90s3-1649837512252)(network-img/watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBA5LiJ5pyI5LiN54Gt,size_20,color_FFFFFF,t_70,g_se,x_16.png)]](https://i-blog.csdnimg.cn/blog_migrate/0513c37f3a3e6e1c15af4784bceb617d.png)
//二叉树的前序遍历
class Solution {
/*
//方法1:
public TreeNode mergeTrees(TreeNode t1, TreeNode t2) {
if(t1 == null)
return t2;
if(t2 == null)
return t1;
//先合并根节点
t1.val += t2.val;
//再递归合并左右子树
t1.left = mergeTrees(t1.left,t2.left);
t1.right = mergeTrees(t1.right,t2.right);
return t1;
}*/
//方法二:不改变原节点的方式:
public TreeNode mergeTrees(TreeNode t1, TreeNode t2) {
if(t1 == null && t2 == null)
return null;
//先合并跟节点
TreeNode node = new TreeNode((t1 == null ? 0 : t1.val) + (t2 == null ? 0 : t2.val));
//再递归合并左右子树
node.left = mergeTrees(t1 == null ? null : t1.left,t2 == null ? null : t2.left);
node.right = mergeTrees(t1 == null ? null : t1.right,t2 == null ? null : t2.right);
return node;
}
}
BM35 判断是不是完全二叉树
//方法:层次遍历+标记第一个空节点
public class Solution {
public boolean isCompleteTree (TreeNode root) {
// write code here
//空树一定是完全二叉树
if(root == null) return true;
//辅助队列
Queue<TreeNode> queue = new LinkedList<>();
queue.offer(root);
//定义一个首次出现的标记位,标记第一个null出现的地方
boolean notComplete = false;
TreeNode cur;
while(!queue.isEmpty()){
cur = queue.poll();
//标记第一次遇到空节点
if(cur == null){
notComplete = true;
continue;
}
//后续访问已经遇到空节点了,说明经过了叶子
if(notComplete) return false;
queue.offer(cur.left);
queue.offer(cur.right);
}
return true;
}
}
543. 二叉树的直径
//遍历每一个节点,以每一个节点为中心点计算最长路径(左子树边长+右子树边长),更新全局变量max。
//注意点:是求路径,和二叉树节点值无关,且最长直径不一定过根节点
class Solution {
int max= 0;
public int diameterOfBinaryTree(TreeNode root) {
if(root == null)
return 0;
dfs(root);
return max;
}
public int dfs(TreeNode node){
if(node.left == null && node.right == null)//如果当前节点是一个叶子节点,则递归终止
return 0;
//进行前序遍历,得到左右子树的路径和,更新全局变量max。
int left = node.left == null ? 0 : dfs(node.left) + 1;
int right = node.right == null ? 0 : dfs(node.right) + 1;
max = Math.max(left + right,max);
return Math.max(left, right);
}
}
337. 打家劫舍 II
/*
分析:首先这题存在一个坑,刚开始想当然的用层序遍历,计算每层的和,但是 [2,1,3,null,4]这样的情况,即不连接的点也可能位于相邻两层的不同子树中,这种也能rob。
思路:对于每个节点n,其左右孩子是r,l,
如果n被抢,则它的左右孩子不能抢,则sel(n) = unsel(n.left) + unsel(n.right);
如果n未被抢,则他的左右孩子可以被抢也可以不被抢,就找他们的最大值相加即可,即
unsel(n) = Math.max(sel(n.left),unsel(n.left)) + Math.max(sel(n.right),unsel(n.right))
就按上述规则,从根节点开始,使用后序遍历,从下往上遍历
*/
class Solution {
Map<TreeNode,Integer> sel = new HashMap<>();//保存每个节点被抢时的能获得的最大金额
Map<TreeNode,Integer> unsel = new HashMap<>(); //保存每个节点未被抢时能获得的最大金额
public int rob(TreeNode root) {
if(root == null)
return 0;
dfs(root);
return Math.max(sel.get(root),unsel.get(root));//获取当前节点被抢或未被抢中能获得的最大金额
}
public void dfs(TreeNode node){
if(node == null) return;
dfs(node.left);
dfs(node.right);
//如果n被选中,则它的左右孩子不能选,则sel(n) = unsel(n.left) + unsel(n.right)
sel.put(node, node.val + unsel.getOrDefault(node.left,0) + unsel.getOrDefault(node.right,0));
//如果n未被选中,则他的左右孩子可能被选也可能没有被选,就找他们的最大值相加即可,即unsel(n) = Math.max(sel(n.left),unsel(n.left)) + Math.max(sel(n.right),unsel(n.right))
unsel.put(node,Math.max(sel.getOrDefault(node.left,0),unsel.getOrDefault(node.left,0)) + Math.max(sel.getOrDefault(node.right,0),unsel.getOrDefault(node.right,0)));
}
/*
执行用时:3 ms, 在所有 Java 提交中击败了 19.30% 的用户
内存消耗:41 MB, 在所有 Java 提交中击败了 33.58% 的用户
时间复杂度 O(n):对二叉树做了一次后序遍历
空间复杂度 O(n):由于递归会使用到栈空间,空间代价 O(n),哈希表的空间代价也是 O(n)
*/
//可以对上述方法在空间上进行小小的优化,递归时每一层只用保存当前节点的两种状态,即当前节点被抢或者未被抢能rob到的最大值,然后将结果返回给上一层继续进行计算。这样可以省去哈希表的空间。
//代码如下:
public int rob(TreeNode root) {
int[] res = helper(root);
return Math.max(res[0],res[1]);
}
//方法helper(node):在以node为根节点的树中,返回抢劫根节点与不抢劫根节点可获得的最大值
public int[] helper(TreeNode node){
if(node == null) return new int[2];//边界条件
/*对于以r.left为根的树,计算抢劫根节点(r.left)与不抢劫根节点可获得最大金额.
left[0]则为不抢r.lrft可获得的最大金额,left[1]则为抢劫r.left可获得的最大金额。right[] 分析同理 */
int[] left = helper(node.left);
int[] right = helper(node.right);
//定义一个数组res,长度为2,res[0]表示不抢该节点可获得最大值,res[1]表示抢劫该节点可获得最大值
int[] res = new int[2];
res[0] = Math.max(left[0],left[1]) + Math.max(right[0],right[1]);
res[1] = node.val + left[0] + right[0];
return res;
}
/*
执行用时:0 ms, 在所有 Java 提交中击败了 100.00%的用户
内存消耗:41.2 MB, 在所有 Java 提交中击败了8.58%的用户
*/
//时间复杂度o(n),空间复杂度O(n):栈的递归调用
}
98. 验证二叉搜索树
class Solution {
/*
//[5,4,6,null,null,3,7]正确答案是false
5
/ \
4 6
/ \
3 7
//下述是第一次写法,只是简单的比较了每个节点的父节点要大于右孩子节点且小于左孩子节点,但是上述情况存在没有考虑,所以有问题。
public boolean isValidBST(TreeNode root) {
if(root == null)
return true;
if(root.left != null && root.val <= root.left.val)
return false;
if(root.right != null && root.val >= root.right.val)
return false;
return isValidBST(root.left) && isValidBST(root.right);
}
*/
//解法二(推荐):BST中序遍历是递增的,可以通过中序遍历每个节点是否大于上一个节点
long pre = Long.MIN_VALUE;
public boolean isValidBST(TreeNode root) {
if (root == null) {
return true;
}
// 访问左子树
if (!isValidBST(root.left)) {
return false;
}
// 访问当前节点:如果当前节点小于等于中序遍历的前一个节点,说明不满足BST,返回 false;否则继续遍历。
if (root.val <= pre) {
return false;
}
pre = root.val;
// 访问右子树
return isValidBST(root.right);
}
//解法三:BST的性质,左子树(不是左孩子)的所有值都要小于根节点,右子树(不是右孩子)的所有值都要大于根节点,所以每次传入一个最大值,一个最小值来限定。左子树的最大值一定是左子树的父节点,右子树的最小值一定是右子树的父节点。
public boolean isValidBST(TreeNode root) {
//初始化时最小值是Long.MIN_VALUE,最大值是Long.MAX_VALUE
//注意要使用long而不能是int,因为有一测试用例是[2147483647],只有一个节点,节点值为Integer.MAX_VALUE
return validate(root, Long.MIN_VALUE, Long.MAX_VALUE);
}
public boolean validate(TreeNode node,long min,long max){
if(node == null) return true;
if(node.val <= min || node.val >= max) return false;
//左子树的最大值一定是左子树的父节点,右子树的最小值一定是右子树的父节点。
return validate(node.left,min,node.val) && validate(node.right,node.val,max);
}
//解法四:将BST存进数组,该数组一定是一个有序递增的
}
226. 翻转二叉树 | 二叉树的镜像
class Solution {
/*
对树进行后续遍历,从叶子节点开始依次往上翻转。
如果当前遍历到的节点 root 的左右两棵子树都已经翻转,则交换两棵子树的位置,即可完成以root 为根节点的整棵子树的翻转。
*/
public TreeNode invertTree(TreeNode root) {
if(root == null)
return null;
TreeNode left = invertTree(root.left);
TreeNode right = invertTree(root.right);
root.left = right;
root.right = left;
return root;
}
}
105. 从前序与中序遍历序列构造二叉树
class Solution {
/*
前序遍历划分 [ 3 | 9 | 20 15 7 ]
中序遍历划分 [ 9 | 3 | 15 20 7 ]
根据以上性质,可得出以下推论:
前序遍历的首元素 为 树的根节点 node 的值。
在中序遍历中搜索根节点 node 的索引 ,可将 中序遍历 划分为 [ 左子树 | 根节点 | 右子树 ] 。
根据中序遍历中的左(右)子树的节点数量,可将 前序遍历 划分为 [ 根节点 | 左子树 | 右子树 ]
如节点 node 的左右子树在inorder中的范围是 [nl,nr],假设 node 在 preorder 中下标为 pre_idx,在 inorder 中的下标是 in_idx,
则 node 的左子树节点个数为 rl-ri,右子树节点个数 rr-ri,
则在 preorder 中,node 的左孩子下标为 pre_inx + 1;右孩子节点下标为 pre_inx + (rl - ri) + 1,
依次递归进行构造。
*/
int[] preorder;
HashMap<Integer,Integer> map;
public TreeNode buildTree(int[] preorder, int[] inorder) {
this.preorder = preorder;
map = new HashMap<>();
for(int i = 0; i < inorder.length; i++)
map.put(inorder[i],i);
return recur(0,0,preorder.length - 1);
}
/*
root:根节点在前序遍历的索引root,
in_left:子树在中序遍历的左边界
in_right:子树在中序遍历的右边界
*/
public TreeNode recur(Integer root, Integer in_left, Integer in_right){
//当 in_left > in_right ,代表已经越过叶节点,此时返回null
if(in_left > in_right)
return null;
//1、建立根节点 node : 节点值为 preorder[root] ;
TreeNode node = new TreeNode(preorder[root]);
//2、划分左右子树: 查找根节点在中序遍历 inorder 中的索引 i ;
int i = map.get(preorder[root]);
//3、构建左右子树: 开启左右子树递归;
node.left = recur(root + 1, in_left, i - 1);
node.right = recur(root + (i - in_left) + 1,i + 1,in_right);
return node;
}
}
68. 二叉树的最近公共祖先
//按后序遍历依次从下往上查找公共节点
class Solution {
public TreeNode lowestCommonAncestor(TreeNode root, TreeNode p, TreeNode q) {
/*
终止条件:
1、当越过叶节点,则直接返回 null;
2、当 root 等于 p,或q,则直接返回 root;
*/
if(root == null || root == p || root == q) return root;
TreeNode left = lowestCommonAncestor(root.left, p, q);
TreeNode right = lowestCommonAncestor(root.right, p, q);
/*
//返回值: 根据 left 和 right ,可展开为四种情况;
1)当 left和 right同时为空: 说明 root 的左 / 右子树中都不包含 p,q ,返回 null;
2)当 left 和 right 同时不为空: 说明 p,q 分列在 root 的 异侧 (分别在 左 / 右子树),因此 root 为最近公共祖先,返回 root;
3)当 left 为空 ,right 不为空: p,q 都不在 root 的左子树中,直接返回 right 。具体可分为两种情况:
p,q 其中一个在 root 的 右子树 中,此时 right 指向 p(假设为 p );
p,q 两节点都在 root 的 右子树 中,此时的 right 指向 最近公共祖先节点 ;
4)当left不为空,right为空:与情况3同理。
*/
if(left == null) return right;
if(right == null) return left;
return root;
}
}
BM37 二叉搜索树的最近公共祖先
//方法一:和找二叉树最近公共祖先一样
public class Solution {
public int lowestCommonAncestor (TreeNode root, int p, int q) {
// write code here
return recur(root,p,q).val;
}
public TreeNode recur(TreeNode root, int p, int q) {
// write code here
if(root == null || root.val == p || root.val == q) return root;
TreeNode left = recur(root.left, p, q);
TreeNode right = recur(root.right, p, q);
if(left == null) return right;
if(right == null) return left;
return root;
}
}
//方法二:利用二叉搜索树性质
public class Solution {
public TreeNode commonAncestor (TreeNode root, int p, int q) {
if (null == root) return null;
if (root.val == p || root.val == q) return root;
// 通过递归假设我们知道了运算结果 题目含义是不会出现重复节点
if (p < root.val && q < root.val) return commonAncestor(root.left, p, q);
else if (p > root.val && q > root.val) return commonAncestor(root.right, p, q);
else return root;
}
public int lowestCommonAncestor (TreeNode root, int p, int q) {
// write code here
return commonAncestor(root, p, q).val;
}
}
114. 二叉树展开为链表
class Solution {
//一下方法参考https://leetcode-cn.com/problems/flatten-binary-tree-to-linked-list/solution/xiang-xi-tong-su-de-si-lu-fen-xi-duo-jie-fa-by--26/
//解法一:
/*
1
/ \
2 5
/ \ \
3 4 6
//将 1 的左子树插入到右子树的地方
1
\
2 5
/ \ \
3 4 6
//将原来的右子树接到左子树的最右边节点
1
\
2
/ \
3 4
\
5
\
6
//将 2 的左子树插入到右子树的地方
1
\
2
\
3 4
\
5
\
6
//将原来的右子树接到左子树的最右边节点
1
\
2
\
3
\
4
\
5
\
6
......
*/
public void flatten(TreeNode root) {
if(root == null)
return ;
while(root != null){//依次遍历每个节点
//左子树为null,直接考虑下一个节点
if(root.left == null)
root = root.right;
else{
TreeNode pre = root.left;
// 找左子树最右边的节点,该节点就是右子树的前驱节点
while(pre.right != null) pre = pre.right;
//将原来的右子树接到左子树的最右边节点
pre.right = root.right;
// 将左子树插入到右子树的地方
root.right = root.left;
root.left = null;
//考虑下一个节点
root = root.right;
}
}
}
/*
//解法二:
//按二叉树的后序遍历从下往上,将左子树连接到父节点的右边,然后找到该左子树最右边的节点,该节点就是右子树的前驱节点,将左子树连接在该节点的右边
1
/ \
2 5
/ \ / \
3 4 6 7
1 1
/ \ / \
2 5 2 5
/ / \ \ / \
3 6 7 4 ----> 3 6 7
\
4
1 1
/ \ / \
2 5 2 5
\ / \ \
3 6 7 ----> 3 6
\ \ \
4 4 7
1 1
/ \
2 5 2
\ \ ---> \
3 6 3
\ \ \
4 7 4
\
5
\
6
\
7
......
*/
public void flatten(TreeNode root) {
if(root == null) return;
flatten(root.left);
flatten(root.right);
TreeNode tmp = root.right;
root.right = root.left;
root.left = null;
//找到该左子树最右边的节点,该节点就是右子树的前驱节点
while(root.right != null) root = root.right;
root.right = tmp;
}
//解法三:先序遍历
public void flatten(TreeNode root) {
if(root == null)
return;
LinkedList<TreeNode> s = new LinkedList<>();
s.push(root);
TreeNode pre = null;
while(!s.isEmpty()){
TreeNode tmp = s.pop();
if(pre != null){
pre.right = tmp;
pre.left = null;
}
//由于先序遍历会丢失右孩子,所以先把右孩子入栈
if(tmp.right != null)
s.push(tmp.right);
if(tmp.left != null)
s.push(tmp.left);
pre = tmp;
}
}
}
437. 路径总和 III
class Solution {
//方法一:双重递归 思路:首先先序递归遍历每个节点,再以每个节点作为起始点递归寻找满足条件的路径。
int res = 0;
public int pathSum(TreeNode root, int targetSum) {
if(root == null)
return 0;
sum(root,targetSum);
pathSum(root.left,targetSum);
pathSum(root.right,targetSum);
return res;
}
public void sum(TreeNode root, int sum){
if(root == null)
return;
sum -= root.val;
if(sum == 0) res++;
sum(root.left,sum);
sum(root.right,sum);
return;
}
//上述方法,时间复杂度应该O(nlogn),总的遍历n个节点,然后每个节点遍历至其所有子节点,后面平均长度为logn。
//方法二:如果一条路径上某节点A的前缀和为currSum,在之前的路径上存在节点B的前缀和为currSunm - target的节点,则从A到B是一条满足条件的节点(A-B的和为target).
public int pathSum(TreeNode root,int sum){
Map<Integer,Integer> prefixSumCount = new HashMap<>();//key:前缀和,value:该前缀和出现的次数
prefixSumCount.put(0,1);//前缀和为0的有1条( 当currSum-target等于0时,表示从根节点到达此节点即为一条目标和为target的路径!所以该状态要放入)
return recurPathSum(root,prefixSumCount,sum,0);
}
/*
/**
* 前缀和的递归回溯思路
* 从当前节点反推到根节点(反推比较好理解,正向其实也只有一条),有且仅有一条路径,因为这是一棵树
* 如果此前有和为currSum-target,而当前的和又为currSum,两者的差就肯定为target了
* 所以前缀和对于当前路径来说是唯一的,当前记录的前缀和,在回溯结束,回到本层时去除,保证其不影响其他分支的结果
* @param node 树节点
* @param prefixSumCount 前缀和Map
* @param target 目标值
* @param currSum 当前路径和
* @return 满足题意的解
*/
public int recurPathSum(TreeNode root,Map<Integer,Integer> prefixSumCount,Integer target,Integer currSum){
//递归终止条件
if(root ==null) return 0;
//2、本层要做的事
int res = 0;
currSum += root.val;//当前路径上的和
// 看看root到当前节点这条路上是否存在节点前缀和加target为currSum的路径
// 当前节点->root节点反推,有且仅有一条路径,如果此前有和为currSum-target,而当前的和又为currSum,两者的差就肯定为target了
// currSum-target相当于找路径的起点,起点的sum+target=currSum,当前点到起点的距离就是target
res += prefixSumCount.getOrDefault(currSum - target, 0);
//更新路径上当前节点前缀和的个数
prefixSumCount.put(currSum, prefixSumCount.getOrDefault(currSum, 0) + 1);
//3、进入到下一层
res += recurPathSum(root.left,prefixSumCount,target,currSum);
res += recurPathSum(root.right,prefixSumCount,target,currSum);
//4、回到本层,恢复状态,去除当前节点的前缀和数量
prefixSumCount.put(currSum,prefixSumCount.get(currSum)-1);
return res;
}
}