题目描述
给定一个按照升序排列的整数数组 nums,和一个目标值 target。找出给定目标值在数组中的开始位置和结束位置。
你的算法时间复杂度必须是 O(log n) 级别。
如果数组中不存在目标值,返回 [-1, -1]。
示例 1:
输入: nums = [5,7,7,8,8,10], target = 8
输出: [3,4]
示例 2:
输入: nums = [5,7,7,8,8,10], target = 6
输出: [-1,-1]
思路解析
两个问题:查找相等的最左边和最右边
二分查找:思路很简单,细节是魔鬼。深入细节,比如不等号是否应该带等号,mid 是否应该加一等等。分析这些细节的差异以及出现这些差异的原因,保证你能灵活准确地写出正确的二分查找算法。分析二分查找的一个技巧是:不要出现 else,而是把所有情况用 else if 写清楚,这样可以清楚地展现所有细节。
while(left <= right) 的终止条件是 left == right + 1,写成区间的形式就是 [right + 1, right],或者带个具体的数字进去 [3, 2],可见这时候搜索区间为空,因为没有数字既大于等于 3 又小于等于 2的吧。所以这时候 while 循环终止是正确的,直接返回 -1 即可。
while(left < right) 的终止条件是 left == right,写成区间的形式就是 [left, right],或者带个具体的数字进去 [2, 2],这时候搜索区间非空,还有一个数 2,但此时 while 循环终止了。也就是说这区间 [2, 2] 被漏掉了,索引 2 没有被搜索,如果这时候直接返回 -1 就是错误的。
当然,如果你非要用 while(left < right)也可以,我们已经知道了出错的原因,就打个补丁好了
- 为什么 left = mid + 1,right = mid - 1?我看有的代码是 right = mid或者 left = mid,没有这些加加减减,到底怎么回事,怎么判断?
答:这也是二分查找的一个难点,不过只要你能理解前面的内容,就能够很容易判断。
刚才明确了「搜索区间」这个概念,而且本算法的搜索区间是两端都闭的,即 [left, right]。那么当我们发现索引 mid 不是要找的 target 时,如何确定下一步的搜索区间呢?
当然是 [left, mid - 1] 或者 [mid + 1, right] 对不对?因为 mid 已经搜索过,应该从搜索区间中去除
- 此算法有什么缺陷?
答:至此,你应该已经掌握了该算法的所有细节,以及这样处理的原因。但是,这个算法存在局限性。
比如说给你有序数组 nums = [1,2,2,2,3],target = 2,此算法返回的索引是 22,没错。但是如果我想得到 target 的左侧边界,即索引 11,或者我想得到 target 的右侧边界,即索引 33,这样的话此算法是无法处理的。
这样的需求很常见。你也许会说,找到一个 target,然后向左或向右线性搜索不行吗?可以,但是不好,因为这样难以保证二分查找对数级的复杂度了。
本题:
因为我们需找到 target 的最左侧索引
所以当 nums[mid] == target 时不要立即返回
而要收紧右侧边界以锁定左侧边界
因为我们需找到 target 的最右侧索引
所以当 nums[mid] == target 时不要立即返回
而要收紧左侧边界以锁定右侧边界
又因为收紧左侧边界时必须 left = mid + 1
class Solution:
def searchRange(self, nums, target):
size = len(nums)
# 特判,这一步很重要,否则执行到后序方法可能会出现数组下标越界
# 同时后序两个方法也不用做特殊判断了
if size == 0:
return [-1, -1]
num1 = self.__find_lower_bound(nums, target)
# 细节:如果左边界都搜索不到,右边界也没有必要看了
if num1 == -1:
return [-1, -1]
num2 = self.__find_up_bound(nums, target)
return [num1, num2]
def __find_lower_bound(self, nums, target):
# 找大于等于 target 的第 1 个数的索引,小于一定不符合要求
size = len(nums)
left = 0
right = size - 1
while left < right:
# 根据分支逻辑,这里选择左中位数
# mid = left + (right - left) // 2
mid = (left + right) >> 1
# 因为找大于等于 target 的第 1 个数,因此小于一定不符合要求
# 把它写在分支的前面
if nums[mid] < target:
left = mid + 1
else:
right = mid
# 因为有可能不存在目标元素,最后一定要单独判断一下
if nums[left] != target:
return -1
return left
def __find_up_bound(self, nums, target):
# 找小于等于 target 的最后 1 个数的索引,大于一定不符合要求
# 因为有可能不存在,最后一定要单独判断一下
size = len(nums)
left = 0
right = size - 1
while left < right:
# 根据分支逻辑,这里选择右中位数
# mid = left + (right - left + 1) // 2
mid = (left + right + 1) >> 1
# 因为找小于等于 target 的最后 1 个数,因此大于一定不符合要求
# 把它写在分支的前面
if nums[mid] > target:
right = mid - 1
else:
left = mid
# 因为有可能不存在目标元素,最后一定要单独判断一下
if nums[left] != target:
return -1
return left
AC34
网友评论