二分查找法的介绍与应用

本文详细介绍了二分查找算法的原理、关键点及常见应用场景,包括基本的二分查找、搜索二维矩阵、寻找序列边界的二分查找以及在解决最长递增子序列和能力检测问题中的应用。通过实例解析,帮助读者深入理解二分查找的实现和变式,并提供了相关LeetCode题目的解题思路。
摘要由CSDN通过智能技术生成

二分法介绍

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

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

黄波波19

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

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

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

打赏作者

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

抵扣说明:

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

余额充值