二分查找法的介绍与应用

二分法介绍

二分查找对具有指定左索引和右索引的连续序列进行操作。这就是所谓的查找空间。二分查找维护查找空间的左、右和中间指示符,并比较查找目标或将查找条件应用于集合的中间值;如果条件不满足或值不相等,则清除目标不可能存在的那一半,并在剩下的一半上继续查找,直到成功为止。如果查以空的一半结束,则无法满足条件,并且无法找到目标。时间复杂度为O(logn)。

总而言之,二分查找法直观简单,在很多题中二分查找是求解的重要组成部分或者其中的一个步骤。虽然二分法原理清晰,但是很多人使用起来头绪就很乱(道理都懂,但自己就总是出问题)。其根本原因就是对二分法的关键点还理解不够深入,只有理解透彻了才能灵活地运用起来。

1 、二分法的关键点

要理解好二分法需要注意的要三点:
1)初始条件:搜索空间(也叫做解空间)的确定
如常见的有,left = 0, right = len(nums) - 1。

**2)终止条件
left > right 或 left >= right

3) 查找的过程:搜索空间的调整
二分的中心:mid = (left+right)/2
nums[mid] < target时:left = mid + 1
nums[mid] > targe时t:right = mid - 1,常看到还有的是right = mid,这与初始条件的设置有关,也就是left = 0, right = len(nums)。
nums[mid] == target时视情况而定。

2 、二分法的应用

2.1 二分查找

1)二分查找: 在数组nums查找target,如果存在返回所在索引,否则返回-1。
Leetcode 704

下面使用的是二分查找,很好地对应了上面的三个关键点,具体见代码注释。

def binarySearch(nums, target):
    l, r = 0, len(nums)-1   # 初始条件:搜索空间的确定
    while l <= r:       # 这里地终止条件是l <= r,这是因为初始条件的右边设为:r = len(nums)-1
     # 当l,r相加很大时,为了防止整型溢出,这与mid = (r+l) / 2 是一样的
        mid = l + (r - l) // 2   # 二分的中心
        if nums[mid] == target:
            return mid     
        elif nums[mid] > target:
        # 搜索空间的调整:搜索空间变为[l, mid - 1]
            r = mid - 1    
        else:
        # 搜索空间的调整:解空间变为[mid+1, r]
            l = mid + 1    
    return -1

nums = [1, 3, 4,  6, 11, 12, 13]
target = 13
print(binarySearch(nums, target))

与上一题类似的的是Leetcode 74,主要是把二维数组根据索引之间的运算转换为一维数组。

2) 搜索二维矩阵 Leetcode 74

编写一个高效的算法来判断m x n矩阵中,是否存在一个目标值。该矩阵具有如下特性:每行中的整数从左到右按升序排列。每行的第一个整数大于前一行的最后一个整数。

class Solution:
    def searchMatrix(self, matrix, target):
        if matrix is None or len(matrix) == 0:
            return False
        row = len(matrix)
        col = len(matrix[0])
        l = 0
        r = row * col - 1
        while l <= r:
            m = l + (r - l)//2  # 二分的中心
            element = matrix[m//col][m%col]   # 
            if element == target:
                return True
            elif element > target:
                r = m - 1
            else:
                l = m + 1
        return False
matrix = [[1,3,5,7],[10,11,16,20],[23,30,34,60]]
target = 7
print(Solution().searchMatrix(matrix,target))

更多类似地题目还有Leetcode162. 寻找峰值

2.2 左侧边界的⼆分查找

在d数组中二分查找,找到第一个d比target大的数d[i] (也就是左边界),返回d[i]所在索引。
如d = [1, 3, 4, 6, 11, 12, 13],target = 9,返回4。如果d最后的数都不满足条件,那么返回-1。这个索引可以认为是d数组中比target小的元素个数是多少。

def left_bound(d, target):
    l, r = 0, len(d) - 1
    loc = r
    while l <= r:
        mid = (l + r) // 2
        if d[mid] > target:
            r = mid - 1
        elif d[mid] < target:
            l = mid + 1
        elif d[mid] == target:
            r = mid - 1

    if if l >= len(d) and d[l - 1] != target: # 如果d最后的数都不满足条件,那么返回-1
        return -1
    else:
        return l

d = [1, 3, 6, 8, 11]
target = 16
# d = [1, 3, 4,  6, 11, 12, 13]
# target = 9
print("------寻找左侧边界-------")
print(left_bound(d, target))

与之类似地有 Leetcode 35. 搜索插入位置

注意: 当d = [1, 2, 3, 3, 3, 11],target = 3时,返回的是2。那么为什么不是5,这个就是下面的寻找右侧边界问题。

2.3 右侧边界的⼆分查找

def right_bound(d, target):
    l, r = 0, len(d) - 1
    loc = r
    while l <= r:
        mid = (l + r) // 2
        if d[mid] > target:
            r = mid - 1
        elif d[mid] < target:
            l = mid + 1
        elif d[mid] == target:
            # r = mid - 1  
            l = mid + 1   # 改变这里
	# 改变这里
    if l < 0 and d[r-1] != target: # 如果d最后的数都不满足条件,那么返回-1
        return -1
    else:
        return l

# d = [1, 3, 6, 8, 11, 20, 22,22,22]
# target = 22
# d = [1, 3, 4,  6, 11, 12, 13]
# target = 9
d = [1, 2, 3, 3, 3, 11]
target = 3
print("------寻找右侧边界-------")
print(right_bound(d, target))

2.4 最长递增子序列 Leetcode300

方法:贪心算法+二分查找。考虑一个简单的贪心,如果我们要使上升子序列尽可能的长,则我们需要让序列上升得尽可能慢,因此我们希望每次在上升子序列最后加上的那个数尽可能的小。

基于上面的贪心思路,我们维护一个数组d[i] ,表示长度为 i的最长上升子序列的末尾元素的最小值,起始时 d[0]=nums[0]。这里的二分就是寻找左边界的过程。

class Solution:
    def lenghOfLIS(self, nums):
        d = []
        for num in nums:
            # 如果nums[i] > d[-1],则直接加入到d数组末尾,并其长度加1
            if not d or num > d[-1]:
                d.append(num)
            # 否则,在d数组中二分查找,找到第一个比nums[i]大的数d[k],并更新d[k+1]=nums[i]
            # 这里就是相当于寻找左边界问题
            else:
                l, r = 0, len(d) - 1
                while l <= r:
                    mid = (l+r) // 2
                    if d[mid] >= num:
                        # loc = mid
                        r = mid - 1
                    else:
                        l = mid + 1
                d[l] = num
        return len(d) # 返回d的长度就是所求的最长的递增子序列长度

# nums = [0, 8, 4, 12, 2]
nums = [8, 2, 3, 1, 9, 8, 11]
print(Solution().lenghOfLIS(nums))

2. 5 能力检测问题

Leetcode875 爱吃香蕉的珂珂
方法:二分查找
思路:如果珂珂能以 K 的进食速度最终吃完所有的香蕉(在 H 小时内),那么她也可以用更快的速度吃完。当珂珂能以 K 的进食速度吃完香蕉时,我们令 possible(K) 为 true,那么就存在 X 使得当 K >= X 时, possible(K) = True。

class Solution(object):
    def minEatingSpeed(self, piles, h):
        """
        :type piles: List[int]
        :type h: int
        :rtype: int
        """
        # 向上取整,先计算以速度K吃每一堆苹果的时间,然后再把全部时间相加,最后判断t<=h
        def possibale(K):
           t = sum((p-1) // K + 1 for p in piles)
           return t <= h

        # 每次最少吃一个,最多吃苹果里数量最大的
        left,  right = 1, max(piles)
        while left <= right:
            mid = left + (right-left) // 2
            # 如果以mid速度能吃完,那么减小速度
            if possibale(mid):
                right = mid - 1
            # 如果以mid速度不能吃完,那么增大速度
            else:
                left = mid + 1
        return left

明白了这一题,还有一题相似的Leetcode 1011 在 D 天内送达包裹的能力
以上就是我对二分法的简单介绍和应用二分法解题,起到了抛砖引玉的作用。除了上述的三个场合,还有很多都会使用二分,更多详见⻄法的刷题秘籍:https://leetcode-solution-leetcode-pp.gitbook.io/leetcode-solution/thinkings/binary-search-1

此外还有对于二分法的固定模板问题,labuladong的算法小抄官方完整版(字节)一书有详细地讲解。https://mp.weixin.qq.com/s/M1KfTfNlu4OCK8i9PSAmug

参考文章

[1] 我作了首诗,保你闭着眼睛也能写对二分查找——labuladong
[2] 二分专题上、下——⻄法的刷题秘籍
[3] https://leetcode-cn.com/leetbook/read/binary-search/xehmn2/

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

黄波波19

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值