二分元素第一次出现的位置和最后一次出现的位置

本文介绍了一种使用二分查找法实现的上界和下界搜索算法,旨在为STL中的lower_bound()和upper_bound()提供替代方案。通过自定义函数binsearch_low()和binsearch_up(),可以在有序数组中找到指定元素的第一个或最后一个出现位置。

stl里面封装了挺不错的lower_bound()和upper_bound()。但是有时候条件的判断又得自己写,所以摸索着写了两个函数,还是比较习惯 (low<=high) 的写法

//上界:
int binsearch_low(int lef, int rig,int key,int a[])
{
    int ans = -1;
    int low = lef,high = rig;
    while(low <= high){
        int mid = (low + high)/2;
        if(key == a[mid]){
            ans = mid;
            high = mid - 1;
        }
        else if(key < a[mid])
            high = mid - 1;
        else
            low = mid + 1;
    }
    return ans;
}



//下界:
int binsearch_up(int lef, int rig,int key,int a[])
{
    int ans = -1;
    int low = lef,high = rig;
    while(low <= high){
        int mid = (low + high)/2;
        if(key == a[mid]){
            ans = mid;
            low = mid + 1;
        }
        else if(key < a[mid]){
            high = mid - 1;
        }
        else{
            low = mid + 1;
        }
    }
    return ans;
}

<think>我们正在讨论二分查找的不同区间写法(闭区间左闭右开)与查找元素第一次出现位置(下界)或最后一次出现位置(上界)的关系。 关键点:在二分查找中,当数组有重复元素时,我们可能需要找到目标值的第一个出现位置(左边界)或最后一个出现位置(右边界)。不同的区间写法在实现这两种变体时,边界条件的处理有所不同。 根据引用[3]:“左闭右开型的适合用来寻找下界,左开右闭型的适合用来寻找上界”。这里下界指的是目标值第一次出现的位置(左边界),上界指的是目标值最后一次出现的位置(右边界)。 我们将分别讨论闭区间左闭右开区间在寻找左边界右边界时的应用。 一、闭区间写法([left, right])在寻找边界时的应用 闭区间写法通常使用循环条件:while (left <= right),更新时left=mid+1或right=mid-1。 1. 寻找左边界(第一次出现位置)的闭区间实现: 思路:即使找到目标值,我们也不立即返回,而是继续向左缩小搜索区间(因为左边可能还有相同的值)。 步骤: - 初始化left=0, right=n-1 - 循环条件:left<=right - 当arr[mid]==target时,记录当前位置,然后令right=mid-1(继续向左搜索) - 循环结束后,检查记录的左边界是否有效(或者检查left是否越界等) 2. 寻找右边界(最后一次出现位置)的闭区间实现: 思路:找到目标值后,继续向右搜索。 步骤: - 初始化left=0, right=n-1 - 循环条件:left<=right - 当arr[mid]==target时,记录当前位置,然后令left=mid+1(继续向右搜索) - 循环结束后,同样需要检查记录的右边界。 二、左闭右开区间写法([left, right))在寻找边界时的应用 左闭右开区间使用循环条件:while (left < right),更新时right=mid或left=mid+1。 根据引用[1][3]: - 寻找左边界(下界)时,使用左闭右开区间非常自然。 因为左闭右开区间在更新时,当中间值等于目标值时,我们让right=mid,这样就可以继续在左半区间([left, mid))中寻找更靠左的目标值。 这样,循环结束时leftright相等,且指向第一个等于目标值的位置(或者第一个大于目标值的位置,如果没有找到目标值则指向插入位置)。 算法步骤(寻找左边界): left=0, right=n # 注意:右边界初始为n,表示区间[0, n) while left < right: mid = left + (right-left)//2 if nums[mid] < target: left = mid+1 else: # 当nums[mid]>=target,说明目标值在左半部分,注意这里等于时我们也要向左移动 right = mid # 因为区间是左闭右开,所以right=mid保证了区间缩小为[left, mid) 循环结束后,left(或right)就是第一个大于等于目标值的索引(即左边界)。如果等于目标值,则就是第一个目标值的位置。 - 寻找右边界(上界)时,可以使用左开右闭区间(但引用[3]提到左开右闭型适合寻找上界)。不过,我们也可以使用左闭右开区间来寻找右边界,但需要调整。 另一种常见做法是:寻找右边界时,我们实际上在寻找第一个大于目标值的元素位置,然后减一。 使用左闭右开区间寻找右边界: left=0, right=n while left < right: mid = left + (right-left)//2 if nums[mid] <= target: # 注意:这里用<= left = mid+1 # 这样可以继续向右推进,直到找到第一个大于目标值的位置 else: right = mid # 循环结束后,left指向第一个大于目标值的位置,那么left-1就是最后一个等于目标值的位置(如果存在) 三、总结关系 1. 闭区间写法: - 左右边界都可以实现,但需要记录中间结果,并在循环中不断更新边界。 - 循环条件为left<=right,更新边界时用mid±1。 2. 左闭右开区间写法: - 寻找左边界(下界)非常直接:在相等时收缩右边界(right=mid),循环结束后的left即为左边界(第一个>=target的位置)。 - 寻找右边界(上界)可以通过先找到第一个大于目标值的位置(即目标值的下一个位置),然后减一得到。因此,我们可以通过两次二分(先找左边界再找右边界)或者调整条件来实现。 引用[3]指出:左闭右开型适合寻找下界,左开右闭型适合寻找上界。但实际中,左闭右开区间通过调整也可以用于寻找上界。 示例代码: 左闭右开区间寻找左边界(下界): int lower_bound(vector<int>& nums, int target) { int left = 0, right = nums.size(); while (left < right) { int mid = left + (right-left)/2; if (nums[mid] < target) { left = mid+1; } else { right = mid; } } return left; // 返回第一个大于等于target的位置 } 左闭右开区间寻找右边界(上界): int upper_bound(vector<int>& nums, int target) { int left = 0, right = nums.size(); while (left < right) { int mid = left + (right-left)/2; if (nums[mid] <= target) { left = mid+1; } else { right = mid; } } return left; // 返回第一个大于target的位置,那么右边界就是left-1 } 然后,右边界为left-1(注意检查left-1是否有效)。 闭区间写法寻找左边界: int lower_bound(vector<int>& nums, int target) { int left = 0, right = nums.size()-1; int ans = -1; // 记录可能的位置 while (left <= right) { int mid = left + (right-left)/2; if (nums[mid] >= target) { // 如果当前值等于target,可能是一个候选,但还要继续向左找 if (nums[mid] == target) { ans = mid; // 记录候选位置 } right = mid-1; // 向左缩小 } else { left = mid+1; } } return ans; // 如果没有记录过,则返回-1 } 但是这样写,如果有多个target,ans会记录从右向左扫描过程中遇到的最后一个target?实际上,我们向左缩小区间,所以最后一次记录的是最左边的target吗?不一定,因为当找到一个target后,我们继续向左,如果左边还有target,会继续更新ans。但是,如果左边没有target了,那么ans就是最左边的。所以这个写法正确。 然而,更常见的闭区间写法寻找左边界并不记录ans,而是利用循环结束后的left: int lower_bound(vector<int>& nums, int target) { int left = 0, right = nums.size()-1; while (left <= right) { int mid = left + (right-left)/2; if (nums[mid] < target) { left = mid+1; } else { right = mid-1; } } // 循环结束后,left指向第一个大于等于target的位置,但要注意检查left是否在[0, n-1]范围内,且nums[left]是否等于target。 // 因为循环条件结束时,left = right+1,而right是最后一个小于target的位置(如果存在),那么left就是第一个>=target的位置。 if (left < nums.size() && nums[left] == target) { return left; } else { return -1; } } 注意:这个写法中,当找到目标值时,我们执行的是right=mid-1,所以循环结束后left指向的可能是第一个等于目标值的位置(如果目标值存在)。但是,如果目标值不存在,left可能越界或者指向比目标值大的第一个数。 因此,闭区间写法也可以不记录中间结果,而是利用循环结束后的left(或right)位置。但是,这种写法需要仔细处理边界。 结论: 闭区间左闭右开区间都可以实现寻找左右边界,但左闭右开区间在寻找左边界时更简洁(不需要记录中间结果),而通过左闭右开区间寻找右边界时,可以通过先找到第一个大于目标值的位置再减一得到。 引用[1]提供了左闭右开区间实现普通二分查找的代码(非边界问题),引用[3]则指出区间类型与上下界搜索的关系[^1][^3]。 最后,我们用一个例子来验证: 数组:nums = [1,2,3,3,3,5,6], target=3 左边界应为2,右边界应为4。 使用左闭右开区间找左边界(lower_bound): 初始:left=0, right=7 mid=3 -> nums[3]=3 >=3 -> right=3 mid=1 -> nums[1]=2 <3 -> left=2 mid=2+(3-2)/2=2 -> nums[2]=3>=3 -> right=2 循环结束,left=2 -> 返回2,正确。 使用左闭右开区间找右边界(通过upper_bound-1): upper_bound是找第一个大于3的位置: 初始:left=0, right=7 mid=3 -> nums[3]=3<=3 -> left=4 mid=4+(7-4)/2=5 -> nums[5]=5>3 -> right=5 mid=4+(5-4)/2=4 -> nums[4]=3<=3 -> left=5 循环结束,left=5,所以右边界=5-1=4,正确。 因此,在解决“第一次出现位置最后一次出现位置”的问题时,我们可以根据区间写法的特点选择合适的实现方式。 </think> ### 闭区间、左闭右开区间与边界搜索的关系 在二分查找中,闭区间左闭右开区间与元素**首次/末次出现位置**(边界搜索)的关系体现在区间定义对边界更新的影响上。关键区别在于: --- #### 一、核心关系总结 | **问题类型** | **推荐区间类型** | **循环条件** | **边界更新规则** | **结果位置** | |--------------------|------------------|--------------|-------------------------------|--------------------------| | **首次出现位置**<br>(左边界/下界) | 左闭右开 `[l, r)` | `l < r` | 相等时 `r = mid` | 循环结束时的 `l` | | **末次出现位置**<br>(右边界/上界) | 闭区间 `[l, r]` | `l <= r` | 相等时 `l = mid + 1` 并记录位置 | 最后一次记录的 `mid` 值 | --- #### 二、首次出现位置(左边界)实现 采用**左闭右开区间**更自然(引用[1][3])。核心逻辑: 1. **区间定义**:`[l, r)` 保证左边界可收缩 2. **更新规则**:当 `arr[mid] == target` 时,**不停止**而是继续左移右边界 (`r = mid`),以搜索更小的索引 3. **终止条件**:`l == r` 时,`l` 指向第一个等于目标值的位置 ```python # 查找首次出现位置 (左闭右开区间) def first_occurrence(arr, target): l, r = 0, len(arr) # 区间 [0, n) while l < r: # 左闭右开关键条件 mid = (l + r) // 2 if arr[mid] < target: l = mid + 1 # 排除左半区 else: r = mid # 关键:包含相等时左移右边界 return l if l < len(arr) and arr[l] == target else -1 ``` **原理**: - 当 `arr[mid] == target`,`r = mid` 将区间缩小为 `[l, mid)`,继续检查左侧可能存在的更早出现位置 - 最终 `l` 指向**第一个等于或大于 `target`** 的位置(即左边界)[^1] --- #### 三、末次出现位置(右边界)实现 采用**闭区间**更直接(引用[2][4])。核心逻辑: 1. **区间定义**:`[l, r]` 允许完整覆盖端点 2. **更新规则**:当 `arr[mid] == target` 时,**记录位置**并右移左边界 (`l = mid + 1`) 3. **终止条件**:`l > r` 时,返回记录的最后一次匹配位置 ```python # 查找末次出现位置 (闭区间) def last_occurrence(arr, target): l, r = 0, len(arr) - 1 # 闭区间 [0, n-1] result = -1 while l <= r: # 闭区间关键条件 mid = (l + r) // 2 if arr[mid] < target: l = mid + 1 elif arr[mid] > target: r = mid - 1 else: result = mid # 记录匹配位置 l = mid + 1 # 关键:继续右移搜索 return result ``` **原理**: - 当 `arr[mid] == target`,`l = mid + 1` 强制搜索右半区 `[mid+1, r]` - 最终 `result` 记录**最后一次匹配位置**(即右边界)[^2][^4] --- #### 四、选择依据与对比 1. **左闭右开优势** - 自然适应**左边界搜索**(下界),循环结束时的 `l` 直接是解 - 无需额外变量记录位置(引用[3]) 2. **闭区间优势** - 直观处理**右边界搜索**(上界),通过显式记录位置确保正确性 - 边界更新逻辑更符合闭区间数学定义(引用[4]) 3. **通用性** - 左边界问题优先左闭右开(如 `lower_bound`) - 右边界问题优先闭区间(如 `upper_bound`) > 引用[3]总结: > *“左闭右开型适合寻找下界,左开右闭型适合寻找上界”* > 实际编码中,左闭右开通过调整也可用于上界搜索(如先找 `upper_bound` 再减一)。 --- ### 总结 闭区间 (`l <= r`) 左闭右开区间 (`l < r`) 在边界搜索中的差异源于: 1. **首次出现位置**:需向左收缩区间 → 左闭右开更优(`r=mid` 保留左边界) 2. **末次出现位置**:需向右推进 → 闭区间更直观(`l=mid+1` 强制右移) 两种方法均可实现边界搜索,但根据问题特性选择区间类型能简化逻辑并避免边界错误[^1][^3]。
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值