LeetCode刷题记录,2024年最新分享一点面试小经验

先自我介绍一下,小编浙江大学毕业,去过华为、字节跳动等大厂,目前阿里P7

深知大多数程序员,想要提升技能,往往是自己摸索成长,但自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!

因此收集整理了一份《2024年最新Linux运维全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友。
img
img
img
img
img

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,涵盖了95%以上运维知识点,真正体系化!

由于文件比较多,这里只是将部分目录截图出来,全套包含大厂面经、学习笔记、源码讲义、实战项目、大纲路线、讲解视频,并且后续会持续更新

如果你需要这些资料,可以添加V获取:vip1024b (备注运维)
img

正文

    for(int i = 0;i < n;i++){//找到先序数据在inorder中的位置
        inorderIndex.put(inorder[i],i);
    }
    TreeNode root = dfs(0,n - 1,0,n - 1,preorder,inorder);
    return root;
}

}


## 2024/2/21


[从中序与后序遍历序列构造二叉树]( )  
 中序:左中右  
 后序:左右中  
 [[左子树]根[右子树]]  
 [[左子树][右子树]根]  
 逆序遍历postorder数组,找到根,利用Map记录根在inorder中的index,完成操作。



class Solution {
Map<Integer,Integer> map = new HashMap<>();
TreeNode dfs(int il,int ir,int pl,int pr,int[] inorder,int[] postorder){
if(pr < pl)return null;
//获取根在中序中的位置
int rootIndex = map.get(postorder[pr]);
//创建根节点
TreeNode root = new TreeNode(postorder[pr]);
//获取左右子树长度
int left_size = rootIndex - il;
int right_size = ir - rootIndex;
//遍历左右子树
root.left = dfs(il,rootIndex - 1,pl,pl + left_size - 1,inorder,postorder);
root.right = dfs(rootIndex + 1,ir,pr - right_size,pr - 1,inorder,postorder);
return root;
}
public TreeNode buildTree(int[] inorder, int[] postorder) {
int n = postorder.length;
for(int i = 0;i < n;i++){
map.put(inorder[i],i);
}
return dfs(0,n - 1,0,n - 1,inorder,postorder);
}
}


## 2024/2/22


[根据前序和后序遍历构造二叉树]( )  
 先序:根左右  
 后序:左右根  
 [根,左子树,右子树]  
 [左子树,右子树,根]  
 存在多个答案,即遍历合理即可。  
 根后的第一个元素,是左子树的根,可以找到对应于后续的index,然后算出左子树的长度。



class Solution {
Map<Integer,Integer> map = new HashMap<>();
TreeNode dfs(int preL,int preR,int postL,int postR,int[] preorder,int[] postorder){
if(preR < preL) return null;
//创建root
TreeNode root = new TreeNode(preorder[preL]);
//计算左子树长度
int leftSize = preL + 1 < preR ? map.get(preorder[preL + 1]) - postL + 1: 0;
//计算右子树长度
int rightSize = preR - preL - leftSize;
//算出左右子树
root.left = dfs(preL + 1,preL + leftSize,postL,postL + leftSize - 1,preorder,postorder);
root.right = dfs(preL + leftSize + 1,preR,postL + leftSize,postR - 1,preorder,postorder);
return root;
}
public TreeNode constructFromPrePost(int[] preorder, int[] postorder) {
int n = preorder.length;
for(int i = 0;i < n;i++){
map.put(postorder[i],i);
}
return dfs(0,n-1,0,n-1,preorder,postorder);
}
}


## 2024/2/23


[二叉树中的第 K 大层和]( )   
 自己的思路:深度遍历+map记录+sort排序。但是这样很浪费时间,不是最优。  
 应该的做法:BFS+双队列记录当前层次的所有节点+排序,节约时间。



//自己做法
class Solution {
Map<Integer,Long> map = new HashMap<Integer,Long>();
public void dfs(int index,TreeNode root){
if(root == null)return;
map.put(index,map.getOrDefault(index,0L) + root.val);
dfs(index + 1,root.left);
dfs(index + 1,root.right);
}
public long kthLargestLevelSum(TreeNode root, int k) {
dfs(1,root);
Set set = map.keySet();
List list = new ArrayList<>();
for(Integer key:set){
list.add(map.get(key));
}
if(k > list.size())return -1L;
Collections.sort(list,Collections.reverseOrder());
return list.get(k - 1);
}
}
//题解
class Solution {
public long kthLargestLevelSum(TreeNode root, int k) {
List a = new ArrayList<>();
List q = List.of(root);
while (!q.isEmpty()) {
long sum = 0;
List tmp = q;
q = new ArrayList<>();
for (TreeNode node : tmp) {
sum += node.val;
if (node.left != null) q.add(node.left);
if (node.right != null) q.add(node.right);
}
a.add(sum);
}
int n = a.size();
if (k > n) {
return -1;
}
Collections.sort(a);
return a.get(n - k);
}
}


## 2024/2/24


[二叉搜索树最近节点查询]( )  
 BFS + 二分。  
 二分需要再学一学,模板有问题。



/*
BFS + 二分
*/
class Solution {
public List<List> closestNodes(TreeNode root, List queries) {
Queue pq = new LinkedList<>();
List querySum = new ArrayList<>();
pq.offer(root);
while(!pq.isEmpty()){
Queue lq = pq;
pq = new LinkedList<>();
for(TreeNode temp:lq){
querySum.add(temp.val);
if(temp.left != null)pq.offer(temp.left);
if(temp.right != null)pq.offer(temp.right);
}
}
Collections.sort(querySum);
List<List> ans = new ArrayList<>();
// return ans;
int n = querySum.size();
int[] a = new int[n];
for(int i = 0;i < n;i++)a[i] = querySum.get(i);
for(Integer i:queries){
int j = lowerBound(a, i);
int mx = j == n ? -1 : a[j];
if (j == n || a[j] != i) { // a[j]>i, a[j-1]<i
j–;
}
int mn = j < 0 ? -1 : a[j];
ans.add(List.of(mn, mx));
}
return ans;
}
private int lowerBound(int[] a, int target) {
int left = -1, right = a.length; // 开区间 (left, right)
while (left + 1 < right) { // 区间不为空
int mid = (left + right) >>> 1; // 比 /2 快
if (a[mid] >= target) {
right = mid; // 范围缩小到 (left, mid)
} else {
left = mid; // 范围缩小到 (mid, right)
}
}
return right;
}
}


## 2024/2/25


[二叉搜索树的最近公共祖先]( )  
 我的做法:遍历每个结点,然后记录路径,最后使用双层循环进行匹配最近节点。  
 题解:p、q一起遍历,对于当前`root`如果是相同的比较结果,证明不是分岔点,如果有不同的比较结果,那么就是分岔点,当前`root`就为最终结果。



//我的做法
class Solution {
List list;
public void dfs(TreeNode root,TreeNode x){
if(root == null){
return;
}
list.add(root);
if(root.val == x.val)return;
if(root.val > x.val)dfs(root.left,x);
else dfs(root.right,x);
}
public TreeNode lowestCommonAncestor(TreeNode root, TreeNode p, TreeNode q) {
list = new ArrayList<>();
dfs(root,p);
List pList = list;
list = new ArrayList<>();
dfs(root,q);
int n = pList.size();
int m = list.size();
for(int i = n - 1;i >=0;i–){
for(int j = m - 1;j >= 0;j–){
if(pList.get(i).val == list.get(j).val){
return pList.get(i);
}
}
}
return null;
}
}
//题解:
class Solution {
public TreeNode lowestCommonAncestor(TreeNode root, TreeNode p, TreeNode q) {
TreeNode ancestor = root;
while (true) {
if (p.val < ancestor.val && q.val < ancestor.val) {
ancestor = ancestor.left;
} else if (p.val > ancestor.val && q.val > ancestor.val) {
ancestor = ancestor.right;
} else {
break;
}
}
return ancestor;
}
}


## 2024/2/26


[二叉搜索树的范围和]( )  
 我的思路:中序遍历+遍历求解。比较浪费时间,其实可以dfs的时候就可以进行求解。  
 题解思路:判断是否遍历左右子树,直接求解。



//我的
class Solution {
List list = new ArrayList<>();
public void dfs(TreeNode root){
if(root == null)return;
dfs(root.left);
list.add(root.val);
dfs(root.right);
}
public int rangeSumBST(TreeNode root, int low, int high) {
//中序遍历,并求值
dfs(root);
int n = list.size();
int ans = 0;
for(int i = 0;i < n;i++){
if(list.get(i) > high)break;
if(list.get(i) >= low)ans+=list.get(i);
}
return ans;
}
}
//题解
class Solution {
public int dfs(TreeNode root,int low,int high){
if(root == null)return 0;
if(root.val > high){
return dfs(root.left,low,high);
}
if(root.val < low){
return dfs(root.right,low,high);
}
return root.val + dfs(root.left,low,high) + dfs(root.right,low,high);
}
public int rangeSumBST(TreeNode root, int low, int high) {
return dfs(root,low,high);
}
}


## 2024/2/27


[统计树中的合法路径项目]( )  
 冥思苦想1.30个小时,没找到正确的思路。被提示误导,以为就是深搜之后根据不同的类型进行动态规划。  
 正确的题解:  
 1.利用埃氏筛计算质数  
 2.枚举所有质数节点,对于当前的质数来说,会将整个树划分为多个连通块。对于每个连通块,记录所有合法的路径(即全是合数的路径,这样可以与当前质数节点形成一条合法的路径)。此时,不同连通块之间的合数路径,可以通过当前的质数节点进行相连,两者之间进行选择,此时为最终的结果。  
 ![在这里插入图片描述](https://img-blog.csdnimg.cn/direct/6beb8d5d72de40c1b5986958d56c0124.png)



class Solution {
private final static int MX = (int)1e5;
private final static boolean[] prime = new boolean[MX + 5];//质数为false,非质数为true
static{
prime[1] = true;
for(int i = 2;i * i <= MX;i++){
if(prime[i])continue;
for(int j = i * i;j <= MX;j+=i){
prime[j] = true;
}
}
}
public long countPaths(int n, int[][] edges) {
//将树建为图
List[] g = new ArrayList[n + 1];
Arrays.setAll(g,e -> new ArrayList<>());//建立邻接表
for(int i = 0;i < n - 1;i++){
int x = edges[i][0];
int y = edges[i][1];
g[x].add(y);
g[y].add(x);
}
long ans = 0;
int[] size = new int[n + 1];//一个trick,用于记录是否遍历过,以及记录
List nodes = new ArrayList<>();//用于记录当前连通块数量
for(int x = 1;x <= n;x++){
if(prime[x])continue;//跳过质数
int sums = 0;
for(int y:g[x]){//当前质数x,将整棵树划分为了多个子连通块
if(!prime[y])continue;//如果当前为质数,则跳过
if(size[y] == 0){//当前节点的连通合数并未计算
nodes.clear();//用于统计从当前点出发能遍历到的所有合数
dfs(y,-1,g,nodes);//遍历y所在的连通块,在不经过质数的情况,能有多少非质数。
for(int z:nodes){// 连通块中的节点能到达的合数数量是一致的,记录
size[z] = nodes.size();
}
}
//这size[y]个质数与之前遍历的sum个非质数,两两之间的路径只包含当前x
ans += (long) size[y] * sums;
sums += size[y];
}
ans += sums;
}
return ans;
}
private void dfs(int x,int fa,List[] g,List nodes){
//fa保证不往回走
nodes.add(x);
for(int y:g[x]){
if(y != fa && prime[y]){
dfs(y,x,g,nodes);
}
}
}
}


## 2024/2/28


[使二叉树所有路径值相等的最小代价]( )  
 我的思路:1.只能增加,不能减少,因此只能向最大值进行靠近。2.若同属于一个根节点的两个子节点都需要更新,那么就更新两者最小值到父节点,递归进行。  
 题解思路:对于两个有相同根的叶子节点来说,除了自己以外其余所有路径都相同,因此只需要将小的往大的靠就可以实现路径相等。对于不是叶子的兄弟节点,从根到当前节点的路径,除了这两个兄弟节点不一样,其余节点都一样,所以把路径和从叶子往上传,这样就可以按照叶子节点的方式进行比较和更新了。



//我的做法:
class Solution {
int[] addCost;
public void dfs(int index,int[] pathCost,int maxs,int n){
if(index > n)return;
dfs(index << 1,pathCost,maxs,n);
dfs(index << 1 | 1,pathCost,maxs,n);
if(index * 2 > n){//子节点
if(pathCost[index] < maxs){
addCost[index] = maxs - pathCost[index];
}
return;
}
//非子节点,判断子节点更新的次数
int mins = Math.min(addCost[index * 2],addCost[index * 2 + 1]);
addCost[index] += mins;
addCost[index * 2] -=mins;
addCost[index * 2 + 1] -= mins;
}
public int minIncrements(int n, int[] cost) {
/*
1.只能增加,不能减少,因此只能向最大值进行靠近
2.若同属于一个根节点的两个子节点都需要更新,那么就更新两者最小值到父节点,递归进行。
*/
int[] pathCost = new int[n + 1];
pathCost[0] = 0;
int maxs = -1;//最大权值
for(int i = 0;i < n;i++){
int index = i + 1;
pathCost[index] = pathCost[index / 2] + cost[i];
maxs = Math.max(maxs,pathCost[index]);
}
addCost = new int[n + 1];
Arrays.fill(addCost,0);
dfs(1,pathCost,maxs,n);
int ans = 0;
for(int i = 1;i <= n;i++)ans+=addCost[i];
return ans;
}
}
//题解做法:
class Solution {
public int minIncrements(int n, int[] cost) {
int ans = 0;
for (int i = n / 2; i > 0; i–) { // 从最后一个非叶节点开始算
ans += Math.abs(cost[i * 2 - 1] - cost[i * 2]); // 两个子节点变成一样的
cost[i - 1] += Math.max(cost[i * 2 - 1], cost[i * 2]); // 累加路径和
}
return ans;
}
}


## 2024/2/29


[统计可能出现的树根数目]( )  
 未做出来的题:虽然有想到根与子树之间的换根,只需要记录交换根本身与子节点之间的序列变化,即可完成该题,但对于如何记录以及求得以root为根的树正确的猜测,还是有点无从下手。  
 该题是一个换根DP问题,前置知识为:[树中距离之和]( )  
 ![在这里插入图片描述](https://img-blog.csdnimg.cn/direct/ca86772ba82d4de6ac3fbe70a86049bd.png)  
 子树大小计算:后序遍历并统计。  
 保证每个节点只递归访问1次:对于图来说,使用`vis`数组记录每个点的访问次数。但是对于树来说,一直向下递归,就不会遇到之前访问的点,所以不需要数组,只需要避免重复访问父节点即可。  
 树中距离之和:



class Solution {
private List[] g;
private int[] ans,size;
public int[] sumOfDistancesInTree(int n, int[][] edges) {
g = new ArrayList[n];
Arrays.setAll(g,e -> new ArrayList<>());
for(int[] e:edges){
g[e[0]].add(e[1]);
g[e[1]].add(e[0]);
}
ans = new int[n];
size = new int[n];
Arrays.fill(ans,0);
dfs(0,-1,0);
reboot(0,-1);
return ans;
}
private void dfs(int index,int fa,int depth){
ans[0] += depth;
size[index] = 1;
for(int y:g[index]){
if(y != fa){
dfs(y,index,depth + 1);
size[index] += size[y];
}
}
}
private void reboot(int x,int fa){
for(int y:g[x]){
if(y!=fa){
ans[y] = ans[x] + g.length - 2 * size[y];//ans[y] = ans[x] - size[y] + (n - size[y])
reboot(y,x);//以y为根,x为父节点
}
}
}
}


对于本题来说:  
 如果节点x和y相邻,那么[以x为根的树]变为[以y为根的树],就只有x和y的父子关系改变了,其余相邻节点之间的父子关系没有改变。所以只有[x,y]和[y,x]这两个猜测的正确性变化了,其余猜测的正确性不变。  
 因此,在计算出以0为根的`cnt`之后,可以再次从0出发,DFS这颗树。从节点`x`递归到节点`y`时:  
 若有猜测[x,y],那么猜对的次数-1。  
 若有猜测[y,x],那么猜对的次数+1。  
 DFS的同时,统计猜对次数>=k的节点个数,即为答案。  
 这里我所担心的:可能出现的可行解但是次序相反的问题不会出现,所以建树即可。  
 统计可能出现的树根数目:



class Solution {
private List[] g;
private Set set;
int k,ans,cnt0;
public int rootCount(int[][] edges, int[][] guesses, int k) {
this.k = k;
//建树
g = new ArrayList[edges.length + 1];
Arrays.setAll(g,e -> new ArrayList<>());
for(int[] e:edges){
g[e[0]].add(e[1]);
g[e[1]].add(e[0]);
}
//将guesses映射到set中,将2个4字节映射为1个8字节。
set = new HashSet<>();
for(int[] guess:guesses){
set.add((long)guess[0] << 32 | guess[1]);
}
cnt0 = 0;
dfs(0,-1);
ans = 0;
reroot(0,-1,cnt0);
return ans;
}
private void dfs(int x,int fa){
for(int y:g[x]){
if(y != fa){
if(set.contains((long)x << 32 | y))cnt0++;
dfs(y,x);
}
}
}
private void reroot(int x,int fa,int cnt){
if(cnt >= k)ans++;
for(int y:g[x]){
if(y != fa){
int c = cnt;
if(set.contains((long)x << 32 | y))c–;
if(set.contains((long)y << 32 | x))c++;
reroot(y,x,c);
}
}
}
}


## 2024/3/1


[检查数组是否有效划分]( )  
 dp问题,一开始自己是把模型都建立出来了,但是却错误纠结覆盖子数组中是否有被其它数组挪用的情况。对于f[i]来说,只要前2个或者3个是能划分的,那么就能判定当前的子数组是否能划分。  
 也就是说,这里我思考的:  
 dp[i]前i个数字是否能有效划分  
 对于i来说,若能划分,有这些情况:  
 1.dp[i - 1]能划分,但dp[i - 2]不能划分,且nums[i - 1] = nums[i - 2] = nums[i]  
 2.dp[i - 1]不能划分,但dp[i - 2]能划分,且nums[i - 1] = nums[i]  
 3.dp[i - 1]不能划分,且dp[i - 2]也不能划分,但dp[i - 3]能划分,且有nums[i] = nums[i - 1] + 1 = nums[i - 2] + 2  
 只需要考虑能划分,不需要思考中间的数据不被划分的情况。  
 此时可以进行递推,得到最终的结果。



class Solution {
public boolean validPartition(int[] nums) {
/*
dp[i]前i个数字是否能有效划分
对于i来说,若能划分,有这些情况:
1.dp[i - 1]能划分,但dp[i - 2]不能划分,且nums[i - 1] = nums[i - 2] = nums[i]
2.dp[i - 1]不能划分,但dp[i - 2]能划分,且nums[i - 1] = nums[i]
3.dp[i - 1]不能划分,且dp[i - 2]也不能划分,但dp[i - 3]能划分,且有nums[i] = nums[i - 1] + 1 = nums[i - 2] + 2
会不会出现这种情况:
4,4,4,4
*/
int n = nums.length;
boolean[] f = new boolean[n + 1];
f[0] = true;
for(int i = 1;i < n;i++){
if (f[i - 1] && (nums[i] == nums[i - 1])||
i > 1 && f[i - 2] && ((nums[i] == nums[i - 1] && nums[i - 1] == nums[i - 2]) ||
nums[i] == nums[i - 1] + 1 && nums[i] == nums[i - 2] + 2))
f[i + 1] = true;
}
return f[n];
}
}


## 2024/3/2


[受限条件下可到达节点的数量]( )  
 `DFS`遍历,唯一注意点的就是不要遍历到父节点。



class Solution {
int cnt = 0;

public int reachableNodes(int n, int[][] edges, int[] restricted) {
    boolean[] isrestricted = new boolean[n];
    for (int x : restricted) {
        isrestricted[x] = true;
    }

    List<Integer>[] g = new List[n];
    for (int i = 0; i < n; i++) {
        g[i] = new ArrayList<Integer>();
    }
    for (int[] v : edges) {
        g[v[0]].add(v[1]);
        g[v[1]].add(v[0]);
    }
    dfs(0, -1, isrestricted, g);
    return cnt;
}

public void dfs(int x, int f, boolean[] isrestricted, List<Integer>[] g) {
    cnt++;
    for (int y : g[x]) {
        if (y != f && !isrestricted[y]) {
            dfs(y, x, isrestricted, g);
        }
    }
}

}


## 2024/3/3


[用队列实现栈]( )  
 简单题,运用Deque即可。  
 `offerLast,peekLast,pollLast,offerFirst,peekFirst,pollFirst,new LinkedList<>()`



class MyStack {
private Deque deque;
public MyStack() {
this.deque = new LinkedList<>();
}

public void push(int x) {
    deque.offerLast(x);
}

public int pop() {
    return deque.pollLast();
}

public int top() {
    return deque.peekLast();
}

public boolean empty() {
    return deque.isEmpty();
}

}


[删除有序数组中的重复项]( )  
 面试150题中的中等题。  
 我的思路就是纯模拟,时间复杂度略高,代码量也大一点。对于双指针运用有点不熟练,需要多练习。  
 题解思路:利用`slow`和`fast`双指针并行操作,slow用于更新,fast用于遍历。由于数组以及排好序了,所以对当前的fast来说,只要当前值与slow - 2的值不重复,就代表满足条件,否则slow就更新。



//我的代码
class Solution {
public int removeDuplicates(int[] nums) {
//找需要替换的子数组开始和结束坐标
int i = 0;
int pre = -1;
int ans = nums.length;
int cnt = 0;
int from,to;
while(true){
if(i >= ans)return ans;
if(nums[i] != pre){
cnt = 1;
pre = nums[i];
i++;
continue;
}
cnt++;
if(cnt > 2){
from = i;
while(i + 1 < ans && nums[++i] == pre){
cnt++;
}
int j = i;
int k = from;
while(j < ans){
nums[k++] = nums[j++];
}
ans -= (cnt - 2);
i = from;
}
else i++;
}
}
}
//题解:
class Solution {
public int removeDuplicates(int[] nums) {
int n = nums.length;
if(n <= 2)return n;//对于数组长度为2的,直接返回
int fast = 2,slow = 2;
while(fast < n){
if(nums[slow - 2] != nums[fast]){
nums[slow++] = nums[fast];
}
fast++;
}
return slow;
}
}


## 2024/3/4


[栈实现队列]( )  
 Deque操作即可。



class MyQueue {
Deque deque;
public MyQueue() {
this.deque = new LinkedList();
}

public void push(int x) {
    deque.addLast(x);
}

public int pop() {
    return deque.pollFirst();
}

public int peek() {
    return deque.peekFirst();
}

public boolean empty() {
    return deque.isEmpty();
}

}


[轮转数组]( )  
 第一种方法:使用额外数组存储。  
 第二种方法(题解):首先,将数组进行整体反转,此时后k % n位到前面来了。然后将前k位进行反转,得到正确的k % n位顺序,最后将后 n - (k % n)位进行反转,得到正确的前置顺序。



class Solution {
public void rotate(int[] nums, int k) {
// for(int i = 0;i < k;i++)rotateOneStep(nums);//这样O(nk)会超时
//后k个数可以视为一个子数组,顺序是不变的,依次放入即可,空间复杂度为O(k)

    int[] kNums = new int[k];
    int n = nums.length;
    k = k % n;
    if(n == 1)return;
    for(int j = 0;j < k;j++){
        kNums[j] = nums[j + n - k];
    }
    for(int i = n - k - 1;i >= 0;i--){
        nums[i + k] = nums[i];
    }
    for(int i = 0;i < k;i++){
        nums[i] = kNums[i];
    }
}
public void rotateOneStep(int[] nums){
    int n = nums.length;
    int last = nums[n - 1];
    for(int i = n - 1;i > 0;i--){
        nums[i] = nums[i - 1];
    }
    nums[0] = last;
}

}
//题解:
class Solution {
public void rotate(int[] nums, int k) {
/*
首先,将数组进行整体反转,此时后k % n位到前面来了
然后将前k位进行反转,得到正确的k % n位顺序
最后将后 n - (k % n)位进行反转,得到正确的前置顺序
*/
int n = nums.length;
k %= n;
reverse(nums,0,n - 1);
reverse(nums,0,k - 1);
reverse(nums,k,n - 1);
}
public void reverse(int []nums,int s,int e){
int temp;
while(s < e){
temp = nums[s];
nums[s] = nums[e];
nums[e] = temp;
s++;
e–;
}
}
}


[买卖股票的最佳时机I]( )  
 从前往后,记录直到现在的最小购入价格,同时计算最大利润。



class Solution {
public int maxProfit(int[] prices) {
int mins = 10005;
int ans = 0;
for(int i = 0;i < prices.length;i++){
ans = Math.max(ans,prices[i] - mins);
mins = Math.min(mins,prices[i]);
}
return ans;
}
}


[买卖股票的最佳时机II]( )  
 我的思路:逆序递推,dp[i]从当天开始进行操作能获取到的最大利润。若当天选择不买入,则dp[i] = dp[i + 1],若当天选择买入,则枚举后续值是否有操作比当前还大。时间复杂度为(O(n2)),案例较弱,让我混过去了。  
 题解dp:dp[i][0]表示第i天交易完后手里没有股票的最大利润,dp[i][1]代表第i天交易完后手里持有一只股票的最大利润。  
 dp[i][0] = max(dp[i-1][0],dp[i-1][1] + prices[i])  
 dp[i][1] = max(dp[i - 1][1],dp[i - 1][0] - prices[i])  
 dp[0][0] = 0  
 dp[0][1] = -prices[0]  
 最后答案为dp[n-1][0]  
 题解贪心:题目中给定的描述是,可以当天买入,当天卖出。所以即时第三天直接卖出的利润比第四天卖出的利润要小,但是由于这个设置,我们可以在第三天卖出之后再买入,再在第四天卖出,实际的利润也等于第四天直接卖出。所以只需要统计所有上升的窗口为2的数组即可。



//我的dp
class Solution {
public int maxProfit(int[] prices) {
int n = prices.length;
int[] dp = new int[prices.length + 1];
dp[n - 1] = 0;
dp[n] = 0;
int ans = 0;
for(int i = n - 2;i >= 0;i–){
dp[i] = dp[i + 1];//当前不买
for(int j = i + 1;j < n;j++){
dp[i] = Math.max(dp[i],prices[j] - prices[i] + dp[j + 1]);
}
ans = Math.max(ans,dp[i]);
}
return ans;
}
}
//题解dp
class Solution {
public int maxProfit(int[] prices) {
int n = prices.length;
int[][] dp = new int[n][2];
dp[0][0] = 0;
dp[0][1] = -prices[0];
for (int i = 1; i < n; ++i) {
dp[i][0] = Math.max(dp[i - 1][0], dp[i - 1][1] + prices[i]);
dp[i][1] = Math.max(dp[i - 1][1], dp[i - 1][0] - prices[i]);
}
return dp[n - 1][0];
}
}
//题解贪心
class Solution {
public int maxProfit(int[] prices) {
int n = prices.length;
int dp0 = 0, dp1 = -prices[0];
for (int i = 1; i < n; ++i) {
int newDp0 = Math.max(dp0, dp1 + prices[i]);
int newDp1 = Math.max(dp1, dp0 - prices[i]);
dp0 = newDp0;
dp1 = newDp1;
}
return dp0;
}
}


## 2024/3/5


[到达目的地的方案数]( )  
 在用dij求最短路径时,利用dp记录方案数量。  
 dp[i] = 从起点0到达i的最少时间方案数量。  
 dp[0] = 1  
 如果dis[i] > dis[index] + g[index][i],代表最短路被更新,那么dp[i] = dp[index]  
 如果dis[i] == dis[index] + g[index][i],那么代表有另外的最短路方案,dp[i] += dp[index]  
 需要注意的点是,time < 1e9,而Integer.MAX\_VALUE ≈ 1e9,若用其做最大值标注,可能出现溢出的情况,保险起见,还是用Long来记录图和dis。  
 由于是稠密图,所以无需使用堆进行优化。



class Solution {
private int mod = (int)1e9 + 7;
/*
dp[i]:从起点0出发到达i的最少时间的方案数量。
dp[i][j]:从i到j的最少时间方案数量
dis[i]:从起点0出发到达i的最少时间
vis[i]:i是否已经被访问了
*/
long[][] g;
long[] dis;
int[] dp;
boolean[] vis;
public int countPaths(int n, int[][] roads) {
g = new long[n][n];
dis = new long[n];
dp = new int[n];
vis = new boolean[n];
for(int i = 0;i < n;i++)Arrays.fill(g[i],Long.MAX_VALUE / 2);
for(int i = 0;i < roads.length;i++){
g[roads[i][0]][roads[i][1]] = roads[i][2];
g[roads[i][1]][roads[i][0]] = roads[i][2];
}
Arrays.fill(dis,Long.MAX_VALUE / 2);
Arrays.fill(dp,0);
Arrays.fill(vis,false);
dij(n);
return dp[n - 1] % mod;
}
public void dij(int n){
dis[0] = 0;
vis[0] = true;
dp[0] = 1;
//初始化
for(int i = 1;i < n;i++){
dis[i] = g[0][i];
if(dis[i] != Long.MAX_VALUE / 2)dp[i] = 1;
}
for(int i = 0;i < n;i++){
//找n - 1条路径
long mins = Long.MAX_VALUE / 2;
int index = 0;
for(int k = 0;k < n;k++){
if(!vis[k] && mins > dis[k]){
mins = dis[k];
index = k;
}
}
//更新vis
vis[index] = true;
//遍历剩余节点,更新dis和dp
for(int k = 0;k < n;k++){
if(!vis[k] && dis[k] > dis[index] + g[index][k]){
dis[k] = dis[index] + g[index][k];
dp[k] = dp[index];//最短距离更新了,那么对应的方案也需要更新。
}
else if(!vis[k] && dis[k] == dis[index] + g[index][k]){
dp[k] += dp[index];
dp[k] %= mod;
}
}
}
}
}


[跳跃游戏]( )  
 贪心:策略是花更少的步数走的更远。  
 因此对于当前所在的位置i来说,停留的位置为max j + nums[i + j],其中0 < j <= nums[i].



class Solution {
public int jump(int[] nums) {
/*
[2,1,1,1,4]
贪心策略,目的是花更少的步数走的更远
因此,走到范围内和加起来最多的即可。
*/
int n = nums.length;
int ans = 0;
int idx = 0;//当前所在的坐标
while(true){
if(idx >= n - 1)break;
int maxs = 0;
int index = -1;
for(int i = 1;i <= nums[idx];i++){
if(idx + i >= n - 1)return ans + 1;
if(i + nums[idx + i] > maxs){
maxs = i + nums[idx + i];
index = idx + i;
}
}
if(index != -1){
ans++;//走到下一步。
idx = index;
}
}
return ans;
}
}


[H指数]( )  
 二分搜索,在[0,n]中搜索K是否满足要求。



class Solution {
public int hIndex(int[] citations) {
int n = citations.length;
/*
[0,n]搜索
check一下
*/
int left = 0,right = n;
Arrays.sort(citations);
while(left <= right){
int mid = (left + right) >> 1;
if(check(mid,n,citations)){
left = mid + 1;
}else{
right = mid - 1;
}
}
return left - 1;
}
boolean check(int h,int n,int[] citations){
int i = n - 1;//检查是否>h
while(i >= 0 && citations[i] >= h)i–;
return (n - i - 1) >= h;
}
}


## 2024/3/6


[找出数组中的K-or值]( )  
 用的map记录,但可以通过枚举每一个32个bit位判断,自己的方法有点浪费时间。



class Solution {
public int findKOr(int[] nums, int k) {
Map<Integer,Integer> map = new HashMap<>();
for(int i = 0;i < nums.length;i++){
int x = nums[i];
int cnt = 0;
while(x!=0){
if((x & 1) == 1){
map.put(cnt,map.getOrDefault(cnt,0) + 1);
}
cnt++;
x >>= 1;
}
}
Set set = map.keySet();
int ans = 0;
for(Integer i:set){
if(map.get(i) >= k){
ans += (1 << i);
}
}
return ans;
}
}
//题解:
class Solution {
public int findKOr(int[] nums, int k) {
int ans = 0;
for (int i = 0; i < 31; ++i) {
int cnt = 0;
for (int num : nums) {
if (((num >> i) & 1) != 0) {
++cnt;
}
}
if (cnt >= k) {
ans |= 1 << i;
}
}
return ans;
}
}


[O(1)时间插入、删除和获取随机元素值]( )  
 我只用了一个set进行操作,但是这样在随机获取元素值时,就只能遍历,最坏情况为O(n)。  
 可以利用list同步记录操作,由于数字的顺序无关,可以将最后一个值放入需要remove的地方,然后移除掉最后一个值。



class RandomizedSet {
Set set;
public RandomizedSet() {
set = new HashSet();
}

public boolean insert(int val) {
    if(set.contains(val) == true)return false;
    set.add(val);
    return true;
}

public boolean remove(int val) {
    if(set.contains(val) == false) return false;
    set.remove(val);
    return true;
}

public int getRandom() {
    int idx = new Random().nextInt(set.size());
    int cnt = 0;
    for(Integer i:set){
        if(idx == cnt)return i;
        cnt++;
    }
    return 0;
}

}
//题解:
class RandomizedSet {
Random random;
HashMap<Integer,Integer> map;
ArrayList list;

public RandomizedSet() {
    map = new HashMap();
    list = new ArrayList();
    random = new Random();
}

public boolean insert(int val) {
    if(map.containsKey(val)){
        return false;
    }

    int index = list.size();
    list.add(val);
    map.put(val,index);
    return true;
}

public boolean remove(int val) {
    if(!map.containsKey(val)){
        return false;
    }
    int index = map.get(val);
    int last = list.get(list.size()-1);
    list.set(index,last);
    map.put(last,index);
    map.remove(val);
    list.remove(list.size()-1);
    return true;
}

public int getRandom() {
    return list.get(random.nextInt(list.size()));
}

}


[除自身以外的乘积]( )  
 对前缀思想的利用。只记住前缀和是不行的。  
 除自身以外的乘积,是由左部分的乘积 \* 右部分的乘积。  
 因此利用前缀的思想,将左部分的乘积和右部分的乘积单独进行记录。  
 最终答案 = L[i] \* R[i]



class Solution {
public int[] productExceptSelf(int[] nums) {
int n = nums.length;
int[] L = new int[n];
int[] R = new int[n];
L[0] = 1;
for(int i = 1;i<n;i++){//左边第一个元素没有左乘积
L[i] = nums[i - 1] * L[i - 1];
}
R[n - 1] = 1;
for(int i = n - 2;i >=0 ;i–){//右边第一个元素没有右乘积
R[i] = nums[i + 1] * R[i + 1];
}
for(int i = 0;i < n;i++){
nums[i] = L[i] * R[i];
}
return nums;
}
}


## 2024/3/7


[2575. 找出字符串的可整除数组]( )  
 看了提示,才想到可以由上一步的余数推导出当前的余数。  
 假设上一步的数为x,倍数为y,余数为z,则有  
 x = y \* m + z  
 假设当前步增加h  
 x \* 10 + h = 10 \* (y \* m + z) + h = 10 \* y \* m + 10 \* z + h  
 余数应为:(10 \* z + h) % m  
 需要注意的点:即时是上一步遗留的余数\*10+h,有可能会爆int,因为m给的范围很大。



class Solution {
public int[] divisibilityArray(String word, int m) {
/*
卡大数据long也没法过。
z = x % m;
x = m * y + z;
x * 10 + h = (m * y + z) * 10 + h = 10 * y * m + 10 * z + h
x * 10 + h = 10y * m + 10 * z + h;
10 * z + h 就为x * 10 + h的余数
*/
int n = word.length();
int[] div = new int[n];
int[] dp = new int[n];
long preMod = 0;
Arrays.fill(div,0);
for(int i = 0;i < n;i++){
int c = word.charAt(i) - ‘0’;
preMod = (preMod * 10 + c) % m;
if(preMod == 0){
div[i] = 1;
}
}
return div;
}
}


[134. 加油站]( )  
 看了题解,一目了然。  
 本质上是利用一个可以传递的性质,进行一次遍历。  
 对于从x出发,最多能到达的加油站y来说,对于所有在其中间的加油站j(x < j < y),其最远距离都只能到达y。因为从x到j,其油量必须>=0,对于=0来说,j能到达y,对于>0来说,可能连y都达不到。  
 因此,可以利用一次遍历,对于当前起点i来说,若走到y之后走不下去了,就换到起点y + 1进行遍历。



class Solution {
public int canCompleteCircuit(int[] gas, int[] cost) {
int n = gas.length;
int i = 0;//记录起点
while(i < n){
int cnt = 0;//记录当前的遍历次数
int gasSum = 0;//剩余油量
int idx = i;
while(cnt < n){
gasSum += gas[idx];
gasSum -= cost[idx];
if(gasSum < 0){
break;
}
idx = (idx + 1) % n;
cnt++;
}
if(cnt == n){
return i;
}
i = i + cnt + 1;
}
return -1;
}
}


## 2024/3/8


[2834. 找出美丽数组的最小和]( )  
 贪心策略,若想放入值最小,那么就应该从1开始放入,但是又要避免不美丽的情况,对于1来说,target - 1就不能放入了,以此类推。  
 因此,假设[1,target)中填充m个数组,那么m = target / 2个,其中的数据分别为[1,2,…,m],对于>=target的数据来说,则挨着存即可,[target,target + n - m],等差数列求和。  
 需要注意的点是,存在有n远远小于target的情况,当满足条件target > 2 \* n + 1,代表及时[1,n]全部填充,也不会到达target。



class Solution {
public int mod = (int)1e9 + 7;
public int minimumPossibleSum(int n, int target) {
/*
填充规则:从1开始,如果当前值<target,那么,只填充i,不填充target - i。直到 >= target为止。
分为两部分[1,target).length = m,[target,target + n - m]
*/
long sums = 0;
//1 + … + target/2
if(target > 2 * n + 1){
sums = n * (n + 1) / 2 % mod;
return (int)sums % mod;
}
long m = target / 2;
sums += (m * (m + 1) / 2) % mod;
//target + … + target + n - m
long k = n - m;
sums += (k * (target + target + k - 1) / 2) % mod;
return (int) sums % mod;
}
}


[42. 接雨水]( )  
 我的思路:利用单调栈进行求解,从大到小记录高度,若当前高度<=栈顶高度,则加入。  
 若当前高度>栈顶高度,则开始循环判断,若前面的数据中存在落差(即height[j] > pre,pre = height[j]),则计算当前能存储的雨量,并将该落差填平。  
 因此当当前高度>栈顶高度,开始判断时,有以下情况:


1. 存在落差(栈不为空,且有前面的数据 > 后面的数据的情况)  
 此时需要计算填充的数量。利用idx记录左右两边的坐标,以最小的高度 \* 长度,算出最大能填充的数量,并减去pre高度 \* 长度(易见,在这种情况下,中间全是pre高度,否则早就形成了落差并填平)。  
 算出后,将原有左端点继续放入栈中,代表填平。  
 仍存在两种情况,一种当前高度<=左边高度,那么以当前高度为右的区域就不能再形成落差,那么就可以结束当前循环。  
 若当前高度>左边的高度,那么可能还存在左边还有更高的高度能和当前形成落差,因此循环继续。
2. 不存在落差,结束当次循环  
 最后,将当前高度加入到栈顶。



class Solution {
public int trap(int[] height) {
/*
单调栈问题:
从大到小记录高度,若当前高度<=栈顶高度,则加入。
若当前高度>栈顶高度,则开始出栈,由于是需要形成一个水坝,所以需要找到落差。
出栈终止条件为,栈顶>pre,出栈时记录出栈元素,计算出后,把当前计算的值填充并加入栈,再把当前元素加入到栈。
*/
int n = height.length;
class Node{
int val;
int idx;
Node(int x,int y){
this.val = x;
this.idx = y;
}
}
Deque dq = new LinkedList<>();
int sums = 0;
for(int i = 0;i < n;i++){
if(dq.isEmpty() || height[i] <= dq.peekLast().val){
dq.addLast(new Node(height[i],i));
}
else{
//出栈,并判断是否有落差,记录cnt
while(true){
int cnt = 0;
int pre = height[i];
while(!dq.isEmpty()&&pre >= dq.peekLast().val){
pre = dq.pollLast().val;
cnt++;
}
//若不存在落差
if(dq.isEmpty() || cnt == 0){
break;
}
//存在落差,计算值
Node temp = dq.pollLast();
int mins = Math.min(temp.val,height[i]);
//计算距离
int dis = i - temp.idx - 1;
sums += mins * dis - pre * dis;//计算最终的雨量
dq.addLast(temp);
if(height[i] <= temp.val){
break;
}
}
dq.addLast(new Node(height[i],i));
}
}
return sums;
}
}


## 2024/3/9


[2386. 找出数组的第 K 大和]( )  
 我的思路:有考虑到利用绝对值+优先队列的方式求解。但是并没有将问题转化为找第k小子序列问题。  
 题解思路:  
 对于最大值,可以通过计算数组中的所有正数进行计算。对于第k大和来说,可以在最大值的基础上减去一个正数或者加上负数,其实本质上都是减去其绝对值。此时可以将问题变为nums[i]取绝对值之后的第k小子序列,最后的第K大和答案就是最大值-第k小子序列。  
 如何构造第k小子序列?最小的子序列一定是空集,在空集的基础上,可以通过替换一个值或者增加一个值进行构建子序列。比如序列[1,2,3]的最小子序列:  
 1.[]  
 2.[1] -> 空集基础上增加1  
 3.[2] -> 步骤2的基础上将1换为2  
 4.[1,2] -> 步骤2的基础上增加2  
 5.[3] -> 步骤3的基础上将2换为3  
 6.[1,3] -> 步骤4的基础上将2换为3  
 7.[2,3] -> 步骤3的基础上增加3  
 8.[1,2,3] -> 步骤4的基础上增加3  
 利用堆记录最小子序列之和,即可求解。



class Solution {
public long kSum(int[] nums, int k) {
/*
题解:将问题转换为nums[i]取绝对值后找第k小子序列。
对于非负子序列来说,最小值为空集,0;
对于样例[2,4,-2]来说,转换为绝对值后,[2,4,2] -> [2,2,4];
(1) => []
(2) => [2]
(3) => [2]
(4) => [4]
(5) => [2,2]
(6) => [2,4]

最后的话

最近很多小伙伴找我要Linux学习资料,于是我翻箱倒柜,整理了一些优质资源,涵盖视频、电子书、PPT等共享给大家!

资料预览

给大家整理的视频资料:

给大家整理的电子书资料:

如果本文对你有帮助,欢迎点赞、收藏、转发给朋友,让我有持续创作的动力!

网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。

需要这份系统化的资料的朋友,可以添加V获取:vip1024b (备注运维)
img

一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!
i] <= temp.val){
break;
}
}
dq.addLast(new Node(height[i],i));
}
}
return sums;
}
}


## 2024/3/9


[2386. 找出数组的第 K 大和]( )  
 我的思路:有考虑到利用绝对值+优先队列的方式求解。但是并没有将问题转化为找第k小子序列问题。  
 题解思路:  
 对于最大值,可以通过计算数组中的所有正数进行计算。对于第k大和来说,可以在最大值的基础上减去一个正数或者加上负数,其实本质上都是减去其绝对值。此时可以将问题变为nums[i]取绝对值之后的第k小子序列,最后的第K大和答案就是最大值-第k小子序列。  
 如何构造第k小子序列?最小的子序列一定是空集,在空集的基础上,可以通过替换一个值或者增加一个值进行构建子序列。比如序列[1,2,3]的最小子序列:  
 1.[]  
 2.[1] -> 空集基础上增加1  
 3.[2] -> 步骤2的基础上将1换为2  
 4.[1,2] -> 步骤2的基础上增加2  
 5.[3] -> 步骤3的基础上将2换为3  
 6.[1,3] -> 步骤4的基础上将2换为3  
 7.[2,3] -> 步骤3的基础上增加3  
 8.[1,2,3] -> 步骤4的基础上增加3  
 利用堆记录最小子序列之和,即可求解。



class Solution {
public long kSum(int[] nums, int k) {
/*
题解:将问题转换为nums[i]取绝对值后找第k小子序列。
对于非负子序列来说,最小值为空集,0;
对于样例[2,4,-2]来说,转换为绝对值后,[2,4,2] -> [2,2,4];
(1) => []
(2) => [2]
(3) => [2]
(4) => [4]
(5) => [2,2]
(6) => [2,4]

最后的话

最近很多小伙伴找我要Linux学习资料,于是我翻箱倒柜,整理了一些优质资源,涵盖视频、电子书、PPT等共享给大家!

资料预览

给大家整理的视频资料:

[外链图片转存中…(img-rJQHI86W-1713143872171)]

给大家整理的电子书资料:

[外链图片转存中…(img-b18kURmZ-1713143872171)]

如果本文对你有帮助,欢迎点赞、收藏、转发给朋友,让我有持续创作的动力!

网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。

需要这份系统化的资料的朋友,可以添加V获取:vip1024b (备注运维)
[外链图片转存中…(img-7zm8cuLU-1713143872172)]

一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!

  • 15
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值