ICode9

精准搜索请尝试: 精确搜索
首页 > 其他分享> 文章详细

4.二分搜索

2022-05-17 21:33:47  阅读:27  来源: 互联网

标签:二分 right target nums int 搜索 left


二分搜索

最常见的二分查找场景:寻找一个数、寻找左侧边界、寻找右侧边界。分析细节差异和原因

仅局限于【在有序数组中搜索指定元素】

//寻找一个数
//搜索一个数,如果存在,返回其索引,否则返回-1
int binarySearch(int[] nums,int target){
	int left = 0,right = nums.length - 1;//两端闭区间[left,right],每次进行搜索的区间
	while(left <= right){//搜索区间为空的时候应该终止,意味着没得找了,等于没找到
		int mid = left + (right - left) / 2;//防止溢出
		if(nums[mid] == target){
			return mid;//找到目标值时候,终止搜索
		}else if(nums[mid] < target){
			left = mid + 1;
		}else if(nums[mid] > target){
			right = mid - 1;
		}
	}
	return -1;//没有找到,循环终止,返回-1
}

//寻找左侧边界的二分搜索
int left_bound(int[] nums, int target) {
	int left = 0, right = nums.length - 1;
	while(left <= right){
		int mid = left + (right - left) / 2;
		if(nums[mid] < target){
			left = mid + 1;
		}else if(nums[mid] > target) {
			right = mid - 1;
		}else if(nums[mid] == target) {
			right = mid - 1;//收紧右侧边界,以锁定左侧边界
		}
	}
	//检查left 越界情况
	if(left >= nums.length || nums[left] != target){
		return -1;	
	}
	return left;
}

//寻找右侧边界的二分搜素
int right_bound(int[] nums, int target) {
	int left = 0, right = nums.length - 1;
	while(left <= right){
		int mid = left + (right - left) / 2;
		if(nums[mid] < target){
			left = mid + 1;
		}else if(nums[mid] > target) {
			right = mid - 1;
		}else if(nums[mid] == target) {
			left = mid + 1;
		}
	}
 	//检查right 出界情况
	if(right < 0 || nums[right] != target){
		return -1;	
	}
	return right;
}

1.while 循环条件是<= ,而不是<

while循环的条件是 ≤,因为初始化right的赋值是 nums.length - 1,即最后一个元素的索引,而不是 nums.length

right不同的赋值,二者出现在不同功能的二分查找,区别是:前者相当于两端闭区间[left,right],后者相当于左闭右开[left,right),因为索引大小为nums.length是越界的

while(left ≤ right)终止条件是 left = right + 1,即[right + 1,right],这时候区间为空

while(left < right)终止条件是 left == right ,即[right,right],例如[2,2],此时区间非空,还有一个数字2,索引2没有被搜索。

2.left = mid + 1, right = mid - 1

明确搜索区间,此算法两端都闭[left,right],mid已经搜索过,应该从搜索区间去除

即去搜索[left, mid - 1]或者[mid + 1, right]

3.缺陷

存在局限性,可以返回索引,但是无法探知索引边界,例如[1,2,2,2,3],无法探知左右侧边界1,3。

一个想法是 找到一个target,然后向左或向右线性搜索,可以但是不好,难以保证二分查找对数级的复杂度。

[左侧边界原思路](D:\Moomin\坚果云\算法框架\1.数组链表\4.1 左侧边界原思路.md)

二分搜索算法应用

具体的算法问题没有那么直接,可能很难看出这个问题能够用到二分搜索

在具体的算法问题中,常用到的是【搜索左区间】和【搜索右区间】

一般求最值,过程必然是搜索一个边界的过程

image-20220509231709876

image-20220509231716848

所有能够抽象出上述图像的问题,都可以使用二分搜索解决

二分搜索问题的泛化

首先,从题目中抽象出一个自变量x,一个关于 x 的函数 f(x),以及一个目标值 target。同时,三个量满足以下条件:

  1. f(x) 必须是在 x 上的单调函数(增/减都可)
  2. 题目是让你 设计满足 约束条件 f(x) == target 时的 x 的值
//把数组中元素的索引认为是自变量 X 
int f(int x, int[] nums){
	return nums[x];	
}

函数f就是在访问数组 nums,升序排列的,所以函数f(x) 就是在 x 上单调递增

计算元素 target 的最左侧索引,相当于【满足f(x) == target 的 x 的最小值 是多少】

image-20220509231724386

遇到算法问题,抽象成函数图,对它运用二分搜索算法

解决具体的算法问题,以下框架着手思考

//函数 f 是关于自变量 x 的单调函数
int f(int x){
	//...	
}
//主函数,在 f(x) == target 的约束下求 x 的最值
int solution(int[] nums, int target){
	if(nums.length == 0) return -1;
	int left = ...;//自变量 x 最小值是多少
	int right = ...;//自变量 x 最大值是多少
	while(left < right){
		int mid = left + (right - left) / 2;
		if(f(mid) == target){
			//求左边界还是右边界
		}else if(f(mid) < target){
			//怎么让 f(x)大一点
		}else if(f(mid) > target){
			//怎么让 f(x)小一点
		}
	}
	return left;
}
  1. 确定x,f(x),target 分别是什么,并写出函数 f 的代码
  2. 找到 x 的取值范围作为 二分搜索的搜索区间,初始化left 和right 变量
  3. 根据题目的要求,确定应该使用搜索左侧还是搜索右侧的二分搜索算法

爱吃香蕉的珂珂

1.确定x, f(x), target分别是什么,并写出函数f的代码

二分搜索的本质就是在 搜索自变量。题目让求什么,就把什么设为自变量,珂珂吃香蕉的速度 就是自变量 x

若吃香蕉的速度为x根/小时,则需要f(x)小时吃完所有香蕉。

target 就是吃香蕉的限制时间,对f(x)返回值的最大约束

//速度为 x 时,需要 f(x) 小时吃完所有香蕉,递减
int f(int[] piles, int x){
	int hours = 0;
	for(int i = 0; i < piles.length; i++){
		hours += piles[i] / x;
		if(piles[i] % x > 0){
			hours++;
		}
	}
	return hours;
}

2.找到x的取值范围作为二分搜索的搜索区间,初始化leftright变量

最小速度是1,最大速度是piles数组中元素最大值,两种选择

for循环遍历piles数组计算最大值,或者看题目约束

public int minEatingSpeed(int[] piles,int H){
	int left = 1;
	int right = 1000000000+1;
}

3.根据题目的要求,确定应该使用搜索左侧还是搜索右侧的二分搜索算法,写出解法代码

image-20220509231736447

我们确定了自变量x是吃香蕉的速度,f(x)是单调递减的函数,target就是吃香蕉的时间限制H,题目要我们计算最小速度,也就是x要尽可能小

就是搜索左侧边界,注意是单调递减

public int minEatingSpeed(int[] piles, int H) {
        int left = 1;
        int right = 1000000000 ;
        while(left <=  right){
            int mid = left + (right - left) / 2;
            if(f(piles,mid) == H){
                right = mid -1;//收缩右侧边界
            }else if (f(piles,mid) < H){
                right = mid -1;//需要让 f(x) 返回值大一些,x往左走
            }else if(f(piles,mid) > H){
                left = mid + 1;//需要让 f(x)返回值小一些,x往右走
            }
        }
        return left;
    }
    int f(int[] piles,int x){
        int hours = 0;
        for(int i = 0; i <piles.length; i++){
            hours += piles[i] / x;
            if(piles[i] % x > 0){
                hours++;
            }
        }
        return hours;
    }

运送货物

1、确定x, f(x), target分别是什么,并写出函数f的代码

题目问什么,什么就是自变量,船的最低运载能力

运输天数和运载能力成反比,f(x)计算 x 的运载能力下需要的运输天数

//当运载能力为 x 时,需要f(x) 天运完所有货物
int f(int[] weights,int x){
	int days = 0;
	for(int i = 0; i < weights.length;){
		int cap = x;
		while(i < weights.length){
			if(cap < weights[i]) break;
			else cap -= weights[i];
			i++;
		}
		days++;
	}
	return days;
}

target 就是运输天数 D ,在f(x) == D的约束下,算出船的最小载重

2、找到x的取值范围作为二分搜索的搜索区间,初始化leftright变量

船的最小载重应该是 weights 数组中元素的最大值,因为每次至少得装一键货物

最大载重就是 weights数组所有元素之和,也就是一次把所有货物装走

public int shipWithinDays(int[] weights,int days){
	int left = 0;
	int right = 0;
	for(int w : weigths){
		left = Math.max(left, w);
		right += w;
	}
}

3、需要根据题目的要求,确定应该使用搜索左侧还是搜索右侧的二分搜索算法,写出解法代码

image-20220509231745984

x尽可能小

public int shipWithinDays(int[] weights, int days) {
        int left=0,right = 0;
        for(int w : weights){
            left = Math.max(left,w);//最大值
            right += w;//所有重量和
        }
        while(left <= right){
            int mid = left + (right - left) / 2;
            if(f(weights,mid) <= days){
                right = mid - 1;
            }else{
                left = mid + 1;
            }
        }
        return left;
    }
    int f(int[] weights,int x){
        int days = 0;
        for(int i = 0; i<weights.length;){
						//尽可能多装货物?
            int cap = x;//当前载重,每天都是用最新的最低载重
            while(i<weights.length){
                if(cap < weights[i]) break;//载重小于货物重量,跳出循环,不装此货
                else cap -= weights[i];//载重大于货物重量,更新此轮,即当天的剩余载重,看还能装哪些货
                i++;
            }
            days++;//货物装载度过的,每一天
        }
        return days;
        }

总结

题目中存在单调关系,就可以尝试使用二分搜索的思路来解决,通过分析和画图,写出最终代码

标签:二分,right,target,nums,int,搜索,left
来源: https://www.cnblogs.com/autumnmoonming/p/16282436.html

专注分享技术,共同学习,共同进步。侵权联系[admin#icode9.com]

Copyright (C)ICode9.com, All Rights Reserved.

ICode9版权所有