LeetCode 第 34 题:“在排序数组中查找元素的第一个和最后一个位置”题解
题解地址:十分好用的二分查找法模板(Python 代码、Java 代码)。
说明:文本首发在力扣的题解版块,更新也会在第 1 时间在上面的网站中更新,这篇文章只是上面的文章的一个快照,您可以点击上面的链接看到其他网友对本文的评论。
传送门:34. 在排序数组中查找元素的第一个和最后一个位置。
给定一个按照升序排列的整数数组 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]
来源:力扣(LeetCode) 链接:https://leetcode-cn.com/problems/find-first-and-last-position-of-element-in-sorted-array 著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。
十分好用的二分查找法模板(Python 代码、Java 代码)
思路分析:毋庸置疑,肯定用二分法,二分法的核心思想是“夹逼法”或者称为“排除法”。
1、尽最大可能排除不符合题意的元素,在每一轮循环中不断减少候选区间的范围,直到候选区间收缩成 1 个数。
2、因为数组中很可能不存在目标值,因此在第 1 步使用“排除法”最后剩下的这个数,再判断一下它是否等于
target
即可。
我把这道题的两个子问题做了一下修改,并写出了关键部分:
1、子问题 1(“开始位置”):找到等于 target
的第 个元素的索引,如果找不到等于 target
的数,返回 ;
关键:如果小于,肯定不是解,此时 left = mid + 1
。
2、子问题 2(“结束位置”):找到等于 target
的最后 个元素的索引,如果找不到等于 target
的数,返回 。
关键:如果大于,肯定不是解:此时 right = mid - 1
。
在「力扣 」第 35 题:“搜索插入位置”的题解《特别好用的二分查找法模板(Python 代码、Java 代码)》 里我写了如何使用二分查找法的模板。
下面给出的参考代码,并且在一些细节处给出了注释,如果你有疑问的话,不妨再看一看我上面给出的文章,相信你很容易找到答案。
参考代码:
Python 代码:
from typing import List
class Solution:
def searchRange(self, nums: List[int], target: int) -> List[int]:
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
if __name__ == '__main__':
solution = Solution()
nums = [5, 7, 7, 8, 8, 10]
target = 8
result = solution.searchRange(nums, target)
print(result)
Java 代码:
import java.util.Arrays;
public class Solution {
public int[] searchRange(int[] nums, int target) {
int len = nums.length;
// 特判,这一步很重要,否则执行到后序方法可能会出现数组下标越界
// 同时后序两个方法也不用做特殊判断了
if (len == 0) {
return new int[]{-1, -1};
}
int num1 = findLowerBound(nums, target);
// 细节:如果左边界都搜索不到,右边界也没有必要看了
if (num1 == -1) {
return new int[]{-1, -1};
}
int num2 = findUpBound(nums, target);
return new int[]{num1, num2};
}
private int findLowerBound(int[] nums, int target) {
// 找等于 target 的第 1 个数的索引,小于一定不符合要求
int len = nums.length;
int left = 0;
int right = len - 1;
while (left < right) {
// 根据分支逻辑,这里选择左中位数
// int mid = left + (right - left) / 2;
int mid = (left + right) >>> 1;
// 因为找大于等于 target 的第 1 个数,因此小于一定不符合要求
// 把它写在分支的前面
if (nums[mid] < target) {
left = mid + 1;
} else {
right = mid;
}
}
// 因为有可能不存在目标元素,最后一定要单独判断一下
if (nums[left] != target) {
return -1;
}
return left;
}
private int findUpBound(int[] nums, int target) {
// 找等于 target 的最后 1 个数的索引,大于一定不符合要求
// 因为有可能不存在,最后一定要单独判断一下
int len = nums.length;
int left = 0;
int right = len - 1;
while (left < right) {
// 根据分支逻辑,这里选择右中位数
// int mid = left + (right - left + 1) / 2;
int 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;
}
public static void main(String[] args) {
int[] nums = {5, 7, 7, 8, 8, 10};
int target = 8;
Solution4 solution4 = new Solution4();
int[] res = solution4.searchRange(nums, target);
System.out.println(Arrays.toString(res));
}
}
复杂度分析:
- 时间复杂度:,这里 是数组的长度,两个子问题都是二分查找,因此时间复杂度为对数级别。
- 空间复杂度:,只使用了常数个数的辅助变量、指针。
补充说明:
上面我把
int mid = left + (right - left) / 2;
注释掉,而选用
int mid = (left + right) >>> 1;
并且把
int mid = left + (right - left + 1) / 2;
注释掉,而选用
int mid = (left + right + 1) >>> 1;
的原因,大家可以参考我为「力扣」第 374 题:“猜数字大小”写的题解 《借本题说一说取中位数的写法(Python 代码、Java 代码)》。