Binary Search模板总结与LeetCode经典范题

本文详细总结了二分查找的概念及其一般化应用,通过一系列LeetCode经典题目展示了如何将二分查找模板应用于不同难度的题目中。文章介绍了如何初始化边界、确定返回值以及构建条件函数,以解决从基本到高级的各种问题,如寻找数组中的特定值、求平方根、搜索插入位置等。此外,还探讨了如何在看似不适用二分查找的题目中挖掘单调性,例如运货能力规划、数组划分、Koko吃香蕉等问题。最后,强调了通过大量练习提高二分查找技巧的重要性。
摘要由CSDN通过智能技术生成

Binary Seach在概念上很容易理解,无非就是每次将搜索空间划分为两份,只保留这二者中可能包含目标值的那一份,持续二分下去,直到完成搜索为止,由此将搜索复杂度从线性时间 O ( n ) O(n) O(n)减少至对数时间 O ( l o g   n ) O(log\ n) O(log n)。但在具体实现上,想要在几分钟内成功写出一份bug free的代码并不是件简单的事情。我们常常会遇到一些恼人的细节问题,诸如while循环的条件应该用while left < right还是用while left <= right,初始化边界的时候leftright该设为什么值,更新边界的时候应该如何从left = mid, left = mid + 1, right = mid, right = mid - 1之中选择合适的组合。另外,Binary Search并不是只能适用于「给定一个数组,搜索一个目标数字」这样简单的场合,它有着更为一般化的应用场景。

我会在这篇文章里详细地总结这些内容,并把它应用到LeetCode的实际题目中。我不希望只是简单地贴出每道题目的代码,我所希望分享的是思路,是如何将最一般化的Binary Search模板应用到各种题目上面,让大家不要再有“诶呀原来这道题目可以用二分查找来做! 我怎么就没有想到呢!”的懊恼。

一般化的Binary Search

给定一个从小到大排好序的数组array,在不同的场景下,我们会有不同的搜索目标,诸如搜索特定的数字target,给一个数字寻找插入的位置等。对于绝大多数任务,我们都可以将其转化下面的最一般化的形式
m i n   k s . t .   c o n d i t i o n ( k )   i s   T r u e min\ k \\ s.t. \ condition(k) \ is\ True\\ min ks.t. condition(k) is True
下面是最一般化的Binary Search模板:

def binary_search(array) -> int:
    def condition(value) -> bool:
        pass

    left, right = 0, len(array)
    while left < right:
        mid = left + (right - left) // 2
        if condition(mid):
            right = mid
        else:
            left = mid + 1
    return left

这一模板的强大之处在于,对于绝大多数的Binary Search题目,我们只需要修改三个部分就可以照搬这个模板,而不必再去担心代码bug和角点解的问题。

  • 正确地初始化leftright这两个变量。只需要满足一点要求即可:初始化之后的边界必须囊括所有可能的元素
  • 根据具体应用场景修改返回值,是return left还是return left - 1还是return -1。只需要记住一点:在while循环终止之后,left便是上面一般化公式中的最小的k值,并且此时leftright相等;
  • 构思好condition函数具体是什么,这是最困难最巧妙的部分,也是最需要勤加练习的部分。

下面我会结合一些经典高质量的LeetCode题目来展示如何套用这个万能的模板。

基本应用

278. First Bad Version [Easy]

You are a product manager and currently leading a team to develop a new product. Since each version is developed based on the previous version, all the versions after a bad version are also bad. Suppose you have n versions [1, 2, ..., n] and you want to find out the first bad one, which causes all the following ones to be bad. You are given an API bool isBadVersion(version) which will return whether version is bad.

Example:

Given n = 5, and version = 4 is the first bad version.

call isBadVersion(3) -> false
call isBadVersion(5) -> true
call isBadVersion(4) -> true

Then 4 is the first bad version. 

首先初始化边界,因为搜索范围是[1, 2, ..., n]所以我们设置left, right = 1, n + 1,然后我们注意到,模板里的condition函数实际上已经给好了(直接调用提供的API即可),我们的搜索目标是找到最小的索引值 k ∗ k^{*} k,此时版本 k ∗ k^{*} k是First Bad Version,而版本 k ∗ − 1 k^{*} - 1 k1则是Last Good Version,于是解法如下:

class Solution:
    def firstBadVersion(self, n) -> int:
        left, right = 1, n
        while left < right:
            mid = left + (right - left) // 2
            if isBadVersion(mid):
                right = mid
            else:
                left = mid + 1
        return left

69. Sqrt(x) [Easy]

Implement int sqrt(int x). Compute and return the square root of x, where x is guaranteed to be a non-negative integer. Since the return type is an integer, the decimal digits are truncated and only the integer part of the result is returned.

Example:

Input: 4
Output: 2
Input: 8
Output: 2

简单明了的题目,我们需要找的是满足 k 2 ≤ x k^2 \le x k2x的最大k值,于是很轻松地就能写出解法:

def mySqrt(x: int) -> int:
    left, right = 0, x + 1
    while left < right:
        mid = left + (right - left) // 2
        if mid * mid <= x:
            left = mid + 1
        else:
            right = mid
    return left - 1

这里有一点需要注明的是,文章开头我们说**「寻找满足条件的最小 k k k值是最具一般化的形式」**,而现在却在搜索最大的 k k k值,是否前后矛盾?不矛盾。满足feasible(k) == False的最大 k k k值加上1,便等于满足feasible(k) == True的最小 k

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值