二分查找的另一种题目:不是对数组下标进行二分,而是对数据的取值区间进行二分。
(可以对数据的取值进行二分,然后遍历数组判断哪一半取值的元素数目多出,因此继续在该范围内查找。)时间换空间
二分搜索法思想:
通过不断缩小解可能存在的范围,从而求得问题的最优解。
二分查找是基于有序序列的查找算法。每次查找当前区间的中间位置的元素,判断其与待查找元素的大小。然后,根据大小移动区间的左右端点。
二分查找的好处在于每次可以去除掉一半的元素,使其时间复杂度为O(logn)。
二分查找的关键点在于对于寻找区间的定义,在整个循环中应该保持区间的定义不变原则:
#include
#include
using namespace std;
// 左闭右闭区间[left,right] 寻找目标值
int binarySearch(int A[],int n,int x){
int left=0;
int right=n-1;
while(left<=right){
//int mid=(left+right)/2; //可能存在上溢问题
int mid=left+(right-left)/2;
if(A[mid]>x){
right=mid-1;
}
else if(A[mid]<x){
left=mid+1;
}
else{
return mid;
}
}
return -1;
}
//左闭右开区间[left,right) 寻找目标值
int binarySearch(int A[],int n,int x){
int left=0;
int right=n;
while(left<right){
//int mid=(left+right)/2; //可能存在上溢问题
int mid=left+(right-left)/2;
if(A[mid]>x){
right=mid;
}
else if(A[mid]<x){
left=mid+1;
}
else{
return mid;
}
}
return -1;
}
该方法与二分查找某元素其实是一样的处理方式,只是在没有找到元素时返回值不同:
不同情况下的返回值手推:
对于在有序序列中查找某元素插入位置的题目,对于插入位置的所有可能情况如下:
无非就是四种情况:
- 目标值在数组所有元素之前
- 目标值等于数组中某一个元素
- 目标值插入数组中两个元素之间的位置
- 目标值在数组所有元素之后
因此,可以总结得到:
- 左闭右闭情况:最终返回值为right+1 / left
- 左闭右开情况:最终返回值为right / left
▶ 例:给定一个排序数组和一个目标值,在数组中找到目标值,并返回其索引。如果目标值不存在于数组中,返回它将会被按顺序插入的位置。
# 左闭右闭解法
class Solution(object):
def searchInsert(self, nums, target):
"""
:type nums: List[int]
:type target: int
:rtype: int
"""
# 左闭右闭区间
left,right=0,len(nums)-1
while left<=right:
mid=left+(right-left)/2
if nums[mid]==target:
return mid
elif nums[mid]<target:
left=mid+1
else:
right=mid-1
return right+1
# 左闭右开解法
class Solution:
def searchInsert(self, nums: List[int], target: int) -> int:
# 左闭右开区间
left,right=0,len(nums)
while left<right:
mid=int(left+(right-left)/2)
if nums[mid]==target:
return mid
elif nums[mid]<target:
left=mid+1
else:
right=mid
return right
寻找序列中第一个满足某条件的元素位置。
例如,寻找序列中第一个>=target的元素的位置:
此时可以将>条件和=条件放在一起处理:
总结:
▶ 例:给你一个按照非递减顺序排列的整数数组 nums,和一个目标值 target。请你找出给定目标值在数组中的开始位置和结束位置。如果数组中不存在目标值 target,返回 [-1, -1]。
处理思想:
在序列A中查找元素x所在的区间:
- 1、区间左端点:查找A中第一个大于等于x的元素的位置
- 2、区间右端点:查找A中第一个大于x元素的位置
=查找A中第一个大于等于x+1的元素的位置-1(如此可以将1、2的处理函数搞成一样的形式)
注意对于数组不存在target时返回[-1,-1]的处理:
一共就三种情况:
- 情况一:target 在数组范围的右边或者左边,例如数组{3, 4, 5},target为2或者数组{3, 4, 5},target为6,此时应该返回{-1, -1}
- 情况二:target 在数组范围中,且数组中不存在target,例如数组{3,6,7},target为5,此时应该返回{-1, -1}
- 情况三:target 在数组范围中,且数组中存在target,例如数组{3,6,7},target为6,此时应该返回{1, 1}
class Solution:
def searchRange(self, nums: List[int], target: int) -> List[int]:
# 区间左端点:数组中第一次出现x的位置=第一个>=的元素的位置
# 区间右端点:数组中最后一次出现x的位置=第一个>x的元素位置=第一个>=x+1的元素的位置-1
def search(nums:List[int],target:int)->int:
# 左闭右闭区间实现
left,right=0,len(nums)-1
while left<=right:
mid=(left+right)//2
if(nums[mid]>=target):
right=mid-1
else:
left=mid+1
return left
def search(nums: List[int], target: int) -> int:
# 左闭右开区间实现
left,right=0,len(nums)
while left<right:
mid=left+(right-left)//2
if nums[mid]>=target:
right=mid
else:
left=mid+1
return right
indL=search(nums,target)
indR=search(nums,target+1)-1
if indL==len(nums) or nums[indL]!=target:
return [-1,-1]
else:
return [indL,indR]
此类问题可以看做是一类问题的特例:
给定一个定义在[L,R]上的单调函数f(x),求方程f(x)=y的根。
!!注意求解结果为浮点数时,需要注意一下几点:
结果为浮点数时,计算模板如下:
//下面的模板以f(x)函数递增为例
const double eps=1e-5;
double calF(double x){
return ...
}
double solve(double left,double right,double y){
double mid;
while(right-left>eps){
mid=left+(right-left)/2;
double fMid=calF(mid);
if(fMid>y){
left=mid;
}
else{
right=mid;
}
}
return mid;
}
▶ 例1:计算\sqrt 2的值
定义函数 f(x)=x^2,限制定义域为[1,2]
该题目即转化为 求解函数f(x)=2的根,其中x的取值范围为[1,2]
所以就是寻找一个数x,使其函数值f(x)无限逼近2.注意点:
- 浮点数的比较要使用极小数进行比较,不可以使用==。
- 解为浮点数时,
- 循环的条件变成了:right-left>eps
- left以及right边界的变化不能再进行-1了
#include
#include
//需要注意对于浮点类型的比较,不可以使用==,要使用一个极小数
const double eps=1e-5;
double calF(double x){
return x*x;
}
double solve(double left,double right,double y){ //最终的目的其实是找一个极小的区间
double mid;
while(right-left>eps){
mid=left+(right-left)/2;
double calMid=calF(mid);
if(calMid<y){
left=mid;
}
else{
right=mid;
}
}
return mid;
}
int main(){
double res=solve(1.0,2.0,2.0);
printf("根号2的值为:%f",res);
return 0;
}
▶ 例2:半圆储水问题
其实就是构造函数,f(h)=S1/S2
求解 f(h)=r 的解。
#include
#include
using namespace std;
const double eps=1e-5;
const double PI=3.14;
double calF(double h,double R){
double a=acos((R-h)/R);
double L=sqrt(R*R-pow((R-h),2));
double S1=PI*R*R/4;
double S2=a*R*R/2-(R-h)*L/2;
return S2/S1;
}
double solve(double y,double R){
double mid;
double left=0,right=R;
while(right-left>eps){
mid=left+(right-left)/2;
double Fmid=calF(mid,R);
if(Fmid<y){
left=mid;
}
else{
right=mid;
}
}
return mid;
}
int main(){
double R,r;
scanf("%lf %lf",&R,&r);
printf("%f,%f\n",R,r);
double res=solve(r,R);
printf("%f",res);
return 0;
}
二分查找一定是对有序的数组才能进行的。 一般是升序的数组。
二分查找易错点:
(1)注意c++中整数上溢问题
(2)注意控制循环不变量
(3)python3实现中,要使用//运算符整除
二分查找元素,循环不变量
二分查找非常容易写错循环条件以及判断条件,
必须要记住一点,到底是进行开区间判断还是闭区间判断。
以闭区间判断为例:
要判断target与[left,right]之间的关系
循环条件while(left<=right)-------这种情况下,区间才是有意义的
https://leetcode.cn/problems/binary-search/
https://leetcode.cn/problems/search-insert-position/
https://leetcode.cn/problems/find-first-and-last-position-of-element-in-sorted-array/
给你一个按照非递减顺序排列的整数数组 nums,和一个目标值 target。请你找出给定目标值在数组中的开始位置和结束位置。
如果数组中不存在目标值 target,返回 [-1, -1]。你必须设计并实现时间复杂度为 O(log n) 的算法解决此问题。
https://leetcode.cn/problems/sqrtx/
给你一个非负整数 x ,计算并返回 x 的 算术平方根 。
由于返回类型是整数,结果只保留整数部分 ,小数部分将被舍去 。
注意:不允许使用任何内置指数函数和算符,例如 pow(x, 0.5) 或者 x ** 0.5 。
# 如果是整数解法如下:
class Solution:
def mySqrt(self, x: int) -> int:
# 采用二分法,求解k^2=x的k的取值
# 左闭右闭
left,right=0,x
while left<=right:
mid=left+(right-left)//2
if mid**2==x:
return mid
elif mid**2<x:
left=mid+1
else:
right=mid-1
return right !!注意此处没有返回right+1,因为right+1相当于是元素要插入的位置,因此其原本的元素一定是mid**2>target的,本题目要求是要保留整数
https://leetcode.cn/problems/valid-perfect-square/
给定一个 正整数 num ,编写一个函数,如果 num 是一个完全平方数,则返回 true ,否则返回 false 。
class Solution:
def isPerfectSquare(self, num: int) -> bool:
left,right=0,num
while left<=right:
mid=left+(right-left)//2
if mid**2==num:
return True
elif mid**2<num:
left=mid+1
else:
right=mid-1
return False
题意理解:
给定n根绳子及其长度,希望将其分割成k条长度一样的绳子,求解分割出的绳子的最大长度L
首先明确,L越大,分割出的绳子的数目越少,因此是一个单调问题
二分解决:
def judge(lengths, len, k):
"""
判断是否满足条件:即 切割出k条len长度的绳子
:param lengths:
:param len:
:param k:
:return:
"""
num = 0
for i in lengths:
num += i // len
if num < k:
return True
else:
return False
# 小数形式
def binarySearch(lenghts, k):
"""
二分求解切割绳子的最大长度
:param lenghts: 输入的绳子的长度list
:param k: 切割处k条长度相同的绳子
:return:
"""
left, right = 0, max(lenghts)
eps = 1e-3
while right - left > eps:
mid = left + (right - left) / 2
if judge(lenghts, mid, k):
left = mid
else:
right = mid
return mid
# 整数形式
def binarySearch(lenghts, k):
"""
二分求解切割绳子的最大长度
:param lenghts: 输入的绳子的长度list
:param k: 切割处k条长度相同的绳子
:return:
"""
left, right = 0, max(lenghts)
while left<=right:
mid = left + (right - left) // 2
if judge(lenghts, mid, k):
right = mid-1
else:
left = mid+1
return right
# 按间距中的绿色按钮以运行脚本。
if __name__ == '__main__':
lengths = [10, 24, 15]
k = 7
print(binarySearch(lengths, k))