0602-2020-LEETCODE-二分查找专题.经典53

二分什么时候用呢?给的数据是有序的,就要想到是不是可以使用二分。

经典.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;
    }
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值