二分查找

leetcode-二分查找

Posted by Fans on September 20, 2021

二分查找

什么是二分查找?二分查找就是不断的找当前序列的中点,通过与二段性比较,如果满足,则解必然在另一半的序列中,将序列一分为二,在另一半中继续搜索,如此往复,因此每次减少一般的搜索空间,达到Ologn的时间复杂度。

二段性

二分查找最重要的就是必须拥有二段性,什么是二段性?必须序列的前一部分满足一个性质,后一部分不满足,因此我们可以通过二分查找找到两个性质转变的中间点。 举个例子,在s=[1,2,3,4,5]中查找一个数字target=4,即我们可以发现在s中有二段性,一部分是大于target的一部分是小于target的,因此我们要找到满足性质转换的那个点,也就是target。 还有比如检验问题,一个序列,在某个点检验失败后,后面的点全部是失败,让我们找第一个失败的点,这个也是有二段性,因为第一个失败点之前都是成功的,后面都是失败的,因此两段性质不同。 对具有二段性质的序列,我们可以使用二分查找,时间复杂度可以达到O(logn)

二分查找代码及逻辑

二分查找的代码可以有很多写法,在这里我们只介绍两种,一种是靠左,一种靠右 靠左的意思是,在选择中点的时候,如果长度是偶数,那么中点选在靠左的位置,这个时候,如果出现重复的target,那么靠左的代码最终选中的是相同target中最左边那个。靠右同理 首先是靠右:

l=0
r=n-1
while l<r:
	mid=(l+r+1)>>1
	if 是否满足二段性中其中一段:
		r=mid-1 
	else:
		l=mid

因为是靠右选的,如果选中的答案不满足性质,那么只考虑mid前面半部分

靠左:

l=0
r=n-1
while l<r:
	mid=(l+r)>>1
	if 是否满足二段性中其中一段:
		l=mid+1 
	else:
		r=mid

因为是靠左选的,如果选中的答案不满足性质,那么只考虑mid右边半部分 那么二分查找代码最终找到的位置为l=r

例题实战

  1. 在一个升序数组nums中寻找target,如果找到返回target的位置,找不到,返回-1 解法:经典的二分查找做法,有序数组,因此考虑二分
    class Solution(object):
     def search(self, nums, target):
         """
         :type nums: List[int]
         :type target: int
         :rtype: int
         """
         l=0
         r=len(nums)-1
         while l<r:
             mid=(l+r+1)>>1
             if nums[mid]<=target:
                 l=mid
             else:
                 r=mid-1
         if nums[r]==target:
             return r
         else:
             return -1
    

    此处我们的判断二段性体现在,选中的中点数字如果大于target,那么target只能出现在中点左边,因此r=mid-1,在左半边继续搜索

  2. 假设你有 n 个版本 [1, 2, …, n],你想找出导致之后所有版本出错的第一个错误的版本。

    你可以通过调用 bool isBadVersion(version) 接口来判断版本号 version 是否在单元测试中出错。实现一个函数来查找第一个错误的版本。你应该尽量减少对调用 API 的次数。

    解法,即一个版本是错的,后面的版本都是错的,让我们找第一个出错的版本,很明显是有二段性,因此考虑二分查找

class Solution:
    def firstBadVersion(self, n):
        """
        :type n: int
        :rtype: int
        """
        l=0
        r=n-1
        while l<r:
            mid=(l+r)>>1
            if isBadVersion(mid+1):
                r=mid
            else:
                l=mid+1
        return r+1

此处我们判断二段的依据在于,我们要找的是第一个错误的版本,因此选择靠左的做法,判断中点是否是错误的,如果是,那么当前点也有可能是第一个错误的,因此从包含该点的左边搜索,因此r=mid 注意:代码中我们用的是0-n-1的下标,因此最后输出的版本要加1,中间判断的版本也要+1

那么为什么不能选择靠右呢? 这里我们考虑,因为要找的是第一个错误的版本,因此在判断是否错误的时候本身的答案是要继续搜索的,如果选择了靠右的代码,那么序列不能被均匀的一分为二,速度会下降,如果选择靠右,那么我们的二段性条件要改变,即要找的点应该是第一个错误的点前面那个点,也就是最后一个成功的点。

  1. 统计一个数字在排序数组中出现的次数。 nums非递减 一个经典的二分做法,这道题就用到了我们的性质了,我们可以考虑靠左靠右同时使用, 分别找到相应的target的最左边和最右边,然后相减可以得到target的出现次数
class Solution(object):
    def search(self, nums, target):
        """
        :type nums: List[int]
        :type target: int
        :rtype: int
        """
        if len(nums)==0:
            return 0
        l=0
        r=len(nums)-1
        while l<r:
            mid=(l+r+1)>>1
            if nums[mid]<=target:
                l=mid
            else:
                r=mid-1
        if nums[r]!=target:
            return 0
        right=r

        l=0
        r=len(nums)-1
        while l<r:
            mid=(l+r)>>1
            if nums[mid]<target:
                l=mid+1
            else:
                r=mid

        return right-l+1

本题没什么可讲的,二段性那个条件可以画图试试就好了,确保分的时候均匀即可

  1. 一个长度为n-1的递增排序数组中的所有数字都是唯一的,并且每个数字都在范围0~n-1之内。在范围0~n-1内的n个数字中有且只有一个数字不在该数组中,请找出这个数字。

解法,这道题很明显也是二分查找,但是二段性条件比较难想,首先我们要知道n-1的长度数组在0-n-1的数据范围且递增,缺了一个数,那么我们很轻易知道,缺失数字前面一部分的数字是和下标相同的,后面一部分和下标不同,因此考虑二分

class Solution(object):
    def missingNumber(self, nums):
        """
        :type nums: List[int]
        :rtype: int
        """
    

        l=0
        r=len(nums)-1
        if nums[0]==1:
            return 0
        
        while l<r:
            mid=(l+r+1)>>1
            if nums[mid]!=mid:
                r=mid-1
            else:
                l=mid
        return nums[r]+1

首先对0缺失要单独考虑,因为最终算到的缺失点是数组边界之外,因此如果0缺失(即第一位是1),那么不管nums再长,直接返回0 我们的二段性条件找的是缺失的数字左边的那个数字,因此如果nums[mid]!=mid的时候,当前中点已经是错误点了,因此考虑r=mid-1,也就是前面一部分。最终我们找到的是缺失的数字左边那个数字,因此需要+1

  1. 整数数组 nums 按升序排列,数组中的值 互不相同 。

    在传递给函数之前,nums 在预先未知的某个下标 k(0 <= k < nums.length)上进行了 旋转,使数组变为 [nums[k], nums[k+1], …, nums[n-1], nums[0], nums[1], …, nums[k-1]](下标 从 0 开始 计数)。例如, [0,1,2,4,5,6,7] 在下标 3 处经旋转后可能变为 [4,5,6,7,0,1,2] 。

    给你 旋转后 的数组 nums 和一个整数 target ,如果 nums 中存在这个目标值 target ,则返回它的下标,否则返回 -1

解法,本题比较难,首先我们需要找到旋转点,因为旋转点两边各自有序 那么首先我们考虑二分去找旋转点,怎么找呢?二段性体现在哪?例如[1,2,3,4,5,6,7]数组在某个点旋转之后变成[5,6,7,1,2,3,4]那么我们发现旋转点是5,但是最终对两段有序分割的点是1,那么我们发现1这个点前面的5,6,7,因为是旋转之后的,所以必定是大于等于5的,同时我们发现1234必定是小于5的(因为原数组中数值各不相同)因此二段性体现在,一部分nums[i]>=nums[0] (此处是已经旋转之后的数组)另一部分相反。因此我们首先二分找到旋转点。代码:

class Solution(object):
    def search(self, nums, target):
        """
        :type nums: List[int]
        :type target: int
        :rtype: int
        """
        
        slen=len(nums)
        if slen == 0:
            return -1
        if slen ==1:
            if nums[0]==target:
                return 0
            else:
                return -1

        l=0
        r=slen-1
        while l<r:
            mid=(l+r+1)>>1
            if nums[mid]>nums[0]:
                l=mid
            else:
                r=mid-1

此时我们的l=r=旋转点前面一个点,也就是数组中最大的元素 因此我们需要对target判断是属于这个点前面还是后面一部分

		if target>=nums[0]:
		            l=0		      
		        else:
		            l=l+1
		            r=slen-1

如果target是大于nums[0]的话,那么属于左边一部分,应该是[0-r],此时的r是最大元素所在位置 否则的话从[l+1,slen-1]搜索 最后一遍二分不用我多说了吧?有序数组中找一个target

		while l<r:
            mid=(l+r+1)>>1
            if nums[mid]<=target:
                l=mid
            else:
                r=mid-1

        if nums[r]==target:
            return r
        else:
            return -1

好了今天的二分就介绍到这里,有问题的话评论一下哦!