📝 题目描述
题目链接:在排序数组中查找元素的第一个和最后一个位置
给你一个按照非递减顺序排列的整数数组 nums,和一个目标值 target。请你找出给定目标值在数组中的开始位置和结束位置。
如果数组中不存在目标值 target,返回 [-1, -1]。
你必须设计并实现时间复杂度为 O(log n) 的算法解决此问题。
示例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| 示例 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 <= 10^5
-10^9 <= nums[i] <= 10^9
nums 是一个非递减数组
-10^9 <= target <= 10^9
💡 解题思路
方法一:调库
如果元素存在,闭区间左界就是 lower_bound 指向的值的索引,右界就是 upper_bound 指向的值的索引。
只需要简单判定,再将迭代器转化为索引即可。
方法二:手搓
直观的思路肯定是从前往后遍历一遍。用两个变量记录第一次和最后一次遇见 target 的下标,但这个方法的时间复杂度为 O(n),没有利用到数组升序排列的条件。
由于数组已经排序,因此整个数组是单调递增的,我们可以利用二分法来加速查找的过程。
考虑 target 开始和结束位置,其实我们要找的就是数组中“第一个等于 target 的位置”(记为 leftIdx)和“第一个大于 target 的位置减一”(记为 rightIdx)。
二分查找中,寻找 leftIdx 即为在数组中寻找第一个大于等于 target 的下标,寻找 rightIdx 即为在数组中寻找第一个大于 target 的下标,然后将下标减一。两者的判断条件不同,为了代码的复用,我们定义 binarySearch(nums, target, lower) 表示在 nums 数组中二分查找 target 的位置,如果 lower 为 true,则查找第一个大于等于 target 的下标,否则查找第一个大于 target 的下标。
最后,因为 target 可能不存在数组中,因此我们需要重新校验我们得到的两个下标 leftIdx 和 rightIdx,看是否符合条件,如果符合条件就返回 [leftIdx,rightIdx],不符合就返回 [−1,−1]。
比如,如果要找第一个等于 6 的数,那么就需要 if(target <= nums[mid]) (如果等于,可能左边还有索引更小的等于 target 的数字),因此我们必须取左边的区间,即 right = mid - 1,同时记录出现的位置。
如果要找第一个大于 6 的数,那么就需要 if(target < nums[mid]) ,说明中点已经大于 target,可能左边还有更小的大于 target 的数字,因此我们必须取左边的区间,即 right = mid - 1,同时记录出现的位置。
🔧 代码实现
1、调库
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| class Solution { public: vector<int> searchRange(vector<int>& nums, int target) { vector<int>::iterator start = lower_bound(nums.begin(), nums.end(), target, less<int>()); vector<int>::iterator end = upper_bound(nums.begin(), nums.end(), target, less<int>()); if (start != nums.end() && *start == target) { int startIndex = start - nums.begin(); int endIndex = end - 1 - nums.begin(); return {startIndex, endIndex}; } else { return {-1, -1}; } } };
|
2、手搓
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25
| class Solution { public: int binarySearch(vector<int>& nums, int target, bool lower) { int left = 0, right = (int)nums.size() - 1, ans = (int)nums.size(); while (left <= right) { int mid = left + (right - left) / 2; if (nums[mid] > target || (lower && nums[mid] >= target)) { right = mid - 1; ans = mid; } else { left = mid + 1; } } return ans; }
vector<int> searchRange(vector<int>& nums, int target) { int leftIdx = binarySearch(nums, target, true); int rightIdx = binarySearch(nums, target, false) - 1; if (leftIdx <= rightIdx && rightIdx < nums.size() && nums[leftIdx] == target && nums[rightIdx] == target) { return vector<int>{leftIdx, rightIdx}; } return vector<int>{-1, -1}; } };
|
📊 复杂度分析
1、调库
- 时间复杂度:O(logn),STL 中的 lower_bound 和 upper_bound 底层实现就是二分查找,单次调用的时间复杂度为 O(logn),调用两次,整体依然是 O(logn)。
- 空间复杂度:O(1),只使用了几个迭代器和整型变量,没有占用额外的堆空间。
2、手搓
- 时间复杂度:O(logn),同样执行了两次二分查找,每次搜索空间减半。
- 空间复杂度:O(1),只使用了几个基本的整型指针(left, right, mid, ans),常数级空间。
🎯 总结