描述
iven an array of integers nums sorted in ascending order, find the starting and ending position of a given target value.
Your algorithm’s runtime complexity must be in the order of O(log n).
If the target is not found in the array, return [-1, -1].
Example 1:
Input: nums = [5,7,7,8,8,10], target = 8
Output: [3,4]
Example 2:
Input: nums = [5,7,7,8,8,10], target = 6
Output: [-1,-1]
解法一
一共有其中七种情况出现的, 假设查找是5的话,
case 1: [5 7] (A[i] = target < A[j])
case 2: [5 3] (A[i] = target > A[j])
case 3: [5 5] (A[i] = target = A[j])
case 4: [3 5] (A[j] = target > A[i])
case 5: [3 7] (A[i] < target < A[j])
case 6: [3 4] (A[i] < A[j] < target)
case 7: [6 7] (target < A[i] < A[j])
使用二分查找方法, 找到 num[mid] = target
然后利用while 在分别往
- 左边找first index of target
- 右边找 last index of target
代码如下
public int[] solution_2(int [] num ,int target){
if(num==null || num.length==0){
return null;
}
int result[] = new int[2];
Arrays.fill(num, -1);
int low =0;
int high = num.length-1;
// 查找位置类型的题目都是low<=high
while(low<=high){
int mid = (low+high)/2;
if(num[mid]>target){
high = mid-1;
}
else if(num[mid]<target) {
low = mid + 1;
}
else {
//查找成功的情况
int left = helper_left(num, low, mid,target);
int right= helper_right(num, mid, high, target);
result[0] = left;
result[1] = right;
return result;
}
}
return result;
}
private int helper_left(int [] num, int low, int mid,int target){
int mark = mid;
while(mark>=low && num[mark]==target){
--mark;
}
return mark+1;
}
private int helper_right(int [] num, int mid, int high, int target)
{
int mark = mid;
while(mark<=high && num[mark]==target)
{
mark++;
}
return mark-1;
}
复杂度分析
这个方法不是最优的情况 ,因为在最坏的情况下, 比如 [8,8,8,8,8,8,8,8 ] target 是中间的mid =8 , 同时要向两边进行扩展的情况出现,就不是很好的情况吧,所以还是没有达到题目要求中的 O(N) 时间复杂度的情况下 ,所以再进行一次优化情况。
分析
使用 二分法 找到目标值,然后以此目标值为中心,向左右两边扩展,但要考虑边界的情况,另一种方法就是,找到nums[mid]==target,然后从区间[low、high]的边缘向内缩小,来确定范围。面试官说这个不是最优解呀
改进方法 解法二
使用下面的方法进行估计
public int[] searchRange(int[] nums, int target) {
int[] result = {-1,-1};
if(nums == null || nums.length == 0) {
return result;
}
int i = 0;
int j = nums.length - 1;
while(i < j) {
int mid = (i+j)/2;
if(nums[mid] < target) {
i = mid + 1;
} else {
j = mid;
}
}
//为何这里是直接返回结果的情况的
if(nums[i] != target) {
return result;
}
//左边如果是第一个就是target的情况出现即可
else (nums[i]==target ){
result[0] = i;
}
j = nums.length - 1;
while(i < j) {
// mid
int mid = (i+j)/2 + 1;
if(nums[mid] > target) {
j = mid - 1;
} else {
i = mid;
}
}
result[1] = j;
return result;
}
主要的思路是下面这种情况 ,第一是查找左边的index, 第二是查找右边的index情况,
查找左边的index情况
分为两种情况
while(i<j)
- 如果num[mid]< target ,
- 那么左边的index 肯定是在右边, 所以 i = mid+1;
- 如果num[mid]>= target,
1. 那么第一个出现的位置index 肯定是在 mid 以及mid的右边的情况
在经过上面的循环后,
就有两种情况发生,
- 元素在数组中出现了, 可以查找到左边的index
- 元素不在数组出现,根本就找不到左边的index
对应于1 找得到的情况,例子如下所示
Input = [2,5,5,5,6,6,8,9,9,9]
target =5
例子(找得到左边的元素情况)
第一次 mid = 6>5 ,所以 j = mid =5, i=0
第二次 mid =5 >=5 j =mid=3, i=0
第三次 mid = 5 >=5 键 j =mid =1, i=0
第四次 mid = 2<5 所以 i = mid +1 =5 ,
例子(找得到右边的元素情况)
i = 1(代表是左边第一个出现的元素情况), j = 9
mid = (1+9)/2+1 = 6 nums[mid]= 8 >target;
所以右边的index应该是 在j =mid -1
如果 num[mid]<= target , 那么肯定是在i = mid 右边的情况来讲
if(num[i]==target){
result [0]=i; 找到了第一个下标的位置情况
}
找不到左边第一个index的情况
input =[2,5,5,5,6,6,8,9,9,9]
target = 4
第一情况
最后是查找不成功的情况,就可以直接返回了
if(nums[i]!=target)
左边是找不到,肯定是正个数组都不存在这种情况
接下来再查找右边的情况
第一个元素的情况如何进行查找的情况
在右边查找最后一个出现的位置信息,可以使用下面的情况进行查找
j= n-1 是从最后一个位置开始
如果上面可以查找左边的成功,那么左边的肯定也是存在的,关注于在右边的情况 ,
while(i<j)
{
int mid = (i+j)/2+1
if(A[mid]>target) j = mid-1;
else i =mid;
}
ret[1]= j,;
return ret;
}
完整代码如下所示的情况
完整的情况代码
对上述的代码进行简化处理
class Solution {
public int[] searchRange(int[] nums, int target) {
int [] result = new int[2];
int len = nums.length;
Arrays.fill(result,-1);
if(nums==null || nums.length==0)
return result;
int i = 0, j = len-1;
while(i<j){
int mid = (i+j)>>1;
if(nums[mid]<target){
i = mid+1;
}
其实下面两个步骤可以进行简化处理
else if(nums[mid]>target){
j = mid-1;
}
else {
j = mid;
}
}
if(nums[i]==target){
result[0]= i;
}
else {
return result;
}
j = nums.length-1;
while(i<j){
int mid = (i+j)/2+1;
if(nums[mid]>target){
j = mid -1;
}
else if(nums[mid]<target){
i = mid +1;
}
// int mid = (i+j)/2+ 1 就是为了考虑这种情况的发生, 比如 [5,7] , i = 0
如果按照 mid = (0+1)/2 = 0 ,nums[mid]= target, 那么 i = mid,
// i本来就是等于 0 , 再使用i = mid 还是没有改变 i的值,所以我们 mid = (0+1)/2 +1 = 1,
// 此时 i = mid =1>= j , 所以循环截止,
else {
i = mid;
}
}
result[1] = j;
return result;
}
}
上面代码要非常注意的就是
int mid = (i+j)/2 +1; 这里面的情况是怎样的
为何要将 mid +1, mid = (i+j)/2 +1 实现左边移动一个元素的情况,
因为i 的位置信息已经确定了,所以只要考虑 i+1 到 j 这里面的元素情况即可
那这里又有个疑问,就是为何不直接 ++i 呢,
如果是这样的话,万一只有一个元素呢,在这种情况下,就超出了长度,
为何是 mid = (i+j)/2 + 1 ?
However, the terminate condition on longer works this time. Consider the following case:
[5 7], target = 5
Now A[mid] = 5, then according to rule 2, we set i = mid. This practically does nothing because i is already equal to mid. As a result, the search range is not moved at all!
The solution is by using a small trick: instead of calculating mid as mid = (i+j)/2, we now do:
mid = (i+j)/2+1
Why does this trick work? When we use mid = (i+j)/2, the mid is rounded to the lowest integer. In other words, mid is always biased towards the left. This means we could have i == mid when j - i == mid, but we NEVER have j == mid. So in order to keep the search range moving, you must make sure the new i is set to something different than mid, otherwise we are at the risk that i gets stuck. But for the new j, it is okay if we set it to mid, since it was not equal to mid anyways. Our two rules in search of the left boundary happen to satisfy these requirements, so it works perfectly in that situation. Similarly, when we search for the right boundary, we must make sure i won’t get stuck when we set the new i to i = mid. The easiest way to achieve this is by making mid biased to the right, i.e. mid = (i+j)/2+1.
All this reasoning boils down to the following simple code: