二分什么时候用呢?给的数据是有序的,就要想到是不是可以使用二分。
经典.53. 统计一个数字在排序数组中出现的次数。
public int search(int[] nums, int target) {
if(nums == null || nums.length == 0 || nums[nums.length - 1] < target) return 0;
int left = 0,right = nums.length - 1;
int res = 0;
while (left <= right){
int mid = left + ((right - left) >> 1);
if (nums[mid]== target){
res++;
return count(nums,mid,res,target);
}
if (nums[mid] < target){
left = mid + 1;
} else {
right = mid - 1;
}
}
return res;
}
private int count(int[] nums, int mid, int res, int target) {
int left = mid - 1;
int right = mid + 1;
while (left >= 0 && nums[left] == target){
left--;
res++;
}
while (right < nums.length && nums[right] == target){
right++;
res++;
}
return res;
}
经典.54. 0~n-1中缺失的数字()
自己写的稍微有点直接,需要几个特判。
public int missingNumber(int[] nums) {
if (nums[0] != 0) return 0;
int left = 0,right = nums.length - 1;
while (left <= right){
int mid = left + ((right - left) >> 1);
if (mid != nums[mid] && mid - 1 == nums[mid - 1]){
return mid;
} else if (mid < nums[mid]){
right = mid - 1;
} else {
left = mid + 1;
}
}
return nums.length;
}
下面的代码比较简洁,需要学习一下。
代码来源:https://leetcode-cn.com/problems/que-shi-de-shu-zi-lcof/solution/mian-shi-ti-53-ii-0n-1zhong-que-shi-de-shu-zi-er-f/
还有一种是异或的写法,但是不具有普遍性,具体可以参见评论区。
public int missingNumber(int[] nums) {
int i = 0,j = nums.length - 1;
while (i <= j){
int m = i + ((j - i) >> 1);
if (nums[m] == m) i = m + 1;
else j = m - 1;
}
return i;
}
异或代码来源:
https://leetcode-cn.com/problems/que-shi-de-shu-zi-lcof/comments/
class Solution {
public:
int missingNumber(vector<int>& nums) {
int res=nums.size();
for(int i=0;i<nums.size();i++){
res^=nums[i];
res^=i;
}
return res;
}
};
周赛第三题:
自己想的是一天一天的遍历,显然这个思路不合适,一天一天的判断不仅判断条件难写,而且很容易超时。
二分是不错的想法,首先我们不再一天一天的判断,目的就是减少循环的次数。找出结果的区间,我们定义为最小值和最大值,最大值可以设置成所有的花的最大的开花的天数。
取中间的值,mid,看看mid够不够制作 m束 k 朵花(k朵花必须连续)。
不够的话,left = mid + 1;
够的话,right = mid; 因为可能 mid - 1 的值就不够了。
代码来源:
评论区大佬。
https://leetcode-cn.com/problems/minimum-number-of-days-to-make-m-bouquets/comments/
class Solution {
public int minDays(int[] bloomDay, int m, int k) {
if (m * k > bloomDay.length) return -1;
int left = 0,right = 0;
for (int num : bloomDay){
right = Math.max(right,num);
}
while (left < right){
int mid = left + ((right - left) >> 1);
int count = count(bloomDay,mid,k);
if (count >= m){
right = mid;
} else {
left = mid + 1;
}
}
return left;
}
private int count(int[] arr, int days, int k) {
int res = 0;
int count = 0;
for (int i = 0; i < arr.length; i++) {
if (arr[i] <= days){
count++;
} else {
count = 0;
}
if (count == k){
res++;
count = 0;
}
}
return res;
}
}
LCP 12.小张刷题计划
思路也是二分,首先我们找出一个最大的时间就是把所有的题的时间全部加起来,作为二分查找的右边界,0作为二分查找的左边界。第一步,取中间数mid,判断其是否满足条件。
写一个辅助函数,目的就是判断当前的mid是不是足够覆盖所有的题号。
如何判断呢?首先根据题意,我们必须有做题顺序的在m天内做完所有的题目,每天有一次场外求助的机会。
我们要思考一下,现在我们要求出一个最小的做题时间,来满足条件,策略就是把所有的题分成m段,
每段的最大值交给场外求助,这一段别的题自己做,但是如何分段其实不是我们在意的,因为我们已经隐形的分好段了,就是mid,mid就是一天做题的最大时间,每次记录当前段的最大值与 sum 和,直到mid 无法大于等于 sum - max,这就是需要重启一天的标志,所有的参数重新设置一次,具体的参数设置的意义都在注释。
代码来源:
https://leetcode-cn.com/problems/xiao-zhang-shua-ti-ji-hua/submissions/
public int minTime(int[] time, int m) {
if (m >= time.length) return 0;
int len = time.length;
int left = 0,right = 0;
for (int i = 0; i < len; i++) {
right += time[i];
}
while (left < right){
int mid = left + (right - left) / 2;
if (CanSolve(time, mid, m)){
right = mid;
} else {
left = mid + 1;
}
}
return left;
}
private boolean CanSolve(int[] time, int mid, int m) {
//m 就是天数,也是场外求助次数。
int max = 0;
int sum = 0;
int n = time.length;
//可以理解为,接下来的代码是为了划分 m 段,找到一个最大值
//mid如果可以覆盖每一个段(每一个段可以删去这个最大值)
for (int i = 0; i < n; i++) {
//如果天数用完,但是还没有遍历到最后,直接返回false
if (m == 0) return false;
//有更大的值,最大值更新。
if (max < time[i]) max = time[i];
//计算当前的sum
sum += time[i];
//如果mid可以覆盖当前的sum 减去最大值(最大值交给别人做),那么就继续循环
//否则,就要进入下一天
//因为当前这天没有覆盖,首先i--,进入下一天就max就重新更新置零,m天数需要--,sum需要更新置零。
//我们最后的判断条件是当前的 mid 在 m 天是内否能覆盖所有的题号。
//如果 m == 0了还没有遍历完毕,那么说明这个mid不够
//如果这个mid足够遍历完所有的题,那么就返回true,二分查找更小的值是不是足够遍历整个数组。
if (mid < sum - max){
i--;
max = 0;
m--;
sum = 0;
}
}
return true;
}
1300.转变数组后最接近目标值的数组和
题目要求是,找到一个阈值使得整个数组的和最接近target。其实画图是非常方便理解的,这个阈值代表了,一切小于阈值的数都等于本身,大于等于阈值的数都等于阈值,其实就是截断效应。一般来说我们把阈值定的越高,最后的sum和也越高(但是不是严格单调的,太大了最后就不变了)。
所以思路是,找到第一个大于等于target的值或者小于等于target的值。这需要考虑
|条件| 可能的取值 |
代码来源:
https://leetcode-cn.com/problems/sum-of-mutated-array-closest-to-target/solution/er-fen-cha-zhao-by-liweiwei1419-2/
|–|--|
| 小于等于target |最后的结果可能是left或者left + 1 |
|大于等于target| 最后的结果可能是left或者left - 1 |
所以就有了以下的代码。
public int findBestValue(int[] arr, int target){
int left = 0;
int right = 0;
for (int num : arr){
right = Math.max(right,num);
}
//找第一个大于等于 target 的数字。
while (left < right){
int mid = left + (right - left) / 2;
int sum = calCum(arr,mid);
if (sum < target){
left = mid + 1;
} else {
right = mid;
}
}
if (Math.abs(calCum(arr,left) - target) >= Math.abs(calCum(arr,left - 1) - target)){
return left - 1;
}
return left;
}*/
public int findBestValue1(int[] arr, int target) {
int left = 0;
int right = 0;
for(int num : arr){
right = Math.max(num,right);
}
//left == right 时必须退出循环
//这个方法找的是第一个小于等于 target 的值,这是很重要的。
while (left < right){
int mid = left + (right - left + 1) / 2;
int sum = calCum(arr,mid);
if (sum > target){
right = mid - 1;
} else {
left = mid;
}
}
//因为选的是小于等于target的值,所以我们要尝试left和left + 1哪一个更接近target
int sum1 = Math.abs(calCum(arr,left) - target);
int sum2 = Math.abs(calCum(arr,left + 1) - target);
if (sum1 > sum2) return left + 1;
return left;
}
private int calCum(int[] arr, int threshold) {
int sum = 0;
for (int num : arr){
sum += Math.min(num,threshold);
}
return sum;
}
需要特别注意的是:只要看到left == mid 那么最后mid的取值一定是 mid = left + (right - left + 1);不然就是死循环。
最后贴上自己写的代码,很混乱,这个题的边界问题一直没有处理好,left right >= 还是 > ,很混乱。
虽然过了,但是很多是试出来的。
其实是不需要排序的。
public int findBestValue(int[] arr, int target) {
//Arrays.sort(arr);
int len =arr.length;
int left = 0,right = arr[len - 1];
if (arr[0] >= (target / len)) {
int temp = target / len;
if (Math.abs(isBetter(arr,temp) - target) <= (Math.abs(isBetter(arr,temp + 1) - target))){
return temp;
} else return temp + 1;
}
while (left < right){
int mid = left + (right - left) / 2;
int tempL = Math.abs(isBetter(arr,left) - target);
int tempR = Math.abs(isBetter(arr,right) - target);
if (tempL <= tempR){
right = mid - 1;
} else {
left = mid + 1;
}
}
return left;
}
private int isBetter(int[] arr, int mid) {
int[] temp = new int[arr.length];
for (int i = 0; i < arr.length; i++) {
temp[i] = arr[i];
}
int sum = 0;
for (int i = 0; i < temp.length; i++) {
if (temp[i] > mid) temp[i] = mid;
sum += temp[i];
}
return sum;
}
最后再了解一下前缀和的做法:
代码来源:
https://leetcode-cn.com/problems/sum-of-mutated-array-closest-to-target/solution/bian-shu-zu-hou-zui-jie-jin-mu-biao-zhi-de-shu-zu-/
public int findBestValue1(int[] arr,int target){
Arrays.sort(arr);
int len = arr.length;
int[] prefix = new int[len + 1];
for (int i = 1; i <= len; i++) {
prefix[i] = prefix[i - 1] + arr[i - 1];
}
int ans = 0;
int diff = target;
for (int i = 0; i < arr[len - 1]; i++) {
int index = Arrays.binarySearch(arr,i);
if (index < 0) {
index = - index - 1;
}
int cur = prefix[index] + (len - index) * i;
if (Math.abs(cur - target) < diff){
ans = i;
diff = Math.abs(cur - target);
}
}
return ans;
}
很厉害的前缀和:
代码来源:
https://leetcode-cn.com/problems/sum-of-mutated-array-closest-to-target/solution/zui-you-jie-fa-onlogn-by-java_lee/
a - (d + k / 2) / k; 很巧妙,相当于找到第一个大于等于target的值以后,把大于target的这个差值平均给后面的k个值消去不就是最接近等于 target 的值了吗?
class Solution {
public int findBestValue(int[] arr, int target) {
Arrays.sort(arr); //arr升序排序处理
int N = arr.length, pre = 0; //arr[](升序后)的前缀合(含首0)
int k=N; //后面还有k项,同时k也是S(a)在a处的斜率!
for( int a : arr) {
int d = pre + a * k - target; //前i项和pre + 后k项全置为a 与 target差值
if (d >= 0){ return a - (d + k / 2) / k; } //等价于 a - 四舍五入(d÷k) 。
pre += a; --k; //当d÷k的小数部分为0.5,则取1(注意整体是减去1)符合同近取小原则。
}
return arr[N-1]; //for循环内无解,取arr[]最大值作为结果。
}
}
作者:java_Lee
链接:https://leetcode-cn.com/problems/sum-of-mutated-array-closest-to-target/solution/zui-you-jie-fa-onlogn-by-java_lee/
来源:力扣(LeetCode)
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
寻找重复数(空间复杂度O(1)时间复杂度不超过O(n2)不能修改原数组):
快慢指针有一篇很好的文章:
https://leetcode-cn.com/problems/find-the-duplicate-number/solution/kuai-man-zhi-zhen-de-jie-shi-cong-damien_undoxie-d/
public int findDuplicate(int[] nums){
int slow = 0;
int fast = 0;
while (true){
slow = nums[slow];
fast = nums[nums[fast]];
if (slow == fast){
int finder = 0;
while (nums[finder] != nums[slow]){
slow = nums[slow];
finder = nums[finder];
}
return finder;
}
}
}
李威威的二分:
https://leetcode-cn.com/problems/sum-of-mutated-array-closest-to-target/solution/er-fen-cha-zhao-by-liweiwei1419-2/
/*public int findDuplicate(int[] nums) {
int len = nums.length;
int left = 1,right = len - 1;
while (left < right){
int mid = (left + right) >>> 1;
int cur = 0;
for (int num : nums){
//统计结果小于等于4的结果
if (num <= mid){
cur++;
}
}
//如果小于等于4的结果最后大于4,那么结果就在 1 到 4
if (cur > mid){
right = mid;
} else {
left = mid + 1;
}
}
return left;
}*/
public int findDuplicate(int[] nums){
int len = nums.length;
int left = 1,right = len - 1;
while (left < right){
int mid = (left + right + 1) >>> 1;
int cur = 0;
for (int num : nums){
//严格小于 4 的个数如果大于等于4,那么结果必然在1到3
if (num < mid) {
cur++;
}
}
if (cur >= mid){
right = mid - 1;
} else {
left = mid;
}
}
return left;
}
875.爱吃香蕉的珂珂
自己想的代码,但是不知为何速度不太快。
public int minEatingSpeed(int[] piles, int H) {
if (piles.length == 1){
if (H >= piles[0]) return 1;
else return piles[0] % H == 0 ? piles[0] / H : piles[0] / H + 1;
}
int left = 1;
int right = 0;
for (int num : piles){
right = Math.max(right,num);
}
while (left < right){
int mid = (left + right) >>> 1;
if (check(piles,mid,H)){
right = mid;
} else {
left = mid + 1;
}
}
return left;
}
private boolean check(int[] piles, int k, int h) {
for (int i = 0; i < piles.length; i++) {
//还有值
if (h <= 0) return false;
if (piles[i] <= k) {
h--;
} else {
int temp = piles[i] / k;
int need = piles[i] % k == 0 ? temp : temp + 1;
if (h < need) return false;
h -= need;
}
}
return true;
}
410.分割数组的最大值
是自己写的,其实和小张刷题特别像,就像一个评论说的,这种题的思路都是相似的,还有一个制作花束的问题,总共三步。
1.确定最大值和最小值的区间,也就是二分的left和right
2.确定下一个收敛的区间,如果mid 可以满足条件,如何确定下一轮更精细的搜索区间
3.写出上一个判断条件
自己写的题解:
https://leetcode-cn.com/problems/split-array-largest-sum/solution/java-ji-bai-100-er-fen-fa-jian-ji-si-lu-by-pdzz/
public int splitArray(int[] nums, int m) {
//要求最小的最大值,我们首先定义个一个边界 left 和 right
int left = 1,right = 0;
for(int e : nums){
right += e;
}
if (right < 0) right = Integer.MAX_VALUE;
while (left < right){
int mid = (left + right) >>> 1;
if (CanSplitMArray(nums,mid,m)){
//如果这个mid值可以把原数组划分成m个数组,那么就试图保留mid的查找更小的值
//所以是right = mid
right = mid;
} else {
//如果这个mid值不够大,也就是用它划分数组结果会不止 m 个,那么就找更大的值
//此时的 mid 也不需要了。
left = mid + 1;
}
}
return left;
}
private boolean CanSplitMArray(int[] nums, int max, int m) {
int len = nums.length;
int sum = 0;
for (int i = 0; i < len; i++) {
//如果单个数值都大于 max ,直接返回false,因为没法划分。
if (nums[i] > max) return false;
//划分完m个数组了,但是还有值等待划分,也返回false,因为最后划分出来的段数大于m,说明这个max不够大
if (m == 0) return false;
sum += nums[i];
if (sum > max){
i--;
sum = 0;
m--;
}
}
return true;
}
1095.山脉数组中找指定的target
模仿着写的,需要仔细体会,如果在while题中间找到了mid要返回的话,原来的模板需要修改一下,就是最后退出是因为left = right ,所以最后要判断一下left或者right是不是想要的值,是的话返回下标,不是的话,说明没找到,返回-1。
还有就是山脉数组中找山峰也需要体会一下自己优化的还可以。
代码的思路来源:
https://leetcode-cn.com/problems/find-in-mountain-array/solution/shi-shi-hou-ji-chu-wo-de-mo-neng-er-fen-mo-ban-lia/
public int findInMountainArray(int target,MountainArray mountainArr) {
int len = mountainArr.length();
int left = 0;
int right = len - 1;
while (left < right){
int mid = (left + right + 1) >>> 1;
int midVal = mountainArr.get(mid);
if (midVal > mountainArr.get(mid - 1)){
left = mid;
} else {
right = mid - 1;
}
}
int index = search(mountainArr,0,left,target,true);
return index == -1 ? search(mountainArr,left,len - 1,target,false) : index;
}
private int search(MountainArray mountainArr, int left, int right, int target,boolean flag) {
while (left <= right){
int mid = (left + right) >>> 1;
if (mountainArr.get(mid) == target){
return mid;
} else if (mountainArr.get(mid) < target){
left = flag ? mid + 1 : left;
right = flag ? right : mid - 1;
} else {
left = flag ? left : mid + 1;
right = flag ? mid - 1 : right;
}
}
return -1;
}