34. 在排序数组中查找元素的第一个和最后一个位置 - 力扣(LeetCode)
题目
给你一个按照非递减顺序排列的整数数组 nums
,和一个目标值 target
。请你找出给定目标值在数组中的开始位置和结束位置。
如果数组中不存在目标值 target
,返回 [-1, -1]
。
你必须设计并实现时间复杂度为 O(log n)
的算法解决此问题。
示例 1:
输入:nums = [5,7,7,8,8,10], target = 8
输出:[3,4]
示例 2:
输入:nums = [5,7,7,8,8,10], target = 6
输出:[-1,-1]
示例 3:
输入:nums = [], target = 0
输出:[-1,-1]
提示:
-
0 <= nums.length <= 105
-
-109 <= nums[i] <= 109
-
nums
是一个非递减数组 -
-109 <= target <= 109
思路
- 直接用二分查找,找到下界和上界,因为可能这个数不存在,所以要判断一下当前找到的数是不是,下界往后判断一步,上界往前判断一步,如果都不是返回-1。
代码实现
class Solution {
public:
vector<int> searchRange(vector<int>& nums, int target) {
int n = nums.size(), left = 0, right, mid, start, end;
if(n == 0) return {-1, -1};
right = n - 1;
while(left < right) {
mid = (left+right)/2;
if(nums[mid] >= target) right = mid - 1;
else left = mid + 1;
}
if(left<n && nums[left] == target) start = left;
else if(left>=n-1 || nums[left+1] != target) start = -1;
else start = left + 1;
left = 0, right = n-1;
while(left < right) {
mid = (left+right)/2;
if(nums[mid] <= target) left = mid + 1;
else right = mid - 1;
}
cout << right;
if(right>=0 && nums[right] == target) end = right;
else if(right<=0 || nums[right-1] != target) end = -1;
else end = right - 1;
return {start, end};
}
};
复杂度分析
- 时间复杂度:两次查找的时间复杂度都是O(logn)的(其实第二次的left可以以第一次的下界开始),所以总的时间复杂度是O(logn)的。
- 空间复杂度:O(1)。
官方题解
- 函数封装还是很有价值的,受教了,可以将二分查找部分的函数封装起来,那么代码量看起来就小了。
- 先理解一下基本的二分查找的思路:
-
int binarySearch(vector<int>& nums, int target) { int left = 0, right = nums.size()-1, ans = nums.size(); while(left <= right) { int mid = left + (right-left)/2; if(nums[mid] >= target) { right = mid - 1; ans = mid; } else left = mid + 1; } return ans; }
- 首先外层循环的结束标志是left>right,这里不论大于还是小于,最后都会在mid基础上跨一步,不妨假设left=mid且nums[mid]=target,那么right就会直接越过left,且mid就是结果;若left=mid-1且nums[mid]=target,那么left=right,此时mid就是结果,但是还会再算一次让left越过right,因为我们让等于时更新right才给ans赋值,所以结果不会被小于的left影响;相对的,因为整型是去尾法,所以right只有在等于left时才会等于mid,这个时候就是回到第二个结果;如果right=mid+1且nums[mid]<target,那么left就会达到right,回到第二种情况。——所以在这里大于等于target同时赋值能满足二分查找保证ans是正确结果。
- 那么反过来可以吗?如果left=mid且nums[mid]<target,那么left就会越过ans,且记录下mid,后续都不会再更新;第二种情况,也是一样——所以反过来也是可行的。
- 因为ans都是更新在有等于的地方,所以最后ans指向的必然是等于target的位置,其他情况下指向的必然就是不等于了。
- 那么如果有多个重复的target,就需要根据大小于号的方向来判断了,如果更新是在小于等于处,那么就会得到最后的target,如果符号是大于等于,那么就会得到最早的target。
- 那么再延伸一下,如果更新ans的地方是不等于的地方呢?
- 那最后left、right还是会更新到等于target的地方,但ans不会到等于的地方,而是到达最靠近等于的地方,那么是上界还是下界就取决于不等于的那个条件判断的符号方向了,如果是小于,那么就是小于target的最大值,如果是大于,那么就是大于target的最小值。
- 所以,根据这个思路,就可以设计一个根据标记判断是要求某区间值的二分查找方法来找到一个边界和对侧边界的最近值。
- 这里实现的是找大于target的最小值和最小的target,通过isLower标记来控制计算条件判断(刚好可以在一个符号方向上更新)。求得后大于target的最小值退1就是要找的下标,然后判断指向的位置是否等于target即可(因为可能没有target)。
-
- 复现:
-
class Solution { public: int binarySearch(vector<int>& nums, int target, bool isLower) { int left = 0, right = nums.size()-1, ans = nums.size(); while(left <= right) { int mid = left + (right-left)/2; if(nums[mid]>target || (isLower && nums[mid]>=target)) { right = mid - 1; ans = mid; } else left = mid + 1; } return ans; } vector<int> searchRange(vector<int>& nums, int target) { int leftIndex = binarySearch(nums, target, true); int rightIndex = binarySearch(nums, target, false)-1; if(leftIndex<=rightIndex && rightIndex<nums.size() && nums[leftIndex]==target && nums[rightIndex]==target) return {leftIndex, rightIndex}; return {-1, -1}; } };