java 二分搜索获得大于目标数的第一位_一套模版玩转所有二分查找题

d16f2a08988655f669ae8cab81491e5e.png

二分核心思想

问题满足二段性的性质,即数组的一半满足给定的问题的解,另一半不满足给定问题的解。

算法思路:
假设目标值在闭区间 [l, r] 中,每次将区间长度缩小一半,当 l = r 时,我们就招到了目标值。

二分查找算法模板

二分模板一共有两个,分别适用于不同情况。

版本 1

当我们将区间 [l, r] 划分为 [l, mid][mid+1, r] 时,其更新操作为 r = mid 或者 l = mid+1 ,计算 mid 时不需要加 1 。

Python 模板:

def bsearch_1(l, r):
    while (l < r):
        mid = (l + r) >> 1
        if (check(mid)):
            r = mid
        else:
            l = mid + 1

版本 2

当我们将区间 [l, r] 划分成 [l, mid-1][mid, r] 时,其更新操作是 r = mid-1 或者 l = mid ,此时为了防止死循环,计算 mid 时需要加 1 。

Python 模板:

def bsearch_1(l, r):
    while (l < r):
        mid = (l + r + 1) >> 1
        if (check(mid)):
            l = mid
        else:
            r = mid - 1

二分只有下面两种情况:

  1. 找大于等于给定数的第一个位置 (满足某个条件的第一个数)
  2. 找小于等于给定数的最后一个位置(满足某个条件的最后一个位置)

7c935f9107e801fa46a17db8aa6d9b40.png

常见二分问题

x 的平方根

问题链接:x的平方根

问题分析

答案区间在 [0, x] ,假设我们的答案是 mid ,则要找的是满足 mid * mid <= x 的最后一个数,选择模板 2 的思路来解决。

代码详解

def mySqrt(x):
    l = 0
    r = x  # 确定边界
    while (l < r):
        mid = (l + r + 1) >> 1  # 模板 2 计算 mid 需要加 1
        if (mid * mid <= x):
            l = mid
        else:
            r = mid - 1
    return r

搜索插入位置

问题链接:搜索插入位置

问题分析

答案区间在 [0, len(nums)-1] ,数组单调递增,我们要找到第一个满足性质 mid >= xmid,则我们需要找的答案是后一段的第一个点,所以利用模板 1 。

代码详解

def searchInsert(nums, target):
    # 边界条件:数组为空或者所有数都小于 target
    if not nums or nums[-1] < target:
        return len(nums)
    l = 0
    r = len(nums) - 1
    while (l < r):
        mid = (l + r) >> 1
        if (nums[mid] >= target):
            r = mid
        else:
            l = mid + 1
    return r

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

问题链接:在排序数组中查找元素的第一个和最后一个位置

问题分析

该问题是对模版 1 和模版 2 的综合,即既需要找到给定满足 target 的第一个点,也需要找到最后一个点。

代码详解

def searchRange(nums, target):
    if not nums:
        return [-1, -1]
    # 找右区域的第一个点,选择模版 1
    l = 0
    r = len(nums) - 1
    while (l < r):
        mid = (l + r) >> 1
        if (nums[mid] >= target):
            r = mid
        else:
            l = mid + 1
    if (nums[r] != target):
        return [-1, -1]
    start = r  # 找到左边界
    # 找左区域的最后一个点,选择模版 2
    l = 0
    r = len(nums) - 1
    while (l < r):
        mid = (l + r + 1) >> 1
        if (nums[mid] <= target):
            l = mid
        else:
            r = mid - 1
    end = r
    return [start, end]

搜索二维矩阵

问题链接:搜索二维矩阵

问题分析

每行的第一个整数大于前一行的最后一个整数,所以整个数组还是单调递增的,所以搜索边界为 [0, n*m-1] ,选择性质 nums[mid] >= target ,则我们要找的是右区域的第一个数,选择模版 1 。

代码详解

def searchMatrix(matrix, target):
    if not matrix or not matrix[0]:
        return False
    n = len(matrix)
    m = len(matrix[0])
    # 确定边界
    l = 0
    r = n * m - 1
    while (l < r):
        mid = (l + r) >> 1
        # 找到 mid 对应的元素
        if matrix[mid // m][mid % m] >= target:
            r = mid
        else:
            l = mid + 1
    if matrix[r // m][r % m] != target:
        return False
    else:
        return True

寻找旋转数组的最小值

问题链接:寻找旋转排序数组中的最小值

问题分析

首先寻找一个性质将整个数组划分为两端,以数组 [4,5,6,7,0,1,2] 为例,我们要找的是元素 0 ,可以发现该元素将整个数组切分成了两部分,前一部分的元素都大于后一部分的元素,且前后两个子序列都是递增的。所以我们可以很容易地找到一个性质来将整个数组划分为两段,即 nums[mid] <= nums[-1] 。而且我们要找的解是右区域的第一个元素,所以选择模版 1 。

代码详解

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

搜索旋转排序数组

问题链接:搜索旋转排序数组

问题分析

虽然整个数组不是有序的,但是可以划分为两个有序的子序列,而且前一个序列的元素都大于后一个序列的元素。因此该题我们可以先找到切分点,将整个数组划分为两个都是递增的子序列,然后在判断目标值在哪一个子序列中,利用二分查找该值。

代码详解

def search(nums, target):
    if not nums:
        return -1
    # 找到分界点,划分为两个子序列
    l = 0
    r = len(nums) - 1
    while (l < r):
        mid = (l + r) >> 1
        if (nums[mid] <= nums[-1]):
            r = mid
        else:
            l = mid + 1
    # 确定二分查找的区间
    if target <= nums[-1]:  # [r, len(nums)-1]
        r = len(nums) - 1
    else:                 # [0, r-1]
        l = 0
        r = r - 1
    # 二分查找
    while (l < r):
        mid = (l + r) >> 1
        if (nums[mid] >= target):
            r = mid
        else:
            l = mid + 1
    return r if nums[r] == target else -1

第一个错误的版本

问题链接:第一个错误的版本

问题分析

查找区间是 [1, n] ,第一个错误的版本将整个数组划分为了两部分,前一部分都是正确的版本,后一部分都是错误的版本,显然利用模版 1 就可以解决。

代码详解

def firstBadVersion(n):
    l = 1
    r = n
    while (l < r):
        mid = (l + r) >> 1
        if isBadVersion(mid):
            r = mid
        else:
            l = mid + 1

寻找峰值

问题链接:寻找峰值

问题分析

由于 nums[-1] = nums[n] = -∞ ,所以划分中点之后,在中点右侧或左侧一定存在一个峰值。如果 nums[mid] < nums[mid+1] ,则右侧一定存在峰值;否则,左侧一定存在峰值。我们的目标是找到 nums[mid] > nums[mid+1] 的第一个点,所以采用模版 1 。

代码详解

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

寻找重复数

问题链接:寻找重复数

问题分析

长度为 n+1 的数组,其值都在 [0, n] 区间。根据抽屉原理,显然存在至少两个重复的整数。由于只有一个数出现重复,所以我们可以将抽屉划分为两半,则必定有一半的数大于区间的长度,因此另一半区间就不需要再进行查找了。

注意:该题我们是将值区间进行二分,而不是将数组的元素进行二分。每次二分之后都需要遍历整个数组,所以时间复杂度为 $O(nlog n)$ 。

代码详解

def findDuplicate(nums):
    # 值区间二分
    l = 1
    r = len(nums) - 1
    while (l < r):
        mid = (l + r) >> 1
        cnt = 0
        # 统计左侧元素
        for x in nums:
            if (x >= l and x <= mid):
                cnt += 1
        if (cnt > mid - l + 1):
            r = mid
        else:
            l = mid + 1
    return r

H指数 II

问题链接:H指数 II

问题分析

数组是升序排列,则对于要找的 h 显然满足二段性。我们要找的是 h 指数中最大的那个,即左区域的最后一个元素,因此使用模版 2 。

注意:h 的取值区间是 [0, len(nums)]

代码详解

def hIndex(citations):
    l = 0
    r = len(citations)
    while (l < r):
        mid = (l + r + 1) >> 1
        # 倒数第 h 个数大于等于 h
        if (citations[len(citations) - mid] >= mid):
            l = mid
        else:
            r = mid - 1
    return r

乘法表中第 k 小的数

问题链接:乘法表中第k小的数

问题分析

输入: m = 3, n = 3, k = 5
输出: 3
解释: 
乘法表:
1   2   3
2   4   6
3   6   9

第5小的数字是 3 (1, 2, 2, 3, 3).

观察上述样例可以发现,整个乘法表满足从左至右是递增的,从上到下也是递增的。而且由于任何一个数都是由其索引相乘得到的,因此对于给定一个索引位置,我们可以很容易的判断在该表中比该位置的数小的总共有多少个数。因此,本题可以采用二分查找来进行求解,由于是找第 k 小的数,即第一个大于等于 k 的位置,因此采用模板 1 进行求解。

例如对于上述问题,查找区间为:[1, 9]

首先:mid = (1+9)>>1 = 5 ,可以知道该位置对应的数字为 4 。

遍历每一行,可以根据 mid / i 来找到每行中小于等于 mid 位置的数的个数;

对于第一行:min(n, mid / i) = min(3, 5/1) =3

对于第二行:min(3, 5/2) = 2

对于第三行:min(3, 5/3) = 1

所以总共小于等于 4 的数有 6 个。

总时间复杂度为:

代码详解

def findKthNumber(m, n, k):
    l = 1
    r = m*n
    while (l < r):
        mid = (l + r) >> 1
        if (k <= check(m, n, mid)):
            r = mid
        else:
            l = mid + 1
    return r

def check(m, n, mid):
    count = 0
    for i in range(1, m+1):
        count += min((mid // i, n))
    return count

if __name__ == "__main__":
    m, n, k = 45, 12, 471
    r = findKthNumber(m, n, k)
    print(r)

问题扩展

拼多多笔试题 :乘法表中第 k 大的数

只需要对上述代码稍作修改,找第 k 大的数,实质上就是找第 m*n-k+1 小的数,所以只需要改变下判断条件即可:

def findKthNumber(m, n, k):
    l = 1
    r = m*n
    while (l < r):
        mid = (l + r) >> 1
        if ((m*n-k+1) <= check(m, n, mid)):
            r = mid
        else:
            l = mid + 1
    return r
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值