利用前序和中序数组构造二叉树
思路: 构造一颗二叉树一定要用递归+hash查找
-
找根 :从
preorder中按prel拿出一个节点作为根。 -
切分 :拿着这个根的值,去
inorder中找到它的位置index。- 计算左子树的节点个数 =
index - in_left(中序左边界)。
- 计算左子树的节点个数 =
-
递归 :根据上面算出的节点个数,把
preorder和inorder切分成左右两部分,分别递归构造左子树和右子树。 -
优化(关键) :因为每次都要在
inorder中找根的位置,如果每次都用for循环找,时间复杂度会退化到 O(n2)O(n2)。我们可以用HashMap提前把inorder里"值 -> 下标"的映射存起来,这样找根的时间就变成了 O(1)O(1)。
/**
* 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;
* }
* }
*/
@param preorder 先序数组
- @param preL 当前子树在先序数组中的左边界
- @param preR 当前子树在先序数组中的右边界
- @param inL 当前子树在中序数组中的左边界
- @param inR 当前子树在中序数组中的右边界
class Solution {
private Map<Integer,Integer> indexmap=new HashMap<>();
private TreeNode build(int\[\] preorder,int prel,int prer,int inl,int inr){
if(prer<prel||inl>inr)return null;
int rootval=preorderprel;
TreeNode root=new TreeNode(rootval);
int indexin=indexmap.get(rootval);
int leftsize=indexin-inl;
root.left=build(preorder,prel+1,prel+leftsize,inl,indexin-1);
root.right=build(preorder,prel+leftsize+1,prer,indexin+1,inr);
return root;
}
public TreeNode buildTree(int\[\] preorder, int\[\] inorder) {
int n=preorder.length;
for(int i=0;i<n;i++){
indexmap.put(inorderi,i);
}
return build(preorder,0,n-1,0,n-1);
}
}
树上求路径和为指定数的路径条数
思路:树上前缀+Hsah
-
前缀和定义 :从根节点走到当前节点的路径和记为
curSum。那么,如果存在某个祖先节点,从根走到那个祖先节点的路径和是oldSum。 -
魔法公式 :如果
curSum - oldSum = targetSum,这就意味着从那个祖先节点到当前节点的这段路径和,刚好等于targetSum。- 变形一下:
oldSum = curSum - targetSum。
- 变形一下:
-
哈希表的作用 :我们在向下遍历的过程中,用一个
HashMap记录当前路径上(从根到当前节点)所有前缀和出现的次数。 -
回溯(恢复现场) :当我们处理完一个节点的左右子树,准备返回它的父节点时,必须把当前节点的
curSum从哈希表里减掉。因为当前节点已经不在我们要探索的下一条路径上了,不能影响其他分支的计算。
细节:
1.为避免树是单链表形式且结点值很大时导致前缀和溢出出现负数,map里的key前缀和要使用long类型
2.初始化:前缀和要先初始化map{0L,1};否则会出现漏掉
3.cursum是当前这条路径从根节点到当前节点的前缀和,当改变一条路径时,必须回溯
/**
* 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;
* }
* }
*/
class Solution {
private int dfs(TreeNode node,long cursum,int targetSum,Map<Long,Integer> premap){
if(node==null)return 0;
cursum+=node.val;
int count=premap.getOrDefault(cursum-targetSum,0);
premap.put(cursum,premap.getOrDefault(cursum,0)+1);
count+=dfs(node.left,cursum,targetSum,premap);
count+=dfs(node.right,cursum,targetSum,premap);
premap.put(cursum,premap.get(cursum)-1);
return count;
}
public int pathSum(TreeNode root, int targetSum) {
Map<Long,Integer> premap=new HashMap<>();
premap.put(0L,1);
return dfs(root,0L,targetSum,premap);
}
}
1.bean是单例吗?安全吗?
Spring Bean 默认是单例的,但容器本身不管线程安全。实际项目里我们的 Service 和 Dao 都是无状态(bean内部无可变的成员变量就叫做无状态)的,只调方法不存数据,所以是安全的。如果确实需要保存状态,我会用 ThreadLocal
2.string、stringBuilder、StringBuffer区别
三者核心区别在可变性 和线程安全。
| String | StringBuilder | StringBuffer | |
|---|---|---|---|
| 可变性 | 不可变(final char\[\]) | 可变 | 可变 |
| 线程安全 | 安全(因为不可变) | 不安全 | 安全(方法加 synchronized) |
| 性能 | 拼接产生新对象,慢 | 最快 | 比 StringBuilder 慢 |
| 适用场景 | 少量字符串、常量 | 单线程大量拼接 | 多线程大量拼接 |
话术:"日常拼接我用 StringBuilder,单线程没有锁开销。StringBuffer 基本不用了,只有明确需要在多线程间共享可变字符串时才考虑。
3.redis淘汰策略
Redis 提供 8 种淘汰策略
-
noeviction:不淘汰,内存满时写入报错(默认)。
-
allkeys-lru:从所有 key 中淘汰最近最少使用的。
-
allkeys-lfu:从所有 key 中淘汰最不经常使用的。
-
allkeys-random:从所有 key 中随机淘汰。
-
volatile-lru / volatile-lfu / volatile-random :只在设置了过期时间的 key 中,按 LRU/LFU/随机淘汰。
-
volatile-ttl:在设置了过期时间的 key 中,淘汰剩余存活时间最短的。
5.GC是啥,内存溢出和泄露是啥
GC(垃圾回收) :JVM 自动回收不再被引用的对象的机制。核心思想是可达性分析 ------从 GC Roots 出发,不可达的对象就是垃圾。JVM 分代回收:新生代用复制算法(对象朝生夕死,回收快),老年代用标记-整理(存活率高,回收慢)。
内存泄漏 vs 内存溢出:
6.Git reset、rebase、revert区别
简单来说,如果是我自己还没推的本地代码搞乱了,我用 reset 直接抹掉重来;如果是想把我的本地提交漂亮地接到主干后面,我用 rebase 变基;如果是线上的公共代码出问题了需要撤回,绝对只能用 revert,因为改历史(reset/rebase)只能动自己,动别人必须用 revert
rebase = Re(重新) + Base(基底/地基)。
想象你开发功能 A,是从主干 master 的节点 C1 开始建的。
但是在你开发期间,master 已经往前跑了,更新到了节点 C4。
此时,你的分支是基于旧的 C1 地基建的。
-
git merge(合并) :相当于在你的分支和master之间架一座桥,保留你的旧地基。结果是历史记录里有分叉,有个Merge commit。C2 --- C3 (master)/
C1 Merge (feature)
\ /
F1 --- F2
-
git rebase(变基) :相当于把你之前写的代码(提交)先"拔"出来,然后把你的新地基 换成master最新的C4,最后把你拔出来的代码重新"插"到C4后面。
C1 --- C2 --- C3 --- F1' --- F2' (feature)
(master)
一句话理解 :把当前分支的"基底",变成另一个分支的最新状态。
子集
给你一个整数数组 nums ,数组中的元素 互不相同 。返回该数组所有可能的子集(幂集)。
解集 不能 包含重复的子集。你可以按 任意顺序 返回解集。
示例 1:
**输入:**nums = 1,2,3
输出:\[,1,2,1,2,3,1,3,2,3,1,2,3]
思路:递归深搜+回溯 脑子里面有一颗树,每一层是从数组中挑选数字
细节:i + 1 而不是start+1的起点控制,避免出现重复、深拷贝收集结果
class Solution {
private void backtrace(int\[\] nums,int start,List path,List<List> res){
res.add(new ArrayList<>(path));
for(int i=start;i<nums.length;i++){
path.add(numsi);
backtrace(nums,i+1,path,res);
path.remove(path.size()-1);
}
}
public List<List> subsets(int\[\] nums) {
List<List> res=new ArrayList<>();
List path=new ArrayList<>();
backtrace(nums,0,path,res);
return res;
}
}
组合总和
给你一个 无重复元素 的整数数组 candidates 和一个目标整数 target ,找出 candidates 中可以使数字和为目标数 target 的 所有 不同组合 ,并以列表形式返回。你可以按 任意顺序 返回这些组合。
candidates 中的 同一个 数字可以 无限制重复被选取 。如果至少一个数字的被选数量不同,则两种组合是不同的。
对于给定的输入,保证和为 target 的不同组合数少于 150 个。
示例 1:
**输入:**candidates = 2,3,6,7, target = 7
输出: \[2,2,3,7]
解释:
2 和 3 可以形成一组候选,2 + 2 + 3 = 7 。注意 2 可以使用多次。
7 也是一个候选, 7 = 7 。
仅有这两种组合。
思路: 利用dfs+回溯+res+path+start标记
class Solution {
private void dfs(int\[\] nums,int sum,int target,int start,List path,List<List> res){
if(sum==target){
res.add(new ArrayList<>(path)); 细节1:拷贝新array
return;
}
for(int i=start;i<nums.length;i++){ 细节2:引入start而不是直接从0开始保证了下一层递归只能从当前元素的右边继续挑,绝对不可能回头去挑左边的元素,避免出门 12 21 这种情况
sum+=numsi;
if(sum>target)break; 细节6:剪枝判断,避免栈溢出
path.add(numsi);
dfs(nums,sum,target,i,path,res); 细节3:i重复选,i+1不重复选
path.remove(path.size()-1); 细节4:回溯
sum-=numsi;
}
}
public List<List> combinationSum(int\[\] candidates, int target) {
List<List> res=new ArrayList<>();
List path=new ArrayList<>();
Arrays.sort(candidates); 细节5:先排序,为了剪枝
dfs(candidates,0,target,0,path,res);
return res;
}
}