问题分析
在一个非递减数组中,寻找目标值的起始和结束位置。若不存在,返回 [-1, -1]
。需在 O(log n)
时间内完成。
关键观察
target
的位置):通过二分查找找到第一个 不小于 target
的位置。target
的位置):通过二分查找找到第一个 大于 target
的位置,再减一。二分查找设计
lowerBound
:寻找第一个 ≥ target
的位置。upperBound
:寻找第一个 > target
的位置。class Solution {
public:
vector<int> searchRange(vector<int>& nums, int target) {
if(nums.empty()) return {-1,-1};
int lower = lowerbound(nums, target);
int upper = upperbound(nums, target) - 1; // 右边界为 upperBound-1
if(lower==nums.size()||nums[lower]!=target) return{-1,-1};
return {lower,upper};
}
int lowerbound(vector<int>& nums,int target)
{
int l=0,r=nums.size(),mid;
while(l<r)
{
mid = l+(r-l)/2;
if(nums[mid]<target)
{
l=mid+1;
}else{
r=mid;
}
}
return r;
}
int upperbound(vector<int>& nums,int target)
{
int l=0,r=nums.size(),mid;
while(l<r)
{
mid = l+(r-l)/2;
if(nums[mid]<=target)
{
l=mid+1;
}else{
r=mid;
}
}
return r;
}
};
lowerBound
函数int lowerBound(vector<int> &nums, int target) {
int l = 0, r = nums.size(), mid;
while (l < r) {
mid = l + (r - l) / 2;
if (nums[mid] < target) {
l = mid + 1; // 目标在右侧,缩小左边界
} else {
r = mid; // 目标在左侧或当前,缩小右边界
}
}
return l; // 最终 l 是第一个 ≥ target 的位置
}
nums[mid] < target
,说明左边界在右侧;否则可能在左侧或当前。l == r
时,l
是第一个满足 ≥ target
的位置。upperBound
函数int upperBound(vector<int> &nums, int target) {
int l = 0, r = nums.size(), mid;
while (l < r) {
mid = l + (r - l) / 2;
if (nums[mid] <= target) {
l = mid + 1; // 目标在右侧,缩小左边界
} else {
r = mid; // 目标在左侧或当前,缩小右边界
}
}
return l; // 最终 l 是第一个 > target 的位置
}
nums[mid] ≤ target
,说明右边界在右侧;否则可能在左侧或当前。l == r
时,l
是第一个满足 > target
的位置。searchRange
vector<int> searchRange(vector<int> &nums, int target) {
if (nums.empty()) {
return {-1, -1}; // 空数组直接返回
}
int lower = lowerBound(nums, target);
int upper = upperBound(nums, target) - 1; // 右边界为 upperBound-1
// 检查是否存在目标值
if (lower >= nums.size() || nums[lower] != target) {
return {-1, -1};
}
return {lower, upper};
}
lower
和右边界 upper
(通过 upperBound - 1
)。lower
是否有效且对应值等于 target
。以 nums = [5,7,7,8,8,10], target = 8
为例:
lowerBound
过程:
3
(第一个 8
的位置)。upperBound
过程:
5
(第一个 >8
的位置),upper = 5-1 = 4
。[3,4]
。[-1, -1]
。lower
越界或 nums[lower] != target
,返回 [-1, -1]
。nums = [1]
,target = 1
,结果为 [0,0]
。O(log n)
,总时间为 O(log n)
。O(1)
,仅用常数空间。[1] target=1的情况会输出[0,-1]
在二分查找中,右边界初始化为 nums.size()
而非 nums.size()-1
是设计上的关键,这与二分查找的区间定义和终止条件密切相关。以下详细解释原因,并通过示例分析说明为什么修改为 r = nums.size()-1
会导致错误。
二分查找的区间可以是 左闭右开 [l, r)
或 左闭右闭 [l, r]
,不同的定义会影响初始化和循环条件。
原题解的设计(左闭右开)
初始区间为 [0, nums.size())
,即所有可能的元素位置。循环条件是 l < r
,确保区间合法性。
若修改为 r = nums.size()-1
此时区间变为左闭右闭 [0, nums.size()-1]
,循环条件需改为 l <= r
,否则会漏掉最后一个元素。
当尝试将 r
初始化为 nums.size()-1
时,若未同步调整循环条件和边界更新逻辑,会导致以下问题:
nums = [1]
, target = 1
假设修改 lowerBound
的初始化为 r = nums.size()-1 = 0
,并保持循环条件 l < r
:
l = 0
, r = 0
→ 循环条件 0 < 0
不成立,直接返回 l = 0
。
lowerBound
返回 0
,看似正确。upperBound
的问题:
l = 0
, r = 0
→ 循环不执行,返回 l = 0
。upper = 0 - 1 = -1
→ 最终结果 [0, -1]
,显然错误。问题根源:在左闭右闭区间下,循环条件必须为 l <= r
,否则无法处理区间内仅剩一个元素的情况。
原题解将区间定义为左闭右开 [l, r)
,初始化为 r = nums.size()
,优势如下:
nums = [1]
, target = 1
lowerBound
过程:
[0, 1)
,循环条件 0 < 1
成立。mid = 0 + (1-0)/2 = 0
,检查 nums[0] < 1
→ 不成立,更新 r = 0
。l = 0
(正确左边界)。upperBound
过程:
[0, 1)
,循环条件 0 < 1
成立。mid = 0
,检查 nums[0] <= 1
→ 成立,更新 l = 1
。l = 1
→ upper = 1 - 1 = 0
(正确右边界)。结果正确:[0, 0]
。
r = nums.size()-1
的后果若强制将 r
初始化为 nums.size()-1
,但未调整其他逻辑,会导致以下问题:
nums = [2, 2]
, target = 2
lowerBound
(假设 r = 1
,循环条件 l < r
):
[0, 1)
(实际只检查索引 0
)。l = 0
(正确左边界)。upperBound
(假设 r = 1
,循环条件 l < r
):
[0, 1)
,mid = 0
,检查 nums[0] <= 2
→ 成立,更新 l = 1
。l = 1
→ upper = 1 - 1 = 0
。[0, 0]
(正确应为 [0, 1]
)。原因:右边界初始化为 1
导致 upperBound
未检查索引 1
。
左闭右开区间 [l, r)
:
覆盖所有元素,循环终止时 l == r
,确保所有可能的位置被检查。
边界更新逻辑:
若 nums[mid] < target
→ 左边界更新为 mid + 1
(排除 mid
);
否则 → 右边界更新为 mid
(保留 mid
作为候选)。
将 r
初始化为 nums.size()
是为了定义左闭右开区间 [l, r)
,确保:
若强行改为 r = nums.size()-1
,需同步修改循环条件和边界更新逻辑,否则会破坏算法的数学保证,导致边界错误。
在题解中,无需额外检查 upper
的越界或值是否匹配,因为以下逻辑已经确保当 lower
有效时,upper
必然有效且正确:
lower
的有效性保证 upper
的有效性lower
的定义:通过 lowerBound
找到的是第一个 ≥ target 的位置。若 lower
有效(未越界且 nums[lower] == target
),说明数组中存在 target
。upperBound
的定义:返回第一个 > target 的位置,因此 upper = upperBound - 1
是最后一个 ≤ target 的位置。
lower
到 upper
之间的所有元素必然都等于 target
。nums = [5,7,7,8,8,10]
,target = 8
:
lowerBound
返回 3
(第一个 8
),upperBound
返回 5
(第一个 >8
的位置),upper = 4
(最后一个 8
)。nums[lower] == target
的覆盖性lower
有效且 nums[lower] == target
,则 upper
必定有效:
upperBound
返回的值范围为 [0, nums.size()]
,因此 upper = upperBound - 1
的范围为 [-1, nums.size()-1]
。lower
有效时(即存在 target
),upper
才会落在 [lower, nums.size()-1]
范围内,不可能越界。target
:nums = [8,8,8]
,lower = 0
,upper = 2
,均有效。target
:nums = [5,7,7,10]
,target = 8
,lower = 3
(越界或 nums[3] != 8
),直接返回 [-1,-1]
。upper
lower
的检查已经覆盖了所有可能的目标值不存在的情况:
[-1, -1]
。lower
越界(如 lower = nums.size()
):说明所有元素 < target
,直接返回 [-1, -1]
。nums[lower] != target
:说明 lower
是第一个 > target
的位置(此时 target
不存在),返回 [-1, -1]
。lower
有效且 nums[lower] == target
,则 upper
必定是最后一个 target
的位置,无需二次验证。假设强行添加对 upper
的检查:
if (lower >= nums.size() || nums[lower] != target || upper >= nums.size() || nums[upper] != target) {
return {-1, -1};
}
lower
有效且 nums[lower] == target
时,upper
一定是最后一个 target
,不可能出现 nums[upper] != target
。nums[upper] != target
,则说明 upperBound
的计算错误,但这与算法的数学逻辑矛盾。通过 lowerBound
和 upperBound
的配合,结合对 lower
的检查,已严格确保结果的正确性。额外检查 upper
是冗余的,因为算法的数学逻辑和代码实现已覆盖所有边界情况。