文章目录
前言
需要开通vip的题目暂时跳过
笔记导航
点击链接可跳转到所有刷题笔记的导航链接
363.矩形区域不超过 K 的最大数值和
给定一个非空二维矩阵 matrix 和一个整数 k,找到这个矩阵内部不大于 k 的最大矩形和。
说明:
-
矩阵内的矩形区域面积必须大于 0。
-
如果行数远大于列数,你将如何解答呢?
-
解答
public int maxSumSubmatrix(int[][] matrix, int k) { int row = matrix.length; int col = matrix[0].length; int max = Integer.MIN_VALUE; for(int left = 0;left<col;left++){ // 滑动窗口左边界 int[] sumRow = new int[row]; for(int right = left;right<col;right++){ // 滑动窗口右边界 for(int r = 0;r<row;r++){//计算滑动窗口内,每一行的和 sumRow[r] += matrix[r][right]; } max = Math.max(max,getMax(sumRow,k));//在滑动窗口内找到一个最大的矩阵和 if (max == k) return k; } } return max; } public int getMax(int[] sumRow,int k){ int result = sumRow[0]; int resultMax = result; for (int i = 1; i < sumRow.length; i++) {//最大子序列和算法 if (result > 0) result += sumRow[i]; else result = sumRow[i]; if (result > resultMax) resultMax = result; } if(resultMax <= k) return resultMax; resultMax = Integer.MIN_VALUE; for(int i = 0;i<sumRow.length;i++){//暴力求解 result = 0; for(int j = i;j<sumRow.length;j++){ result += sumRow[j]; if(result > resultMax && result <= k) resultMax = result; if(resultMax == k)return result; } } return resultMax; }
-
分析
- 滑动窗口来解决。第一个for循环 限制滑动窗口左边的区间
- 在左边区间固定的基础上,新建一个数组,来存储每一行的和。
- 第二个for循环 限制滑动窗口的右区间,sumRow数组就是用来存储滑动窗口内每一行的和。
- 得到了滑动窗口内每一行的和之后,在这个窗口内找一个最大矩阵和。宽度不变,滑动窗口左右区间限制好了,就是找不同高度的矩阵,最大的和。
- 问题就变成了找最大子序列和的算法。
- 但是如果最大子序列和算出来的最大值 不满足 题目限制的小于等于k这个条件。
- 那么就使用暴力来找最大的小于等于k的子序列和。
-
提交结果
365. 水壶问题
有两个容量分别为 x升 和 y升 的水壶以及无限多的水。请判断能否通过使用这两个水壶,从而可以得到恰好 z升 的水?
如果可以,最后请用以上水壶中的一或两个来盛放取得的 z升 水。
你允许:
-
装满任意一个水壶
-
清空任意一个水壶
-
从一个水壶向另外一个水壶倒水,直到装满或者倒空
-
解答
public boolean canMeasureWater(int x, int y, int z) { if (z == 0) { return true; } if (x + y < z) { return false; } Queue<Pair> queue = new ArrayDeque<>(); Pair<Integer, Integer> start = new Pair(0, 0); queue.add(start); Set<Pair> visited = new HashSet<>(); visited.add(start); while (!queue.isEmpty()) { Pair<Integer, Integer> entry = queue.poll(); int curX = entry.getKey(); int curY = entry.getValue(); if (curX == z || curY == z || curX + curY == z) { return true; } if (curX == 0) { // 把第一个桶填满 addIntoQueue(queue, visited, new Pair(x, curY)); } if (curY == 0) { // 把第二个桶填满 addIntoQueue(queue, visited, new Pair(curX, y)); } if (curY < y) { // 把第一个桶倒空 addIntoQueue(queue, visited, new Pair(0, curY)); } if (curX < x) { // 把第二个桶倒空 addIntoQueue(queue, visited, new Pair(curX, 0)); } // y - curY是第二个桶还可以再加的水的升数,但是最多只能加curX升水。 int moveSize = Math.min(curX, y - curY); // 把第一个桶里的moveSize升水倒到第二个桶里去。 addIntoQueue(queue, visited, new Pair(curX - moveSize, curY + moveSize)); // 反过来同理,x - curX是第一个桶还可以再加的升数,但是最多只能加curY升水。 moveSize = Math.min(curY, x - curX); // 把第二个桶里的moveSize升水倒到第一个桶里去。 addIntoQueue(queue, visited, new Pair(curX + moveSize, curY - moveSize)); } return false; } private void addIntoQueue(Queue<Pair> queue, Set<Pair> visited, Pair<Integer, Integer> newEntry) { if (!visited.contains(newEntry)) { visited.add(newEntry); queue.add(newEntry); } }
-
分析
- queue存储每次操作之后的两个桶中的水
- visited存储已经出现过的两个桶中的水量,避免重复的计算。
- 首先两个桶都是0L水,开始操作,每次操作入队,相当于BFS。有如下几种情况
- 把第一个桶罐满
- 把第二个桶罐满
- 把第一个桶倒空,前提是第二个桶不空,如果空的话,再把第一个桶倒空那就变回初始状态,没有意义
- 把第二个桶倒空,条件和上面同理。
- 把第一个桶里的水倒入第二个桶,但是要计算第二个桶剩余可倒入的水和第一个桶中的水哪个更少。如果第一个桶中剩余的水多余第二个桶中可倒入的水,那么只能将一部分倒入第二个桶中,否则全部倒入第二个桶中。所以只需要求更小的那个值,来转移到第二个桶中即可。
- 把第二个桶中的水倒入第一个桶中,原理同上。
- 当curX == z || curY == z || curX + curY == z这个条件成立,则返回true。
-
提交结果
367.有效的完全平方数
给定一个正整数 num,编写一个函数,如果 num 是一个完全平方数,则返回 True,否则返回 False。
说明:不要使用任何内置的库函数,如 sqrt。
-
解答
//方法一 public boolean isPerfectSquare(int num) { long temp = num; while(temp > 1){ temp /= 2; if(temp * temp == num)return true; if((temp * temp) < num){ for(long i = temp;i< 2 * temp;i++){ if( i * i == num)return true; } return false; } } return true; } 方法二 public boolean isPerfectSquare(int num) { if (num < 2) { return true; } long left = 2, right = num / 2, x, guessSquared; while (left <= right) { x = left + (right - left) / 2; guessSquared = x * x; if (guessSquared == num) { return true; } if (guessSquared > num) { right = x - 1; } else { left = x + 1; } } return false; }
-
分析
- 每次除以2,当找到结果平方 小于 num的时候,在temp到2*tmep的范围内寻找满足平方和等于 num的值,如果存在 则返回true。否则循环结束返回false。
- 最后面的一个返回true,是当参数num = 1的时候返回true。
- 方法二对方法一的改进,使用二分查找
-
提交结果
方法一
方法二
368.最大整除子集
给出一个由无重复的正整数组成的集合,找出其中最大的整除子集,子集中任意一对 (Si,Sj) 都要满足:Si % Sj = 0 或 Sj % Si = 0。
如果有多个目标子集,返回其中任何一个均可。
-
解答
//方法一 ArrayList<Integer> res = new ArrayList<>(); public List<Integer> largestDivisibleSubset(int[] nums) { Arrays.sort(nums); backTrack(new ArrayList<>(),nums,0); return res; } public void backTrack(ArrayList<Integer> temp,int[] nums,int index){ if(temp.size() > res.size()){ res = new ArrayList<>(temp); } for(int i = index;i<nums.length;i++){ if(isCanPut(temp,nums[i]) && (temp.size() + nums.length - i) > res.size()){ temp.add(nums[i]); backTrack(temp,nums,i+1); temp.remove(temp.size()-1); } } } public boolean isCanPut(ArrayList<Integer> temp,int num){ if(temp.size() == 0)return true; int biggest = temp.get(temp.size()-1); if(num % biggest == 0)return true; return false; } //方法二 public List<Integer> largestDivisibleSubset(int[] nums) { if(nums.length == 0)return new ArrayList<>(); Arrays.sort(nums); ArrayList<ArrayList<Integer>> lists = new ArrayList<>(); for(int i = 0;i<nums.length;i++){//遍历数组 int cur = nums[i]; ArrayList<Integer> temp = new ArrayList<>(); for(ArrayList<Integer> list : lists){//遍历之前的已知的集合。 int lastVal = list.get(list.size()-1);//拿出集合中的最后一个值进行判断 if(cur % lastVal == 0 && list.size() + 1 > temp.size()){//若可以整除,并且这个最大整除子集大于当前找到的集合。则更新temp。 temp = new ArrayList<>(list); temp.add(cur); } } if(temp.size() == 0){//之前没有可以整除的集合。这里返回自己作为一个集合。 temp.add(cur); } lists.add(new ArrayList<>(temp)); } int maxLen = 0; ArrayList<Integer> res = new ArrayList<>(); for(ArrayList<Integer> list : lists){ if(list.size() > maxLen){ res = list; maxLen = list.size(); } } return res; }
-
分析
-
回溯来实现
-
首先先对数组进行排序,
-
然后每次往候选集合中尝试的放入一个数字。
-
这个数字必须要可以和当前集合中最大的一个数字可以整除。
-
因为当前集合中最大的数字可以整除前面的所有数字,所有如果准备添加进去的数字可以整除最大的数字,那么就可以整除其余的数字。
-
(temp.size() + nums.length - i) > res.size() 剪枝。若剩余可添加的数字 加上候选集的大小 没有大于 答案集合中的数字个数。那么就不可能存在更优的解,直接返回。
-
方法二
记录下当前遍历的数组的过程中,以这个数字为结尾的最大整除子集。
-
-
提交结果
方法一
方法二
371.两整数之和
不使用运算符 + 和 - ,计算两整数 a 、b 之和。
-
解答
public int getSum(int a, int b) { while(b != 0){//直到进位为0 int temp = a ^ b;//无进位相加 b = (a & b) << 1;//获得进位,b保留进位 a = temp;//保留相加的结果 } return a; }
-
分析
- 异或运算可以得到无进位的加法结果。
- 与运算 并且左移一位可以得到进位,
- 直到进位为0,返回结果。
-
提交结果
372.超级次方
你的任务是计算 ab 对 1337 取模,a 是一个正整数,b 是一个非常大的正整数且会以数组形式给出。
-
解答
int base = 1337; public int superPow(int a, int[] b) { int len = b.length; int ans = indexPow(a, b, len); return ans; } private int myPow(int a,int k){ a %=base; int ans=1; for(int i = 0;i<k;i++){ ans *= a; ans %=base; } return ans; } private int indexPow(int a,int[] b,int index){ if(index < 1 )return 1; int part1 = myPow(a, b[index-1]); index--; int part2 = myPow(indexPow(a, b, index), 10); return part1*part2%base; }
-
分析
- 如下图所示,从数组b的个位开始看起,如果不是0,例如个位为8,那么就将这一位改成0,再后面✖️上2的8次方。
- 如果当前位是0,那么就这一位删去,在外面套一个10次方。
- 删去的操作,可以用索引移动位置来代替。索引index的位置就等同于图中的数组b的最后一个数字。
- 就这样可以建立一个递归的逻辑。
- 递归出口就是索引 < 1 返回1。
- 每一层的递归。都包括了两种,底数数组次方的运算 * 底数数字次方的运算。
- 注意:两个因子乘积的模,等于两个因子模的乘积再取模。
-
提交结果
373. 查找和最小的K对数字
给定两个以升序排列的整形数组 nums1 和 nums2, 以及一个整数 k。
定义一对值 (u,v),其中第一个元素来自 nums1,第二个元素来自 nums2。
找到和最小的 k 对数字 (u1,v1), (u2,v2) … (uk,vk)。
-
解答
//方法一 List<List<Integer>> res = new ArrayList<>(); public List<List<Integer>> kSmallestPairs(int[] nums1, int[] nums2, int k) { if(nums1.length == 0 || nums2.length == 0)return res; PriorityQueue<P> priorityQueue = new PriorityQueue<>(new Comparator<P>() { @Override public int compare(P o1, P o2) { return o1.sum-o2.sum; } }); for(int i = 0;i<nums1.length;i++){ for(int j = 0;j<nums2.length;j++){ priorityQueue.add(new P(nums1[i],nums2[j])); } } for(int i = 0;i<k;i++){ P p = priorityQueue.poll(); if(p!=null){ int number1 = p.number[0]; int number2 = p.number[1]; List<Integer> list = new ArrayList<>(); list.add(number1); list.add(number2); res.add(list); } } return res; } class P{ int sum; int[] number; public P(int a,int b){ number = new int[]{a,b}; sum = a+b; } } //方法二 List<List<Integer>> res = new ArrayList<>(); public List<List<Integer>> kSmallestPairs(int[] nums1, int[] nums2, int k) { if(nums1.length == 0 || nums2.length == 0)return res; PriorityQueue<int[]> priorityQueue = new PriorityQueue<>(new Comparator<int[]>() { @Override public int compare(int[] o1,int[] o2) { return nums1[o1[0]] + nums2[o1[1]] - (nums1[o2[0]] + nums2[o2[1]]); } }); for(int i = 0;i<nums1.length;i++){ priorityQueue.add(new int[]{i,0}); } for(int i = 0;i<k;i++){ int[] p = priorityQueue.poll(); if(p!=null){ int index1 = p[0]; int index2 = p[1]; List<Integer> list = new ArrayList<>(); list.add(nums1[index1]); list.add(nums2[index2]); res.add(list); if(index2 + 1 < nums2.length){ priorityQueue.add(new int[]{index1,index2+1}); } } } return res; }
-
分析
- 方法一
- 使用小顶堆暴力求解,将两个数组中所有的组合成对的放入到优先级队列中,根据组合的和来维护小顶堆。
- 之后在从小顶堆中取出k个即可。
- 方法二
- 在方法一基础上的改进
- 优先级队列中,不需要保存准确数值对以及它们的和,只需要记录下两个数值在数组中的索引坐标即可。
- 并且一开始不需要将所有的组合都放入到优先级队列中。
- 因为两个数组都是有序的,所以最小的一对和,肯定是两个数组索引都是0的时候。
- 所以一开始只需要将其中一个数组中的索引和另个数组中的索引0 构成组合对放入优先级队列中。
- 然后在每一次的从堆中取出一对之后,再往优先级队列中放入当前数组1的索引和数组2的索引+1的那一对。
- 这样可以先少插入堆中的次数。
-
提交结果
方法一
方法二
374.猜数字大小
猜数字游戏的规则如下:
- 每轮游戏,系统都会从 1 到 n 随机选择一个数字。 请你猜选出的是哪个数字。
- 如果你猜错了,系统会告诉你,你猜测的数字比系统选出的数字是大了还是小了。
你可以通过调用一个预先定义好的接口 guess(int num) 来获取猜测结果,返回值一共有 3 种可能的情况(-1,1 或 0):
-
解答
public int guessNumber(int n) { int start = 1; int end = n; for(;;){ int mid = start + (end-start)/2; if(guess(mid) == 0) return mid; if(guess(mid) == -1) end = mid -1; else if(guess(mid) == 1) start = mid +1; } }
-
分析
- 其实就是二分查找,找到那个数字
-
提交结果
375. 猜数字大小 II
我们正在玩一个猜数游戏,游戏规则如下:
我从 1 到 n 之间选择一个数字,你来猜我选了哪个数字。
每次你猜错了,我都会告诉你,我选的数字比你的大了或者小了。
然而,当你猜了数字 x 并且猜错了的时候,你需要支付金额为 x 的现金。直到你猜到我选的数字,你才算赢得了这个游戏。
-
解答
//方法一 public int getMoneyAmount(int n) { int[][] dp = new int[n+1][n+1]; for(int len = 2;len <= n;len++){//区间长度 for(int start = 1;start <= n-len+1;start++){//区间起始位置 int min = Integer.MAX_VALUE; for(int i = start;i<start + len -1;i++){//遍历区间 min = Math.min(min,i + Math.max(dp[start][i-1],dp[i+1][start + len - 1])); } dp[start][start + len - 1] = min; } } return dp[1][n]; } //方法二 public int getMoneyAmount(int n) { int[][] dp = new int[n+1][n+1]; for(int len = 2;len <= n;len++){ for(int start = 1;start <= n-len+1;start++){ int min = Integer.MAX_VALUE; for(int i = start + (len - 1)/2;i<start + len -1;i++){ min = Math.min(min,i + Math.max(dp[start][i-1],dp[i+1][start + len - 1])); } dp[start][start + len - 1] = min; } } return dp[1][n]; }
-
分析
- 方法一
- 将问题拆分成子问题,在一个区间内选择一个数字,区间被拆成左右两个。左区间和右区间得到的结果是不一样的。保留大的那个就是区间内选择一个数字后的至少需要多少现金。
- 规模最小的子问题是1个数字,此时不需要支付现金也可以得到。所以是0.
- 之后就求区间长度为2的结果,选择一个点支付现金,剩余部分就是区间为1的结果。
- 依次类推,最后得到区间为n的结果。
- dp[i] [j]表示i - j的数字范围内猜中至少需要多少现金。
- 方法二
- 是对方法一的改进,每个区间不需要从头开始遍历,因为猜大的数字开销会更大,所以只需要从中间开始遍历即可。
-
提交结果
方法一
方法二
376. 摆动序列
如果连续数字之间的差严格地在正数和负数之间交替,则数字序列称为摆动序列。第一个差(如果存在的话)可能是正数或负数。少于两个元素的序列也是摆动序列。
例如, [1,7,4,9,2,5] 是一个摆动序列,因为差值 (6,-3,5,-7,3) 是正负交替出现的。相反, [1,4,7,2,5] 和 [1,7,4,5,5] 不是摆动序列,第一个序列是因为它的前两个差值都是正数,第二个序列是因为它的最后一个差值为零。
给定一个整数序列,返回作为摆动序列的最长子序列的长度。 通过从原始序列中删除一些(也可以不删除)元素来获得子序列,剩下的元素保持其原始顺序。
-
解答
public int wiggleMaxLength(int[] nums) { if(nums.length < 2)return nums.length; ArrayList<Integer> list = new ArrayList<>(); list.add(nums[0]); if(nums[0] != nums[1]) list.add(nums[1]); int tf = nums[1] - nums[0]; for(int i = 2;i<nums.length;i++){ int last = list.get(list.size() -1); int temp = nums[i] - last; if((temp > 0 && tf > 0) || (temp <0 && tf < 0)){ list.remove(list.size()-1); } if(temp != 0){ list.add(nums[i]); tf = temp; } } return list.size(); }
-
分析
- 首先将数组中的前两个放入队列中,然后计算出他们是升序的关系还是降序的关系
- 从数组的第三个数字开始遍历
- 每次和队尾的元素做差,得到升序关系或者降序关系。
- 如果和前者相同,则替换掉当前的队尾元素。
- 如果和前者不同,则插入到队尾,并更新tf为新的升序或降序关系。用于下次的判断。
- 最后返回队列的长度即可。
-
提交结果
377. 组合总和 Ⅳ
给定一个由正整数组成且不存在重复数字的数组,找出和为给定目标正整数的组合的个数。
-
解答
// 方法一 记忆化搜索 int[] memo; public int combinationSum4(int[] nums, int target) { memo = new int[target + 1]; Arrays.fill(memo, -1); memo[0] = 1; return backTrack(nums,target); } public int backTrack(int[] nums,int target){ if(memo[target] != -1) return memo[target]; int res = 0; for(int num:nums){ if(target >= num) res += backTrack(nums,target-num); } memo[target] = res; return res; } // 方法二 dp int[] memo; public int combinationSum4(int[] nums, int target) { int[] dp = new int[target+1]; dp[0] = 1; for(int i = 1; i<= target;i++){ for(int num:nums){ if(i >= num){ dp[i] += dp[i-num]; } } } return dp[target]; }
-
分析
- 直接使用回溯会导致超时
- 所以使用记忆化递归的方式,这样可以减少重复的计算。
- 方法二使用动态规划
- 外层循环是目标值,内层循环是遍历数组nums
- 如果当目标值大于数组中的值。
- 那么当前位置的解就可以加上 dp[i-num];
- 最后返回dp[target]即可
-
提交结果
方法一
方法二
378. 有序矩阵中第K小的元素
给定一个 n x n 矩阵,其中每行和每列元素均按升序排序,找到矩阵中第 k 小的元素。
请注意,它是排序后的第 k 小元素,而不是第 k 个不同的元素。
-
解答
//方法一 public int kthSmallest(int[][] matrix, int k) { PriorityQueue<Integer> priority = new PriorityQueue<>(); for(int i =0;i<matrix.length;i++){ for(int j = 0;j<matrix[0].length;j++){ priority.add(matrix[i][j]); } } int res = 0; while(k > 0){ res = priority.poll(); k--; } return res; } //方法二 public int kthSmallest(int[][] matrix, int k) { int n = matrix.length; int left = matrix[0][0]; int right = matrix[n-1][n-1]; while(left < right){ int mid = left + (right - left)/2; if(dfs(matrix,mid,k)){ right = mid; }else{ left = mid + 1; } } return left; } public boolean dfs(int[][] matrix,int mid,int k){ int i = matrix.length -1; int j = 0; int num = 0; while(i >= 0 && j < matrix.length){ if(matrix[i][j] <= mid){ num += i + 1; j++; }else{ i--; } } return num >= k; }
-
分析
-
方法一 暴力求解
-
将所有的元素都放入到优先级队列中,也就是小顶堆
-
然后从堆顶取k次,第k次就是第k小的元素。
-
方法二
-
利用矩阵 行升序 列升序的性质
-
可以将矩阵从中间拆开 分成大于某一个数的部分和小于某一个数字的部分
如下图所示
-
此时可以从左下脚开始 遇到红线之前 计算小于等于8的部分,同时右移动,否则就上移。
-
这样遍历下来可以计算出小于等于8的个数。
-
参考这种遍历的方式可以使用二分查找的思路。
-
left是数字中的最小值 也就是这里的,在nums[0] [0]的位置, right是数字中的最大值 也就是这里的16,在nums[n-1] [n-1]的位置
-
计算中值mid = (left + right )/2
-
然后就是走上面的搜索矩阵的思路,计算有多少个小于等于mid的数字。
-
如果计算出来的结果大于给定的目标k,那么说明mid太大了。找到了太多的比mid小的数字
-
所以需要缩小右边界,也就是使right变小。令right = mid
-
如果计算出来的结果小于给定的目标k,那么说明mid太小了,需要增大左边界,也就是使left变大。令left = mid +1;
-
然后就是迭代上面的过程。
-
最终输出left或者right就是所要的结果。
-
-
提交结果
方法一
方法二
380.常数时间插入、删除和获取随机元素
设计一个支持在平均 时间复杂度 O(1) 下,执行以下操作的数据结构。
-
insert(val):当元素 val 不存在时,向集合中插入该项。
-
remove(val):元素 val 存在时,从集合中移除该项。
-
getRandom:随机返回现有集合中的一项。每个元素应该有相同的概率被返回。
-
解答
class RandomizedSet { HashMap<Integer, Integer> map; ArrayList<Integer> list; /** * Initialize your data structure here. */ public RandomizedSet() { map = new HashMap<>(); list = new ArrayList<>(); } /** * Inserts a value to the set. Returns true if the set did not already contain the specified element. */ public boolean insert(int val) { if (!map.containsKey(val)) { list.add(val); map.put(val, list.size()-1); return true; } return false; } /** * Removes a value from the set. Returns true if the set contained the specified element. */ public boolean remove(int val) { if (!map.containsKey(val)) { return false; } else { int index = map.get(val); list.set(index, list.get(list.size() - 1)); map.put(list.get(index), index); list.remove(list.size() - 1); map.remove(val); return true; } } /** * Get a random element from the set. */ public int getRandom() { Random random = new Random(); int i = random.nextInt(list.size()); return list.get(i); } }
-
分析
- 数组可以做到随机的访问,所以使用ArrayList来存储数据,
- 但是删除并不能做到O(1)的时间复杂度,因为需要移动数组里面的值,所以考虑仅删除最后一个数,这样就不需要移动数据。
- 但是如何定位到要删除的数呢?需要一个HashMap来存储数字对应的索引。这样就可以根据HashMap 获取到索引。
- 然后将数组中最后一个数字替换到这个索引的位置,再删除数组中的最后一个数字,然后将HashMap中的删除数字的索引关系的键值对 删除掉。
- 添加操作,就是添加再数组的最后面,保留数字和索引的关系记录在HashMap中。
-
提交结果