01 二分查找

对应题目类型

  • 题目中给出数组是有序数组,首先可以考虑能否使用二分法;
  • 同时题目还强调数组中无重复元素,因为一旦有重复元素,使用二分查找法返回的元素下标可能不是唯一的;
  1. 有序数组查找某元素
  2. 有序数组寻找元素插入位置
  3. 有序数组寻找第一个(最后一个,是否存在)满足某条件的元素——求元素区间
  4. 在某一区间上求解单调函数的根

二分查找的另一种题目:不是对数组下标进行二分,而是对数据的取值区间进行二分。

  1. 给定一个数组,长度为n+1,其中存放的元素取值范围是1~n,要求找出一个1重复元素。

(可以对数据的取值进行二分,然后遍历数组判断哪一半取值的元素数目多出,因此继续在该范围内查找。)时间换空间

解题思想

二分搜索法思想:
通过不断缩小解可能存在的范围,从而求得问题的最优解。

二分查找某元素

二分查找是基于有序序列的查找算法。每次查找当前区间的中间位置的元素,判断其与待查找元素的大小。然后,根据大小移动区间的左右端点。
二分查找的好处在于每次可以去除掉一半的元素,使其时间复杂度为O(logn)。

  • 暴力解法时间复杂度:O(n)
  • 二分法时间复杂度:O(logn)

二分查找的关键点在于对于寻找区间的定义,在整个循环中应该保持区间的定义不变原则:

  • 左闭右闭区间
    • right定义:nums.size()-1
    • 循环条件:left<=right
    • right修改:if nums[mid]>target: right=mid-1
  • 左闭右开区间
    • right定义:nums.size()
    • 循环条件:left
    • right修改:if nums[mid]>target: right=mid
#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;
}

二分查找某元素的插入位置

该方法与二分查找某元素其实是一样的处理方式,只是在没有找到元素时返回值不同:

  • “二分查找某元素”:
    • 找到该元素:返回该元素所在的位置
    • 没有找到该元素:返回-1(具体看题目要求)
  • “二分查找某元素的插入位置”:
    • 找到该元素:返回该元素所在的位置,其实该位置就是插入位置
    • 没有找到该元素:
      • 左闭右闭:返回right+1 / left
      • 左闭右开:返回right / left

不同情况下的返回值手推:
对于在有序序列中查找某元素插入位置的题目,对于插入位置的所有可能情况如下:
image.png
无非就是四种情况:

  • 目标值在数组所有元素之前
  • 目标值等于数组中某一个元素
  • 目标值插入数组中两个元素之间的位置
  • 目标值在数组所有元素之后

89299240d90d1d6d6f04479b62fd2d5.jpg

因此,可以总结得到:

  • 左闭右闭情况:最终返回值为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的元素的位置:
此时可以将>条件和=条件放在一起处理:

  • 左闭右闭:>=target :right=mid-1
  • 左闭右开:>=targert: right=mid

总结:

  • 寻找序列中第一个满足某条件的元素位置
  • 寻找序列中最后一个满足某条件的元素位置:可以先求第一个满足(!C)条件的元素位置,然后再-1
  • 寻找数列中是否存在满足条件的元素:利用二分搜索即可

▶ 例:给你一个按照非递减顺序排列的整数数组 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的根。

!!注意求解结果为浮点数时,需要注意一下几点:

  • 浮点数的大小比较使用极小数的形式
  • while循环的条件:right-left>eps
  • 边界的处理不能使用+1,-1的形式了
  • 最后的返回值:return mid

结果为浮点数时,计算模板如下:

//下面的模板以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)-------这种情况下,区间才是有意义的

题目汇总

704. 二分查找

https://leetcode.cn/problems/binary-search/

35.搜索插入位置

https://leetcode.cn/problems/search-insert-position/

34.在排序数组中查找元素的第一个和最后一个位置

https://leetcode.cn/problems/find-first-and-last-position-of-element-in-sorted-array/
给你一个按照非递减顺序排列的整数数组 nums,和一个目标值 target。请你找出给定目标值在数组中的开始位置和结束位置。
如果数组中不存在目标值 target,返回 [-1, -1]。你必须设计并实现时间复杂度为 O(log n) 的算法解决此问题。

69.x 的平方根

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的,本题目要求是要保留整数

367.有效的完全平方数

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

木棒分割问题

01 二分查找_第1张图片
题意理解:
给定n根绳子及其长度,希望将其分割成k条长度一样的绳子,求解分割出的绳子的最大长度L

首先明确,L越大,分割出的绳子的数目越少,因此是一个单调问题
二分解决:

  • 求解区间: 切割出的绳子的长度 L∈[0 , maxlen]
  • 求解条件:切割出的绳子的数目K>=k
  • 题目转化:在区间[0 , maxlen]上求解最后一个满足条件的L==在区间[0 , maxlen]上求解第一个满足!条件的L-1
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))

你可能感兴趣的:(算法,数据结构)