先自我介绍一下,小编浙江大学毕业,去过华为、字节跳动等大厂,目前阿里P7
深知大多数程序员,想要提升技能,往往是自己摸索成长,但自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!
因此收集整理了一份《2024年最新Linux运维全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友。
既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,涵盖了95%以上运维知识点,真正体系化!
由于文件比较多,这里只是将部分目录截图出来,全套包含大厂面经、学习笔记、源码讲义、实战项目、大纲路线、讲解视频,并且后续会持续更新
如果你需要这些资料,可以添加V获取:vip1024b (备注运维)
正文
2024/2/20
从前序与中序遍历序列构造二叉树
主要逻辑:
先序:根左右
中序:左根右
根据先序确定根,然后在中序中找到根,划分子树。根之前为左子树,根之后为右子树。
之后继续划分,直到无法细分为止。
我的做法:按照中序进行划分区间,找区间内谁第一个在先序中出现。
题解(更好的做法):先前序,后中序。这样可以一次找出结果,而不需要遍历。
//我的做法
class Solution {
Map<Integer,Integer> preorderIndex;
public TreeNode dfs(int l,int r,int[] preorder,int[] inorder){
if(r - l == 0){
return new TreeNode(inorder[l],null,null);
}
int minIndex = -1;
int mins = 3005;
for(int i = l;i <= r;i++){
if(preorderIndex.get(inorder[i]) < mins){
mins = preorderIndex.get(inorder[i]);
minIndex = i;
}
}
//此时已经找到根
TreeNode root = new TreeNode(inorder[minIndex],null,null);
if(minIndex != l){
root.left = dfs(l,minIndex - 1,preorder,inorder);
}
if(minIndex != r){
root.right = dfs(minIndex + 1,r,preorder,inorder);
}
return root;
}
public TreeNode buildTree(int[] preorder, int[] inorder) {
preorderIndex = new HashMap<>();
int n = preorder.length;
for(int i = 0;i < n;i++){
preorderIndex.put(preorder[i],i);
}
TreeNode root = dfs(0,n - 1,preorder,inorder);
return root;
}
}
//题解的做法
class Solution {
Map<Integer,Integer> inorderIndex;
public TreeNode dfs(int pl,int pr,int il,int ir,int[] preorder,int[] inorder){
if(pl > pr)return null;
int rootIndex = inorderIndex.get(preorder[pl]);//找到了根的位置,可以计算出左子树的长度和右子树的长度
//此时已经找到根
TreeNode root = new TreeNode(inorder[rootIndex],null,null);
//计算左子树长度
int left_size = rootIndex - il;
//构造左子树
root.left = dfs(pl + 1,pl + left_size,il,rootIndex - 1,preorder,inorder);
//计算右子树长度
int right_size = ir - rootIndex;
//构造右子树
root.right = dfs(pl + left_size + 1,pr,rootIndex + 1,ir,preorder,inorder);
return root;
}
public TreeNode buildTree(int[] preorder, int[] inorder) {
inorderIndex = new HashMap<>();
int n = preorder.length;
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<Integer> set = map.keySet();
List<Long> 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<Long> a = new ArrayList<>();
List<TreeNode> q = List.of(root);
while (!q.isEmpty()) {
long sum = 0;
List<TreeNode> 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<Integer>> closestNodes(TreeNode root, List<Integer> queries) {
Queue<TreeNode> pq = new LinkedList<>();
List<Integer> querySum = new ArrayList<>();
pq.offer(root);
while(!pq.isEmpty()){
Queue<TreeNode> 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<Integer>> 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<TreeNode> 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<TreeNode> 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<Integer> 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.枚举所有质数节点,对于当前的质数来说,会将整个树划分为多个连通块。对于每个连通块,记录所有合法的路径(即全是合数的路径,这样可以与当前质数节点形成一条合法的路径)。此时,不同连通块之间的合数路径,可以通过当前的质数节点进行相连,两者之间进行选择,此时为最终的结果。
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<Integer>[] 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<Integer> 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<Integer>[] g,List<Integer> 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问题,前置知识为:树中距离之和
子树大小计算:后序遍历并统计。
保证每个节点只递归访问1次:对于图来说,使用vis
数组记录每个点的访问次数。但是对于树来说,一直向下递归,就不会遇到之前访问的点,所以不需要数组,只需要避免重复访问父节点即可。
树中距离之和:
class Solution {
private List<Integer>[] 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<Integer>[] g;
private Set<Long> 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<Integer> 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<Integer> deque;
public MyQueue() {
this.deque = new LinkedList<Integer>();
}
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<Integer> 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<Integer> 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<Integer> 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]),则计算当前能存储的雨量,并将该落差填平。
因此当当前高度>栈顶高度,开始判断时,有以下情况:
- 存在落差(栈不为空,且有前面的数据 > 后面的数据的情况)
此时需要计算填充的数量。利用idx记录左右两边的坐标,以最小的高度 * 长度,算出最大能填充的数量,并减去pre高度 * 长度(易见,在这种情况下,中间全是pre高度,否则早就形成了落差并填平)。
算出后,将原有左端点继续放入栈中,代表填平。
仍存在两种情况,一种当前高度<=左边高度,那么以当前高度为右的区域就不能再形成落差,那么就可以结束当前循环。
若当前高度>左边的高度,那么可能还存在左边还有更高的高度能和当前形成落差,因此循环继续。 - 不存在落差,结束当次循环
最后,将当前高度加入到栈顶。
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<Node> dq = new LinkedList<>();
为了做好运维面试路上的助攻手,特整理了上百道 **【运维技术栈面试题集锦】** ,让你面试不慌心不跳,高薪offer怀里抱!
这次整理的面试题,**小到shell、MySQL,大到K8s等云原生技术栈,不仅适合运维新人入行面试需要,还适用于想提升进阶跳槽加薪的运维朋友。**
![](https://img-blog.csdnimg.cn/img_convert/2a70233c3a47685c2fe8414db05e4c2a.png)
本份面试集锦涵盖了
* **174 道运维工程师面试题**
* **128道k8s面试题**
* **108道shell脚本面试题**
* **200道Linux面试题**
* **51道docker面试题**
* **35道Jenkis面试题**
* **78道MongoDB面试题**
* **17道ansible面试题**
* **60道dubbo面试题**
* **53道kafka面试**
* **18道mysql面试题**
* **40道nginx面试题**
* **77道redis面试题**
* **28道zookeeper**
**总计 1000+ 道面试题, 内容 又全含金量又高**
* **174道运维工程师面试题**
> 1、什么是运维?
> 2、在工作中,运维人员经常需要跟运营人员打交道,请问运营人员是做什么工作的?
> 3、现在给你三百台服务器,你怎么对他们进行管理?
> 4、简述raid0 raid1raid5二种工作模式的工作原理及特点
> 5、LVS、Nginx、HAproxy有什么区别?工作中你怎么选择?
> 6、Squid、Varinsh和Nginx有什么区别,工作中你怎么选择?
> 7、Tomcat和Resin有什么区别,工作中你怎么选择?
> 8、什么是中间件?什么是jdk?
> 9、讲述一下Tomcat8005、8009、8080三个端口的含义?
> 10、什么叫CDN?
> 11、什么叫网站灰度发布?
> 12、简述DNS进行域名解析的过程?
> 13、RabbitMQ是什么东西?
> 14、讲一下Keepalived的工作原理?
> 15、讲述一下LVS三种模式的工作过程?
> 16、mysql的innodb如何定位锁问题,mysql如何减少主从复制延迟?
> 17、如何重置mysql root密码?
**网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。**
**需要这份系统化的资料的朋友,可以添加V获取:vip1024b (备注运维)**
![img](https://img-blog.csdnimg.cn/img_convert/bb8a885f5611dfde65f1d7f137ba8dfc.jpeg)
**一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!**
de{
int val;
int idx;
Node(int x,int y){
this.val = x;
this.idx = y;
}
}
Deque<Node> dq = new LinkedList<>();
为了做好运维面试路上的助攻手,特整理了上百道 **【运维技术栈面试题集锦】** ,让你面试不慌心不跳,高薪offer怀里抱!
这次整理的面试题,**小到shell、MySQL,大到K8s等云原生技术栈,不仅适合运维新人入行面试需要,还适用于想提升进阶跳槽加薪的运维朋友。**
[外链图片转存中...(img-kOGRT1HT-1713345954584)]
本份面试集锦涵盖了
* **174 道运维工程师面试题**
* **128道k8s面试题**
* **108道shell脚本面试题**
* **200道Linux面试题**
* **51道docker面试题**
* **35道Jenkis面试题**
* **78道MongoDB面试题**
* **17道ansible面试题**
* **60道dubbo面试题**
* **53道kafka面试**
* **18道mysql面试题**
* **40道nginx面试题**
* **77道redis面试题**
* **28道zookeeper**
**总计 1000+ 道面试题, 内容 又全含金量又高**
* **174道运维工程师面试题**
> 1、什么是运维?
> 2、在工作中,运维人员经常需要跟运营人员打交道,请问运营人员是做什么工作的?
> 3、现在给你三百台服务器,你怎么对他们进行管理?
> 4、简述raid0 raid1raid5二种工作模式的工作原理及特点
> 5、LVS、Nginx、HAproxy有什么区别?工作中你怎么选择?
> 6、Squid、Varinsh和Nginx有什么区别,工作中你怎么选择?
> 7、Tomcat和Resin有什么区别,工作中你怎么选择?
> 8、什么是中间件?什么是jdk?
> 9、讲述一下Tomcat8005、8009、8080三个端口的含义?
> 10、什么叫CDN?
> 11、什么叫网站灰度发布?
> 12、简述DNS进行域名解析的过程?
> 13、RabbitMQ是什么东西?
> 14、讲一下Keepalived的工作原理?
> 15、讲述一下LVS三种模式的工作过程?
> 16、mysql的innodb如何定位锁问题,mysql如何减少主从复制延迟?
> 17、如何重置mysql root密码?
**网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。**
**需要这份系统化的资料的朋友,可以添加V获取:vip1024b (备注运维)**
[外链图片转存中...(img-aPl07Urm-1713345954584)]
**一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!**