二分查找中的区间开闭问题

问题来源:在写二分查找类算法题时,总是不知道如何去对写搜索区间的左右指针变量初始化、赋值以及循环结束条件。

二分查找的思路:尽可能地根据题意,寻找其中的单调性。即在有序数组中进行二分查找,有序性这一点很重要!下面从力扣875.爱吃香蕉的珂珂来初步了解一下二分查找的应用思路。

题目描述:珂珂喜欢吃香蕉。这里有 n 堆香蕉,第 i 堆中有 piles[i] 根香蕉。警卫已经离开了,将在 h 小时后回来。珂珂可以决定她吃香蕉的速度 k (单位:根/小时)。每个小时,她将会选择一堆香蕉,从中吃掉 k 根。如果这堆香蕉少于 k 根,她将吃掉这堆的所有香蕉,然后这一小时内不会再吃更多的香蕉。  珂珂喜欢慢慢吃,但仍然想在警卫回来前吃掉所有的香蕉。返回她可以在 h 小时内吃掉所有香蕉的最小速度 kk 为整数)。

示例 1:输入:piles = [3,6,7,11], h = 8    输出:4

// 暴力解法:两次for循环
class Solution {
    public int minEatingSpeed(int[] piles, int h) {
        int time = 0;
        for(int k = 1; ; k++) {
            for(int i = 0; i < piles.length; i++) {
                time += (piles[i] == k ?  (piles[i] / k) : (piles[i] / k)+ 1);
            }
            if(time <= h) {
                return k;
            } else {
                time = 0;
            }
        }
    }
}

根据题意:吃香蕉速度越小,耗时越多。反之,速度越大,耗时越少,这是本题中的单调性。优化的无非是吃香蕉的速度区间中,不符合的速度,从而找到第一个满足条件(>=)的速度值。

// 二分搜索:本质在于二分查找优化了暴力思路的不必要取值
class Solution {
    public int minEatingSpeed(int[] piles, int h) {
        int maxSpeed = piles[0];
        for(int i = 1; i < piles.length; i++) {
            maxSpeed = Math.max(maxSpeed, piles[i]);
        }
        int left = 1, right = maxSpeed; // 这里取的是左闭右开区间,即[left, right)
        while(left < right) {
            int mid = (right - left) / 2 + left;
            if(calculateSum(piles, mid) > h) {
                left = mid + 1; // [mid + 1, right]
            } else {
                right = mid;
            }
        }
        return left;
    }

    public int calculateSum(int[] piles, int speed) {
        int time = 0;
        for(int pile : piles) {
            time += (pile % speed == 0 ? (pile / speed) : (pile / speed) + 1);
            // 上取整也可以这样写:sum += (pile + speed - 1) / speed;
        }
        return time;
    }
}

可以看到,本题求的是【第一个满足条件(>=)的速度值】,注意这里标注的(>=),后面的思路探讨也是基于此展开。

接下来再看力扣34.在排序数组中查找元素的第一个和最后一个位置】,如下:

描述:给你一个按照非递减顺序排列的整数数组 nums,和一个目标值 target。请你找出给定目标值在数组中的开始位置结束位置

示例 1:输入:nums = [5,7,7,8,8,10], target = 8        输出:[3,4]

 开始位置:即 >=target的数的位置;结束位置:即 <=target的数的位置

class Solution {
    public int[] searchRange(int[] nums, int target) {
        // 开始位置,即第一个 '>= target' 的数
        int start = findNum(nums, target);
        if(start == nums.length || nums[start] != target) {
            return new int[]{-1, -1};
        }
        // 结束位置,即第一个 '>= (target + 1)' 的数再'-1'
        int end = findNum(nums, target + 1) - 1;
        return new int[]{start, end};
    }

    // 寻找第一个 '>= target' 的数(左闭右闭区间写法)
    public int findNum(int[] nums, int target) {
        int l = 0, r = nums.length - 1; // 注意右指针位置
        while(l <= r) {
            int mid = (r - l) / 2 + l;
            if(nums[mid] < target) {
                l = mid + 1; // [mid + 1, r]
            } else {
                r = mid - 1; // [l, mid - 1]
            }
        }
        return l;
    }
}

💡思路扩展:(有序数组中的二分查找思路)

从本题中的第一个和最后一个位置所对应的>=target<=target两种情况,可以衍生为四种情况:>>=<<=。其中【><<=】都可以通过>=进行转化计算得到(如下):

>= : find(x)(通过方法 findNum() 寻找第一个大于等于 x 的数)

  • :find(≥ x) + 1 (寻找第一个 大于等于 x 的数,再取其右边的数(即+1))
  • :find(≥ x) - 1 (寻找第一个大于等于 x 的数,再取其左边的数(即-1))
  • <= :find(> x) - 1(寻找第一个大于等于 x + 1 的数,再取其左边的数(即-1))

💡可以这么理解区间开闭:

  • 左闭右闭区间:循环结束条件是 l <= r ,[l, r] 区间还存在一个元素要检查,故while(l <= r)。l = r 时还要检查一次,不能漏;
  • 左闭右开区间:循环结束条件是 l < r ,[l, r)区间已不存在元素要检查,故while(l < r)。l = r 时直接退出;
  • 左开右开区间:循环结束条件是 l + 1 <= r ,(l, r)区间已不存在元素要检查,故while(l + 1 < r)。l + 1 = r 时退出。
// findNum()方法:寻找第一个 >=target 的数(即开始位置)

// 【左闭右闭区间】
public int findNum(int[] nums, int target) {
    int l = 0, r = nums.length - 1;
    while(l <= r) {
        int mid = (r - l) / 2 + l;
        if(nums[mid] < target) {
            l = mid + 1;
        } else {
            r = mid - 1;
        }
    }
    return l;
}

// 【左闭右开区间】
public int findNum(int[] nums, int target) {
    int l = 0, r = nums.length;
    while(l < r) {
        int mid = (r - l) / 2 + l;
        if(nums[mid] < target) {
            l = mid + 1;
        } else {
            r = mid;
        }
    }
    return l;
}

// 【左开右开区间】
public int findNum(int[] nums, int target) {
    int l = -1, r = nums.length;
    while(l + 1 < r) {
        int mid = (r - l) / 2 + l;
        if(nums[mid] < target) {
            l = mid;
        } else {
            r = mid;
        }
    }
    return r;
}

注意三点:

① 变量 left 和 right 的初始化;② left 和 right 的赋值操作;③ 返回结果是 left 还是 right

最后,前两种写法都是返回 left,左开右开写法返回的是 right。三种实现本质结果都一样,只是根据起始位置的影响不同,可以选择不同实现。

调用这个方法时,需要注意参数的始末位置(见题 2563.统计公平数对的数目,代码如下)

findNum() 方法是 左闭右闭区间[left, right],因此搜索区间始末位置是【 left = i + 1;right = nums.length - 1】;对应地,如果写的是左开右开区间(left, right),则传入的始末位置参数是【 left = i;right = nums.length】

class Solution {
    public long countFairPairs(int[] nums, int lower, int upper) {
        Arrays.sort(nums);
        long res = 0;
        for(int i = 0; i < nums.length; i++) {
            // 符合条件的元素区间(注意 findNum 是左闭右闭区间,搜索区间起始位置是'i + 1')
            int target1 = lower - nums[i];
            int start = findNum(nums, i + 1, target1); // 第一个'大于等于lower'位置
            int target2 = upper - nums[i];
            int end = findNum(nums, i + 1, target2 + 1) - 1; // 第一个'小于等于upper'位置
            res += end - start + 1;
        }
        return res;
    }

    // 寻找第一个 >=target 的数(即开始位置)
    public int findNum(int[] nums, int start, int target) {
        int l = start, r = nums.length - 1;
        while(l <= r) {
            int mid = (r - l) / 2 + l;
            if(nums[mid] < target) {
                l = mid + 1;
            } else {
                r = mid - 1;
            }
        }
        return l;
    }
}

完!😄

  • 13
    点赞
  • 15
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值