二分查找算法
1. 二分查找
- 适用前提:待查找序列有序;
- 时间复杂度:O(log2n);
代码框架:
int binarySearch(int[] nums, int target) {
int left = 0, right = ...;
while(...) {
int mid = left + (right - left) / 2;
if (nums[mid] == target) {
...
} else if (nums[mid] < target) {
left = ...
} else if (nums[mid] > target) {
right = ...
}
}
return ...;
}
无论使用二分搜索是查找单个元素还是搜索边界,最本质的点在于保证搜索区间的合法性
1.1 二分搜索单个元素
- 查找结果:1)没找到返回-1;2)找到返回其对应位置索引;
- 左右指针分别指向数组最左端、最右端位置,初始搜索区间为[left,right];
- 在while (left<=right)时,搜索终止条件为left>right,此时搜索区间为[left,right]=[right+1,right],区间为空,说明找不到;
- 在while (left<right)时,搜索终止条件为left=right,此时搜索区间为[left,right]=[right,right),区间为空,说明找不到;
代码框架:
public static int binarySearch(int[] arr,int target){
int left=0,right=arr.length-1;
while (left<=right){
int mid=left+(right-left)/2;
if (arr[mid]==target){
return mid;
}else if (arr[mid]<target){
left=mid+1;
}else if (arr[mid]>target){
right=mid-1;
}
}
return -1;
}
// 递归方式
public static int binarySearch(int[] arr,int target,int left,int right){
if (left>right){
return -1;
}
int mid=left+(right-left)/2;
if (arr[mid]==target){
return mid;
}else if (arr[mid]<target){
return binarySearch(arr,target,mid+1,right);
}else if (arr[mid]>target){
return binarySearch(arr,target,left,mid-1);
}
return -1;
}
1.2 二分搜索左侧边界
- 序列中某个元素可能有连续多个,此方法可用于确定最左侧的该元素;
- target可能极大,导致left一直右移,可能越界,所以最后需要对l是否越界进行判断;
- [l,r]=[l,mid-1],此时大概率arr[mid]=target,当l>r导致搜索终止时,说明l=mid;
代码框架:
// 查找左边界
public static int searchLeft(int[] arr,int target){
int l=0,r=arr.length-1; // [l,r]
// 终止条件l>r
while (l<=r){
int mid= l+(r-l)/2;
if (arr[mid]==target){
// 收缩右边界,锁定左边界
r=mid-1;
}else if (arr[mid]<target){ // mid+-1取决于当前模式是否已经搜索过mid自身
l=mid+1;
}else if (arr[mid]>target){
r=mid-1;
}
}
// target可能极大,导致left一直右移,可能越界
if (l==arr.length){
return -1;
}
// [l,r]=[l,mid-1],此时大概arr[mid]=target,当l>r导致搜索终止时,说明l=mid
return arr[l]==target?l:-1;
}
1.2 二分搜索右侧边界
- 序列中某个元素可能有连续多个,此方法可用于确定最右侧的该元素;
- target可能极小,导致right一直左移,可能越界,此时r<0,而l>r -> l=r+1 -> l-1=r,所以r<0可写为l-1<0;
- [l,r]=[mid+1,r],此时大概arr[mid]=target,当l>r导致搜索终止时,说明l-1=mid;
代码框架:
// 查找右边界
public static int searchRight(int[] arr,int target){
int l=0,r=arr.length-1; // [l,r]
// 终止条件l>r
while (l<=r){
int mid= l+(r-l)/2;
if (arr[mid]==target){
// 收缩左边界,锁定右边界
l=mid+1;
}else if (arr[mid]<target){ // mid+-1取决于当前模式是否已经搜索过mid自身
l=mid+1;
}else if (arr[mid]>target){
r=mid-1;
}
}
// target可能极小,导致right一直左移,可能越界,此时r<0,而l>r -> l=r+1 -> l-1=r,所以r<0可写为l-1<0
if (l-1<0){
return -1;
}
// [l,r]=[mid+1,r],此时大概arr[mid]=target,当l>r导致搜索终止时,说明l-1=mid
return arr[l-1]==target?l-1:-1;
}
2.力扣题
2.1 力扣704. 二分查找
给定一个 n 个元素有序的(升序)整型数组 nums 和一个目标值 target ,写一个函数搜索 nums 中的 target,如果目标值存在返回下标,否则返回 -1。链接:https://leetcode.cn/problems/binary-search
解题思路:这道题是最原始的在有序数组中查找单个元素的问题,直接使用代码框架即可;
class Solution {
// 二分查找
public int search(int[] nums, int target) {
int l=0,r=nums.length-1; // 搜索区间两端为闭区间
while(l<=r){ // 加等号
int mid=l+(r-l)/2;
if(nums[mid]==target){
return mid;
}else if(nums[mid]<target){
l=mid+1;
}else if(nums[mid]>target){
r=mid-1;
}
}
return -1;
}
// 线性查找
// public int search(int[] nums, int target) {
// for(int i=0;i<nums.length;i++){
// if(target==nums[i]){
// return i;
// }
// }
// return -1;
// }
}
2.2 剑指 Offer II 073. 狒狒吃香蕉
狒狒喜欢吃香蕉。这里有 n 堆香蕉,第 i 堆中有 piles[i] 根香蕉。警卫已经离开了,将在 h 小时后回来。狒狒可以决定她吃香蕉的速度 k (单位:根/小时)。每个小时,她将会选择一堆香蕉,从中吃掉 k 根。如果这堆香蕉少于 k 根,她将吃掉这堆的所有香蕉,然后这一小时内不会再吃更多的香蕉,下一个小时才会开始吃另一堆的香蕉。 狒狒喜欢慢慢吃,但仍然想在警卫回来前吃掉所有的香蕉。返回她可以在 h 小时内吃掉所有香蕉的最小速度 k(k 为整数)。
链接:https://leetcode.cn/problems/nZZqjQ/
分析:
- 不考虑时间限制,如果速度为所有香蕉堆中香蕉数量最大的一个,比如maxK,此时起码可以在时间为数组长度时吃完所有香蕉,即h=piles.length;
- 此时,最小速度为1,最大速度为maxK,而区间[1,maxK]自然有序,所以可以考虑使用二分搜索算法求解合适的速度;
- 合适的速度不唯一,只要大于等于minSpeed的速度均可完成目标,所以本题要求的最小速度其实,是通过二分搜索算法求解左边界问题;
解题思路:
- 确定最大香蕉堆的香蕉数量maxPile,以此作为最大速度,搜索区间为[1,maxPile];
- 在此区间上进行二分搜索,以当前值mid作为速度speed进行尝试,确定最小速度;
如何确定是否当前速度可实现目标:我们知道题目要求狒狒在一个小时内最多只吃speed个香蕉,如果当前香蕉堆的香蕉数量piles[i]>speed,则将当前堆剩余香蕉等到下一个小时再吃,所以可通过以下方式计算当前堆全部吃完的总耗时:piles[i]/speed+(piles[i]%speed==0?0:1);
class Solution {
public int minEatingSpeed(int[] piles, int h) {
int maxPile=getMaxPile(piles);
int l=1,r=maxPile; // 搜索区间[l,r]
while (l<=r){
int mid=l+(r-l)/2;
if (restBanana(piles,h,mid)){ // 可以吃完,说明速度大于等于最小速度
r=mid-1;
}else if (!restBanana(piles,h,mid)){ // 吃不完,说明速度小于最小速度
l=mid+1;
}
}
if (l>maxPile){
return -1;
}
return l;
}
// 以当前速度在h小时内是否可以吃完
private boolean restBanana(int[] piles, int h, int speed) {
int time=0;
for (int pile:piles){
time+=(pile/speed);
time+=(pile%speed==0?0:1);
}
return time<=h;
}
// 确定最大香蕉堆数量,以此作为有可能的最大速度
public int getMaxPile(int[] piles){
int max=piles[0];
for (int i=1;i<piles.length;i++){
max=Math.max(max,piles[i]);
}
return max;
}
}
代码框架,参考labuladong,https://labuladong.gitee.io/algo