二分查找重复元素的范围 Leetcode 34

本文介绍了如何运用二分查找法解决LeetCode第34题,在可能存在重复元素的排序数组中查找目标值的第一个和最后一个位置。关键在于当找到目标值时,需进一步搜索其左右区间以确定范围。文章提供了两种方法,分别通过修改标准二分查找法和调整while循环条件来实现。每种方法都包括寻找起点和终点两个步骤,通过优化减少了重复代码。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

在上一篇文章中( 二分查找法搜寻元素 Leetcode35, Leetcode69 ),我们分析了二分查找法的两种经典思路。现在我们再来挑战一下更复杂的情况:Leetcode 34. 在排序数组中查找元素的第一个和最后一个位置。这道题的数组中可能有重复元素,需要求出值为 target 的元素范围,以 [ 第一个值为 target 的元素索引, 最后一个值为 target 的元素索引 ] 的方式返回。如果没有,就返回 [ -1, -1]。

在此之前,我们都是把二分查找法用于无重复元素的排序数组中搜寻,每个元素的值都是唯一的,只有找到和找不到两种可能。而这道题难在要求出重复元素的范围,也就是说,找到一个值为 target 的元素也不一定符合要求。解决这类问题的关键点在于:当搜寻区间的中间点取值等于 target 时,我们不能止步于此,还要继续搜寻它的左右区间有没有等于 target 的元素,以此来找到重复元素范围的起点和终点。

建议您先看一下上一篇文章(二分查找法搜寻元素 Leetcode35, Leetcode69)的第二部分:查找取值与 target 最接近的元素,对于理解这个题目的解决思路有帮助。

方法一、标准二分查找法 while left <= right

基于标准二分查找 ( while left <= right ),做了一点改动:当 nums[middle] 的值等于 target 时,不能马上返回 middle 结束循环,用一个 index 变量(初始设为 -1)保存此时的 middle 值,然后:

  • 搜索起始点:要在左区间 [ left, middle - 1] 继续搜寻有没有等于 target 的元素。因此,当 nums[middle] == target 时,设置 index = middle, right = middle - 1。

    while 循环结束时,index 的值就是所能找到的等于 target 的元素的最小索引,也就是我们要找的起始点。

  • 搜索终止点:要在右区间 [ middle +1, right ] 继续搜寻有没有等于 target 的元素。因此,当 nums[middle] == target 时,设置 index = middle, left = middle + 1。

    while 循环结束时,index 的值就是所能找到的等于 target 的元素的最大索引,也就是我们要找的终止点。

对比这两次查找会发现,搜索起始点和搜索终止点的二分查找在处理 nums[middle] 不等于 target 时的操作完全一样,只是在 nums[middle] 等于 target 时有不同:要根据找起点或终点来决定是继续在左区间或右区间搜寻。为了减少重复代码,可以用一个函数 searchTarget 来实现两次查找,设置一个 Boolean 变量 isSearchFirst 来标识搜寻起点或终点。具体 Python 代码如下:

# 数组为空的情况
if not nums:
    return [-1, -1]

def searchTarget(nums, target, isSearchFirst):
    # 用idx保存等于target的middle元素
    # 初始值为-1
    idx = -1
    left, right = 0, len(nums) - 1
    while left <= right:
        middle = left + (right - left) // 2

        if nums[middle] == target:
            idx = middle
            # 寻找起点和终点的二分查找代码只有这里不同
            # isSearchFirst为True,搜寻起点。
            # 下一步在左区间搜寻
            if isSearchFirst:
                right = middle - 1
            # isSearchFirst为False,搜寻终点
            # 下一步在右区间搜寻
            else:
                left = middle + 1

        elif nums[middle] < target:
            left = middle + 1
        else:
            right = middle - 1
            
    return idx


# 搜寻元素起始点
firstPos = searchTarget(nums, target, True)
# 如果没找到,直接返回 [-1, -1]
if firstPos == -1:
    return [-1,-1]
else:
    # 搜寻元素终点
    lastPos = searchTarget(nums, target, False)
    return [firstPos, lastPos] 
方法二、二分查找法之二 while left < right

这个方法 while 循环的条件是 left < right,与上述方法在 nums[middle] 的值等于 target 时的处理类似,不同之处:1.这个方法没有用变量保存索引,而是循环结束后再判断得到 ;2.下一步的搜寻区间包含了 middle:

  • 搜索起始点:在包含 middle 的左区间 [ left, middle ] 继续寻找,因此设置 right = middle。这个操作与 nums[middle] > target 时可以合并为:当 nums[middle] >= target 时,right = middle。当 nums[middle] < target 时,left = middle + 1。

    循环结束后,如果 nums[left] == target(left 或 right 均可,因为 left = right),left 就是我们要找的起点,否则说明数组中没有值为 target 的元素。

  • 搜索终止点:在包含 middle 的右区间 [ middle, right ] 继续寻找,因此设置 left = middle。这个操作与 nums[middle] < target 时可以合并为:当 nums[middle] <= target 时,left = middle。当 nums[middle] > target 时,right = middle - 1。

    这里有一个问题:while 循环到只有两个元素时,如果按照原有的求解 middle 方式,middle = left。当 nums[middle] <= target 时,又有 left = middle,这样没有更新搜寻区间,循环无法停止。解决方式很巧妙:设置 middle = ( left + right ) / 2 + 1,把 middle 往右边偏移。最后一次循环时有 middle = right,循环可以终止。

    循环结束后,left 就是我们要找的终点。

Python 代码如下:

# 数组为空的情况
if not nums:
    return [-1, -1]

# result数组保存返回索引
result = [-1, -1]

# 确定重复元素起点的搜寻区间
left, right = 0, len(nums) - 1
while left < right:
    middle = left + (right - left) // 2
    if nums[middle] >= target:
        # 继续在左区间[left, middle]搜寻
        right = middle                
    else:                
        left = middle + 1
if nums[left] != target:
    return result
else:
    result[0] = left
    
# 确定重复元素终点的搜寻区间
# 此时区间的起点left就设置为刚找到的起点
right = len(nums) - 1
while left < right:
    middle = left + (right - left + 1) // 2
    if nums[middle] <= target:
        # 继续在右区间[middle, right]搜寻
        left = middle 
    else:
        right = middle - 1
result[1] = left

return result

本文对您有帮助的话,请点赞支持一下吧,谢谢!

关注我 宁萌Julie,互相学习,多多交流呀!

参考:

1.方法一思路:Easy java O(logn) solution

2.方法二思路:Clean iterative solution with two binary searches

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值