【算法】二分查找总结篇
目录
二分查找
一、核心思想
二、区间选择与模板
三、常见题型与处理技巧
四、解题步骤归纳
本文是对【算法之路】专栏中的【手撕二分查找】、【二分查找上】、【二分查找下】进行总结。希望看完本篇后,大家能够独立解决二分查找的题目
二分查找
一、核心思想
二分查找通过每次缩小一半的搜索范围,将时间复杂度降至O(log N)。关键在于:
1.有序性:数据(或部分)具有某种有序性或单调性
2.边界调整:根据中间值与目标条件的关系,调整左右边界
二、区间选择与模板
根据问题需求选择区间形式,常见两种:
1.左闭右闭 [left,right]--【相错终止】
初始化:left=0,right=nums.length-1
循环条件:while(left<=right)
边界调整:left=mid+1或right=mid-1
适用场景:标准查找、确定存在性
2.左闭右开 [left,right)--【相遇终止】
初始化:left=0,right=nums.length
循环条件:while(left<right)
边界调整:left=mid+1或right=mid
适用场景:寻找边界(如第一个/最后一个位置)--【相遇时即为返回结果】
三、常见题型与处理技巧
1.标准二分查找
目标:找到特定元素或确认存在性
模板:
//左闭右闭
public static int binary_search(int[] nums,int target){
int left=0;
int right=nums.length-1;
while(left<=right){
int mid=left+(right-left)/2;
if(nums[mid]==target){
return mid;
}else if(nums[mid]>target){
right=mid-1;
}else{
left=mid+1;
}
}
}
2.寻找左边界
目标:找到第一个等于目标的索引
模板:
//左闭右开--相遇终止
//寻找左边界
//(如:1 1 2 2 3 3 4 5 6)--返回第一个1/2/3
public static int binary_search(int[] nums,int target){
int left=0;
int right=nums.length;
while(left<right){
int mid=left+(right-left)/2;
if(nums[mid]>=target){
right=mid;
}else{
left=mid+1;
}
}
return left;
}
3.寻找右边界
目标:找到最后一个等于目标的索引
模板:
//左闭右开--相遇终止
//寻找右边界
//(如:1 1 2 2 3 3 4 5 6)--返回第二个1/2/3
public static int binary_search(int[] nums,int target){
int left=0;
int right=nums.length;
while(left<right){
int mid=left+(right-left)/2;
if(nums[mid]<=target){
left=mid+1;
}else{
right=mid;
}
}
//left与right相遇于第一个大于target的位置
//所以left-1即为所求的右边界
return nums[left-1]==target?left-1:-1;
}
4.极值问题(如峰值、旋转点)
关键:利用局部有序性缩小范围
示例(寻找峰值--162. 寻找峰值 - 力扣(LeetCode)):
//左闭右开--相遇终止
//寻找峰值:如:[1,2,3,1]的元素3
public static int binary_search(int[] nums,int target){
int left=0;
int right=nums.length-1;//注意这里原本应该为nums.length
//但由于数组可能只有一个元素,会导致后续代码有越界。
while(left<right){
int mid=left+(right-left)/2;
if(nums[mid]>nums[mid+1]){
right=mid;
}else{
left=mid+1;
}
}
return left;
}
5.条件判断型(如最小运载能力)
步骤:
·确定搜索范围
·编写check函数验证条件
·二分查找满足条件的最小值
例题:
题目链接:1011. 在 D 天内送达包裹的能力 - 力扣(LeetCode)
题目描述:
传送带上的包裹必须在
days
天内从一个港口运送到另一个港口。传送带上的第
i
个包裹的重量为weights[i]
。每一天,我们都会按给出重量(weights
)的顺序往传送带上装载包裹。我们装载的重量不会超过船的最大运载重量。返回能在
days
天内将传送带上的所有包裹送达的船的最低运载能力。示例 1:
输入:weights = [1,2,3,4,5,6,7,8,9,10], days = 5 输出:15 解释: 船舶最低载重 15 就能够在 5 天内送达所有包裹,如下所示: 第 1 天:1, 2, 3, 4, 5 第 2 天:6, 7 第 3 天:8 第 4 天:9 第 5 天:10 请注意,货物必须按照给定的顺序装运,因此使用载重能力为 14 的船舶并将包装分成 (2, 3, 4, 5), (1, 6, 7), (8), (9), (10) 是不允许的。示例 2:
输入:weights = [3,2,2,4,1,4], days = 3 输出:6 解释: 船舶最低载重 6 就能够在 3 天内送达所有包裹,如下所示: 第 1 天:3, 2 第 2 天:2, 4 第 3 天:1, 4示例 3:
输入:weights = [1,2,3,1,1], days = 4 输出:3 解释: 第 1 天:1 第 2 天:2 第 3 天:3 第 4 天:1, 1提示:
1 <= days <= weights.length <= 5 * 10^4
1 <= weights[i] <= 500
题目分析:
简单来说,我们需要在days天内运送完所有包裹,在满足这一前提下,求出每天的运载包裹总重量的最小值x。也就是当每天运载的包裹总重量小于等于x时,我们可以在days天内运送完所有包裹
那么,如何求出每天运载包裹总重量的最小值呢?
我们将 能够在days天内运送完所有包裹 设为事件A
1.在 【最大的包裹重量 和 所有的包裹重量之和】 的区间内,找到一个值x。--初始化左右边界
2.判断值x能够让事件A成立。--设置check函数。如果成立,则可以继续缩小区间。不成立则返回上一次记录的值x
解题代码:
class Solution {
public static int shipWithinDays(int[] weights, int days) {
//初始化left为最大的包裹重量
//right为总的包裹重量
int left=0;
int right=0;
for(int weight:weights){
left=Math.max(left,weight);
right+=weight;
}
//在[left,right]区间内,找到一个最小值x
//使得每天运载包裹重量小于等于x,在days内能够运送完所有包裹
//使用二分查找
int ans=0;//记录值x
while(left<=right){
int mid=left+(right-left)/2;
if(check(weights,mid,days)){
//如果运载能力为mid时,能够在days天内运送完所有包裹,记录答案,缩减区间
ans=mid;
right=mid-1;
}else{
left=mid+1;
}
}
return ans;
}
private static boolean check(int[] weights, int mid, int days) {
//每一天运送的包裹总重量
int sum=0;
int day=1;//天数
for(int weight:weights){
//如果今天运送包裹重量超过mid,就留到下一天装
if(sum+weight>mid){
day++;
//下一天的运送包裹前,需要清零
sum=0;
}
sum+=weight;
}
//是否小于等于days天
return day<=days;
}
}
四、解题步骤归纳
1.明确问题类型:查找位置、寻找边界、极值或条件判断
2.选择区间形式:左闭右闭/左闭右开
3.确定循环条件:根据区间形式选择<=或者<
4.调整边界:通过比较中间值与目标条件,决定左右如何移动
5.处理结果:检查结果有效性(如越界、是否存在等)