二分查找(搜索区间为左闭右开)

本文探讨了二分查找的不同写法,包括左闭右开和左开右闭两种策略,解释了它们的循环条件、边界调整和返回结果的区别,并通过实例展示了C++ STL库中的lower_bound和upper_bound函数的应用。

二分查找有几种写法?它们的区别是什么? - Jason Li的回答

引言

二分查找的过程是不断把搜索边界缩小,往中间“挤”出结果的过程。

二分查找难点

假设 l l l是当前搜索区间左边界, r r r是当前搜索区间右边界, m m m是向下取整中位点, A A A为有序表。
之后示例中 l 0 l_0 l0 r 0 r_0 r0为初始边界, v v v为目标值

  • 循环条件: l < r   o r   l ≤ r l < r \ or \ l \le r l<r or lr
  • 边界调整: l = m   o r   l = m + 1 ; r = m   o r   r = m − 1 l = m \ or \ l = m + 1; r = m \ or \ r=m-1 l=m or l=m+1;r=m or r=m1
  • 返回结果: l   o r   l + 1   o r   r   o r   r + 1 l \ or \ l+1 \ or \ r \ or \ r+1 l or l+1 or r or r+1

以上问题可以通过取左闭右开的搜索区间来尝试解决,最终结论如下

  • 循环条件: l < r l<r l<r
  • 边界调整: l = m + 1 , r = m l = m + 1, r = m l=m+1r=m
  • 返回结果: l   o r   r l \ or \ r l or r

个人感性认识上,正是因为二分除法中,中位数有两种取法,而左闭右开选择了向下取整的中位数,来减少讨论。

C++STL库实现的二分查找函数,提供的参数也是相同的思路
lower_bound(first,last,value):从下标[first, last)内,二分查找第一个大于或等于value的数字
upper_bound(first,last,value):从下标[first, last)内,二分查找第一个大于value的数字

lower_bound的相同效果实现思路

// 求 第一个 x >= v
while (l < r) { 
    m = l + ((r - l) >> 1); // 防溢
    if (A[m] < v) l = m + 1;
    else r = m;
}
return l; // 或 return r;

循环条件

在查找过程中循环继续的条件

  • [ l , r ) [l,r) [l,r)不为空,需要继续搜索
  • 此时 [ l 0 , l ) [l_0,l) [l0,l)所有元素小于 v v v
  • 此时 [ r , r 0 ) [r,r_0) [r,r0)所有元素大于等于 v v v

边界调整

  • A [ m ] < v A[m]<v A[m]<v m m m应该被划分在 [ l 0 , l ) [l_0,l) [l0,l)内,应调整 l = m + 1 l = m + 1 l=m+1
  • A [ m ] ≥ v A[m] \ge v A[m]v m m m应该被划分在 [ r , r 0 ) [r,r_0) [r,r0)内,应调整 r = m r = m r=m

返回结果

最终循环结束时,结果为第一个大于等于 v v v的元素下标

  • [ l , r ) [l,r) [l,r)为空, l = r l = r l=r
  • [ l 0 , l ) [l_0,l) [l0,l)所有元素小于 v v v
  • [ r , r 0 ) [r,r_0) [r,r0)所有元素大于等于 v v v

upper_bound的相同效果实现思路

// 求 第一个 x > v
while (l < r) { 
    m = l + ((r - l) >> 1);
    if (A[m] <= v) l = m + 1; // 差别在此,A[m] == v 的归属
    else r = m;
}
return l; // 或 return r;

循环条件

在查找过程中循环继续的条件

  • [ l , r ) [l,r) [l,r)不为空,需要继续搜索
  • 此时 [ l 0 , l ) [l_0,l) [l0,l)所有元素小于等于 v v v
  • 此时 [ r , r 0 ) [r,r_0) [r,r0)所有元素大于 v v v

在查找过程中循环继续的条件

边界调整

  • A [ m ] ≤ v A[m]\le v A[m]v m m m应该被划分在 [ l 0 , l ) [l_0,l) [l0,l)内,应调整 l = m + 1 l = m + 1 l=m+1
  • A [ m ] > v A[m] > v A[m]>v m m m应该被划分在 [ r , r 0 ) [r,r_0) [r,r0)内,应调整 r = m r = m r=m

返回结果

最终循环结束时,结果为第一个大于 v v v的元素下标

  • [ l , r ) [l,r) [l,r)为空, l = r l = r l=r
  • [ l 0 , l ) [l_0,l) [l0,l)所有元素小于等于 v v v
  • [ r , r 0 ) [r,r_0) [r,r0)所有元素大于 v v v

其他

可以用lower_boundupper_bound解决常用二分查找问题如下

  • 小于 v v v的上界 lower_bound(l,r,v) - 1
  • 小于等于 v v v的上界 upper_bound(l,r,v) - 1
  • 大于等于 v v v的下界 lower_bound(l,r,v)
  • 大于 v v v的下界 upper_bound(l,r,v)

中间两个较为常用

LeetCode 35.搜索插入位置

int searchInsert(int* nums, int numsSize, int target)
{
    int l, r;
    l = 0, r = numsSize;
    while (l < r) { 
        int m = l + ((r - l) >> 1);
        if (nums[m] < target) l = m + 1;
        else r = m;
    }
    return l;
}

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

int* searchRange(int* nums, int numsSize, int target, int* returnSize)
{
    int l = 0, r = numsSize, m;
    int* ret = malloc(sizeof(int) * 2);
    *returnSize = 2;
    ret[0] = ret[1] = -1;

    if (numsSize == 0) return ret;

    while(l < r) {
        m = l + ((r - l) >> 1);
        if (nums[m] < target) l = m + 1;
        else r = m;
    }
    if (r != numsSize && nums[l] == target) ret[0] = l;

    l = 0, r = numsSize;
    while (l < r) {
        m = l + ((r - l) >> 1);
        if (nums[m] <= target) l = m + 1;
        else r = m;
    }  
    if (l != 0 && nums[l - 1] == target) ret[1] = l - 1;

    return ret;
}
<think>嗯,用户想了解二分查找中的右闭区间的实现和应用场景。我先回忆一下二分查找的基本概念。二分查找通常用于有序数组,通过不断缩小搜索范围来快速定位目标元素。常见的区间写法有闭右闭[left, right]和闭右[left, right),但用户提到的是右闭(left, right],这不太常见,需要仔细分析。 首先,右闭区间意味着边界不包含在搜索范围内,而右边界包含。例如,初始区间可能是(0, n],即从索引1到n(假设数组从1始)。这种情况下,循环条件应该是left < right,因为当left等于right时,区间已经无效。然后在中间值的计算上,可能还是用(left + right) // 2,但需要处理边界调整的问题。比如,当中间元素小于目标值时,新的边界应该是mid,而不是mid + 1,因为边界是的,不包含。反之,如果中间元素大于目标值,右边界调整为mid,因为右闭,包含mid。这可能和常见的写法不同,容易出错,需要仔细验证。 然后,应用场景方面,右闭可能在处理某些特定边界问题时更方便,比如数组索引从1始的情况,或者某些数学问题中自然形成的右闭区间。例如,在寻找最后一个满足条件的元素时,这种区间划分可能简化逻辑,减少边界条件的判断。需要举一个具体的例子来说明,比如查找插入位置或者寻找边界/右边界的情况。 接下来,考虑代码实现。假设数组是升序排列,初始left设为0的,即初始区间(-1, n-1],但数组索引通常从0始,所以可能需要调整初始值。比如,如果数组索引从0到n-1,右闭的初始left可以设为-1,right设为n-1。循环条件left < right,计算mid时用(left + right + 1) // 2,因为当区间长度为偶数时,中间位置需要向上取整,避免死循环。调整边界时,如果arr[mid] < target,left = mid,否则right = mid - 1。这样在右闭的区间下,最终left的位置会是目标值的右边界或者插入点。 同时,需要注意这种写法可能在某些情况下更容易处理边界,比如寻找最后一个小于等于目标值的位置。例如,在寻找插入位置时,如果目标值存在多个重复元素,右闭区间可能更容易定位到正确的位置。需要测试几种情况,比如目标值在数组头、中间、结尾,或者不存在的情况,确保代码正确。 最后,对比闭右闭和右闭的异同,指出各自的优缺点。右闭可能在处理某些特定问题时更简洁,但需要更谨慎地处理中间值和边界调整,以避免死循环或漏查元素。同时,引用相关的参考资料,比如用户提供的引用[1]和[2],说明二分查找的不同实现方式及其应用场景,增强回答的可信度。</think>### 二分查找的“右闭”区间实现及场景分析 #### 1. 右闭区间的定义与核心逻辑 右闭区间表示为$(left, right]$,即边界不包含在搜索范围内,右边界包含。例如,若初始区间为$(0, n]$(假设数组索引从$1$始),则实际搜索范围为$1$到$n$。其核心逻辑如下: - **循环条件**:`while left < right`(当`left == right`时区间为空) - **中间值计算**:`mid = (left + right) // 2` - **边界调整**: - 若`arr[mid] < target`,则新区间为$(mid, right]$(边界排除`mid`) - 若`arr[mid] >= target`,则新区间为$(left, mid]$(右边界包含`mid`) #### 2. Python代码实现 ```python def binary_search_left_open(arr, target): left, right = -1, len(arr) - 1 # 初始区间(-1, n-1] while left < right: mid = (left + right + 1) // 2 # 向上取整,避免死循环 if arr[mid] < target: left = mid # 边界排除mid else: right = mid - 1 # 右边界调整到mid-1 return left + 1 # 最终位置为插入点或目标值的起始位置 ``` #### 3. 应用场景分析 1. **查找最后一个小于等于目标值的位置** 例如,在数组$[1, 2, 3, 3, 4]$中查找最后一个$3$的位置。右闭区间可自然定位到索引$3$,无需额外边界判断[^1]。 2. **插入位置的高效确定** 若目标值不在数组中,算法返回其应插入的位置。例如,在$[2, 4, 6]$中查找$5$,返回索引$2$(插入在$4$和$6$之间)。 3. **避免重复元素的冗余遍历** 在含重复元素的有序数组中,右闭可减少比较次数,直接逼近目标边界[^2]。 #### 4. 对比其他区间写法 | 区间类型 | 初始边界示例 | 循环条件 | 边界调整特点 | 适用场景 | |---------------|---------------|------------------|------------------------|------------------------| | 闭右闭$[l, r]$ | `left=0, right=n-1` | `left <= right` | 需要显式排除`mid` | 标准二分查找 | | 闭右$[l, r)$ | `left=0, right=n` | `left < right` | 右边界天然不包含 | 查找边界 | | **右闭$(l, r]$** | `left=-1, right=n-1` | `left < right` | 边界天然不包含 | 查找右边界或插入位置 | #### 5. 注意事项 - **死循环问题**:计算`mid`时需向上取整(`mid = (left + right + 1) // 2`),否则可能导致无限循环。 - **返回值修正**:最终返回`left + 1`,因为`left`是区间的最后一个无效位置。 - **数组索引范围**:需根据编程语言或问题需求调整初始值(例如从$0$或$1$始)。
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值