原题地址(中等)

方法1—一种二分查找

二分查找思路很简单,但细节是魔鬼

思路

这题一看要求时间复杂度 O(logN) ,就能立刻想到是二分查找。

这题考察的就是二分查找的细节。
**
套用模板2试一试
二分查找模板
并且使得 nums[mid]==targethigh=mid ,即

  1. while low < high:
  2. mid = low + (high - low) // 2
  3. if nums[mid] < target:
  4. low = mid + 1
  5. else:
  6. high = mid

这个二分可以查找数组中第一个出现的 target
如果 target 不存在,则会返回第一个比target大的数;
如果都比 target 小,则会返回最后一个数;
如果都比 target 大,则会返回第一个数。

这样,我们就找到了第一个 target ,那么,我们怎么找到最后一个 target 呢?是否需要再想一个二分查找?
再写一个二分查找肯定能够解决,我们放到方法2,但是在这里,我们还是使用上面的二分查找。

上面的二分查找可以查找数组中第一个出现的 target ,那么肯定也可以查找数组中第一个出现的 target+1 ,这个数的前一个,不就是最后一个 **target** 吗。
**
所以运用两次相同的二分查找,就可以解决问题了。

具体细节放到代码的注释中讲解。

代码1

  1. class Solution:
  2. def searchRange(self, nums: List[int], target: int) -> List[int]:
  3. n = len(nums)
  4. if n == 0 or nums[n-1] < target or nums[0] > target: # 特判,直接返回
  5. return [-1, -1]
  6. low, high = 0, n-1
  7. # 以下循环会找出第一个target的下标
  8. while low < high:
  9. mid = low + (high - low) // 2
  10. if nums[mid] < target:
  11. low = mid + 1
  12. else:
  13. high = mid
  14. if nums[low] != target: # 如果不存在target,直接返回
  15. return [-1, -1]
  16. left = low # 记下最左边的target的下标
  17. # 第二个循环,找出第一个比target大的数的下标,
  18. # 有一个特殊情况就是[1,2,3,3,3] target+1=4 这种情况会返回最后一个3,所以在循环结束后用一个if else做了特判
  19. low, high = low, n-1
  20. while low < high:
  21. mid = low + (high - low) // 2
  22. if nums[mid] < target+1:
  23. low = mid + 1
  24. else:
  25. high = mid
  26. if nums[low] > target:
  27. right = low-1
  28. else:
  29. right = low
  30. return [left, right]

代码2

将两次 while 循环写成一个函数

  1. class Solution:
  2. def searchRange(self, nums: List[int], target: int) -> List[int]:
  3. def binarySearch(low, high, k):
  4. while low < high:
  5. mid = low + (high - low) // 2
  6. if nums[mid] < k:
  7. low = mid + 1
  8. else:
  9. high = mid
  10. return low
  11. n = len(nums)
  12. if n == 0 or nums[n-1] < target or nums[0] > target: # 特判,直接返回
  13. return [-1, -1]
  14. left = binarySearch(0, n-1, target)
  15. if nums[left] != target: # 如果不存在target,直接返回
  16. return [-1, -1]
  17. right = binarySearch(left, n-1, target+1)
  18. if nums[right] > target:
  19. right -= 1
  20. return [left, right]

时空复杂度

时间复杂度:两次二分,为 O(logN)
空间复杂度: O(1)

方法2—两种二分查找

思路

方法1中我们用一个二分查找模板,分别代入 targettarget+1 求解出了 targettarget+1 的最左端,从而间接求出了 target 的左右端点。

在方法二中,我们用细节上有差别的两种二分查找,来搜索出左右端点。

如果看完此文还不明白的话,可以回过头来看下面文章:

方法1中的模板是无法既查找左侧区间又查找右侧区间的,那如果我们试一试模板1呢?
语雀内容
也就是说循环中有三个判断条件,而不是两个。

使用此模板的话,如果要找左侧端点,那么当 nums[mid] == target 就应该进入左侧区间;如果要找右侧端点,就应该进入右侧区间。

先来查找左侧端点

  1. left, right = 0, n-1
  2. while left <= right:
  3. mid = left + (right - left) // 2
  4. if nums[mid] == target:
  5. right = mid - 1
  6. elif nums[mid] > target:
  7. right = mid - 1
  8. else:
  9. left = mid + 1

注意,这个代码最后退出循环时一定有 left>rightleft == right+1

  • 如果 nums 中存在 target ,那么最后一定会走 if nums[mid] == target: right = mid - 1 这一步,然后循环条件被破坏,循环终止,此时 nums[left] 就是 target
  • 如果 nums 中不存在 target ,我们也不用考虑太多,比较麻烦。由于我们知道了如果**nums 中存在 target ,则nums[left] 一定是 target 。所以我们只需要判断 left 没越界并且nums[left]==target ,我们就找到了左端,否则就是不存在 target 。**


查找右侧端点:**
就是当nums[mid] == target时让 left=mid+1 ,分析方法和查找左侧端点完全一样,这里不再赘述。

代码1

  1. class Solution:
  2. def searchRange(self, nums: List[int], target: int) -> List[int]:
  3. n = len(nums)
  4. if n == 0 or nums[n-1] < target or nums[0] > target: # 特判,直接返回
  5. return [-1, -1]
  6. # 第一个while,找左端点
  7. left, right = 0, n-1
  8. while left <= right:
  9. mid = left + (right - left) // 2
  10. if nums[mid] == target:
  11. right = mid - 1
  12. elif nums[mid] > target:
  13. right = mid - 1
  14. else:
  15. left = mid + 1
  16. if left < n and nums[left] == target:
  17. a = left
  18. else: # 不存在target,就可以直接返回了
  19. return [-1, -1]
  20. # 第二个while,找右端点
  21. left, right = 0, n-1
  22. while left <= right:
  23. mid = left + (right - left) // 2
  24. if nums[mid] == target:
  25. left = mid + 1
  26. elif nums[mid] > target:
  27. right = mid - 1
  28. else:
  29. left = mid + 1
  30. if right >= 0 and nums[right] == target:
  31. b = right
  32. return [a, b]

代码2

两个while循环只有等号时不一样,所以可以用一个变量将两个循环合并

  1. class Solution:
  2. def searchRange(self, nums: List[int], target: int) -> List[int]:
  3. def binary_search(left, right, isLeft):
  4. while left <= right:
  5. mid = left + (right - left) // 2
  6. if nums[mid] == target:
  7. if isLeft: right = mid - 1
  8. else: left = mid + 1
  9. elif nums[mid] > target:
  10. right = mid - 1
  11. else:
  12. left = mid + 1
  13. if isLeft:
  14. return left if (left<n and nums[left] == target) else -1
  15. else:
  16. return right if (right >= 0 and nums[right] == target) else -1
  17. n = len(nums)
  18. if n == 0 or nums[n-1] < target or nums[0] > target: # 特判,直接返回
  19. return [-1, -1]
  20. a = binary_search(0, n-1, True)
  21. b = binary_search(0, n-1, False)
  22. return [a, b]

时空复杂度

同方法1