LeetCode 34. 在排序数组中查找元素的第一个和最后一个位置
题目链接
34. 在排序数组中查找元素的第一个和最后一个位置
题目描述
给定一个非递减排序的整数数组 nums 和目标值 target,找出 target 在数组中的起始位置和结束位置。若数组中无 target,返回 [-1, -1]。要求时间复杂度为 O(log n)(二分查找)。
三种解法对比分析
三种解法均基于二分查找找下界的核心思路:通过查找 target 的下界(第一个 ≥ target 的位置)确定起始索引,通过查找 target + 1 的下界确定结束索引(结束索引 = target + 1 的下界 - 1)。差异主要体现在二分查找的边界处理细节上。
解法一:循环条件 l <= r(经典二分)
from typing import Listclass Solution: def searchRange(self, nums: List[int], target: int) -> List[int]: # 下界查找:寻找第一个 >= t 的位置 def lower_bound(l: int, r: int, t: int) -> int: while l <= r: # 循环条件:左边界 <= 右边界 m = (l + r) // 2 if nums[m] >= t:  r = m - 1 # 中间值 >= 目标,收缩右边界(跳过m) else:  l = m + 1 # 中间值 < 目标,收缩左边界(跳过m) return l # 循环结束时 l > r,l 为目标位置 # 查找起始位置:范围为 [0, len(nums)-1](数组有效索引范围) start = lower_bound(0, len(nums) - 1, target) # 验证起始位置有效性 if start == len(nums) or nums[start] != target: return [-1, -1] # 查找结束位置:target + 1 的下界 - 1 end = lower_bound(start, len(nums) - 1, target + 1) - 1 return [start, end]
核心特点
- 循环条件:
l <= r,当l > r时退出循环,是最经典的二分查找循环条件。 - 边界设置:初始范围为 
[0, len(nums)-1](数组有效索引范围),符合“索引在数组内”的直觉。 - 收缩逻辑:
- 若 
nums[m] >= t:右边界收缩至m - 1(跳过m,后续通过l还原)。 - 若 
nums[m] < t:左边界收缩至m + 1(跳过m)。 
 - 若 
 - 返回值:
l(此时l > r,l必为第一个 ≥t的位置)。 - 越界处理:需额外判断 
start == len(nums)(目标大于所有元素时的越界情况)。 
解法二:循环条件 l < r
from typing import Listclass Solution: def searchRange(self, nums: List[int], target: int) -> List[int]: # 下界查找:寻找第一个 >= t 的位置 def lower_bound(l: int, r: int, t: int) -> int: while l < r: # 循环条件:左边界 < 右边界 m = (l + r) // 2 if nums[m] >= t:  r = m # 中间值 >= 目标,收缩右边界(保留m) else:  l = m + 1 # 中间值 < 目标,收缩左边界(跳过m) return l # 循环结束时 l == r,即为目标位置 # 查找起始位置:范围为 [0, len(nums)](右边界为数组长度,处理越界) start = lower_bound(0, len(nums), target) # 验证起始位置有效性 if start == len(nums) or nums[start] != target: return [-1, -1] # 查找结束位置:target + 1 的下界 - 1 end = lower_bound(start, len(nums), target + 1) - 1 return [start, end]
核心特点
- 循环条件:
l < r,当l == r时退出循环,避免死循环。 - 边界设置:初始范围为 
[0, len(nums)](右边界为数组长度,而非len(nums)-1),天然处理“目标大于所有元素”的越界情况。 - 收缩逻辑:
- 若 
nums[m] >= t:右边界收缩至m(保留m作为候选,因为可能是第一个 ≥t的位置)。 - 若 
nums[m] < t:左边界收缩至m + 1(m不可能是目标,直接跳过)。 
 - 若 
 - 返回值:
l(此时l == r,必为第一个 ≥t的位置)。 
解法三:循环条件 l + 1 < r(开区间处理)
from typing import Listclass Solution: def searchRange(self, nums: List[int], target: int) -> List[int]: # 下界查找:寻找第一个 >= t 的位置(开区间风格) def lower_bound(l: int, r: int, t: int) -> int: while l + 1 < r: # 循环条件:左右边界不相邻 m = (l + r) // 2 if nums[m] >= t:  r = m # 中间值 >= 目标,收缩右边界 else:  l = m # 中间值 < 目标,收缩左边界 return r # 循环结束时 l 和 r 相邻,r 为目标位置 # 查找起始位置:范围为 (-1, len(nums))(左边界为-1,右边界为数组长度) start = lower_bound(-1, len(nums), target) # 验证起始位置有效性 if start == len(nums) or nums[start] != target: return [-1, -1] # 查找结束位置:target + 1 的下界 - 1 end = lower_bound(start, len(nums), target + 1) - 1 return [start, end]
核心特点
- 循环条件:
l + 1 < r(左右边界不相邻),当l和r相邻时退出循环,避免复杂边界判断。 - 边界设置:初始范围为 
(-1, len(nums))(左边界为-1,右边界为len(nums)),将所有可能的结果包在开区间内,更符合“二分查找范围包含目标”的直觉。 - 收缩逻辑:
- 若 
nums[m] >= t:右边界收缩至m。 - 若 
nums[m] < t:左边界收缩至m(不跳过m,因为循环条件保证最终会收敛)。 
 - 若 
 - 返回值:
r(此时l和r相邻,r必为第一个 ≥t的位置)。 
三种解法对比分析
l <= r)l < r)l + 1 < r)l <= r(l > r 时退出)l < r(相等时退出)l + 1 < r(相邻时退出)[0, len(nums)-1][0, len(nums)](-1, len(nums))m(l/r = m ± 1)m(r = m)m(l = m 或 r = m)l(l > r)l(l == r)r(l 和 r 相邻)start == len(nums))len(nums))正确性验证(边界情况)
nums = [], target = 0[-1,-1][-1,-1][-1,-1][-1,-1]nums = [1], target = 1[0,0][0,0][0,0][0,0]nums = [1,3], target = 2[-1,-1][-1,-1][-1,-1][-1,-1]nums = [5,7,7,8,8,10], target = 8[3,4][3,4][3,4][3,4]nums = [2,2], target = 2[0,1][0,1][0,1][0,1]总结
三种解法均通过二分查找下界实现 O(log n) 时间复杂度,核心差异在于边界处理细节:
- 解法一(
l <= r)是最经典的二分查找实现,逻辑清晰,适合入门理解,但需额外处理越界。 - 解法二(
l < r)通过调整循环条件和范围,天然支持越界处理,实用性强,是工程中常用的写法。 - 解法三(
l + 1 < r)通过开区间范围处理边界,逻辑严谨,适合复杂边界场景,但理解成本稍高。 
实际应用中,三种方法均可正确解决问题,选择取决于个人对二分查找边界的理解习惯。核心是掌握“下界查找”的本质:找到第一个满足 nums[i] >= target 的位置,进而推导出目标值的起始和结束范围。


