【力扣刷题-数组篇】34. 在排序数组中查找元素的第一个和最后一个位置的二分法思路和Java详细代码
给你一个按照非递减顺序排列的整数数组 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 <= 10^5
-10^9 <= nums[i] <= 10^9
nums
是一个非递减数组-10^9 <= target <= 10^9
最优解:
class Solution {
public int[] searchRange(int[] nums, int target) {
int l = 0, r = nums.length - 1;
int result[] = new int[]{-1, -1};
while(l <= r){
int mid = l + (r - l) / 2;
if (nums[mid] == target){
result[0] = mid;
result[1] = mid;
// 下面两个while里面的条件要仔细记住
while(mid>0 && nums[--mid] == target) result[0] = mid;
while(mid<nums.length-1 && nums[++mid] == target) result[1] = mid;
break;
}
else if (nums[mid] < target) l = mid + 1;
else r = mid - 1;
}
return result;
}
}
第二次写的解法
注意看最优解,它是把++和—什么的放在与后面,这样不会越界。它比我条件紧一个,所以它出了循环的mid就是正确的。
注意查到后的break,要不然很耗时。
class Solution {
public int[] searchRange(int[] nums, int target) {
// 先二分找
int left = 0;
int right = nums.length - 1;
// 要定义在外面
int[] res = {-1,-1};
while(left <= right) {
int mid = left + (right - left)/2;
// 要向左向右
if(nums[mid] == target) {
// 别老想着简写,判断里面写--有可能出问题
while(mid >= 0 && target == nums[mid]) {
mid--;
continue;
}
res[0] = ++mid;
// 此时mid又回到第一个等于target的位置了
while(mid < nums.length && target == nums[mid]) {
mid++;
continue;
}
res[1] = mid - 1;
// 这样找到了就不再继续了,要不然还会继续进行最外层的while
break;
} else if(nums[mid] < target) {
left = mid + 1;
} else {
right = mid - 1;
}
}
return res;
}
}
思路
二分法。但是关键是找这个第一个元素和最后一个元素,关键在于改变if(nums[mid] == target)条件下的语句。
我的想法是,把找左边界和右边界分开,先查找左边界:相等的时候,right继续往左移才能找左边界,我用一个flag来记录是不是查找到了,因为如果查不到,最终right + 1;就不是要返回的元素,而是-1。左边界,是right不断往左移,所以会停在不是target的最后一个元素,所以需要加1。
int left = 0, right = nums.length - 1;
if(nums.length == 0) return res;
Boolean flag = false;
while(left <= right) {
int mid = left + (right - left)/2;
if(nums[mid] == target) {
//相等的时候,继续往左移才能找左边界
flag = true;
right = mid - 1;
}else if(nums[mid] > target) {
right = mid - 1;
}else {//nums[mid] < target
left = mid + 1;
}
}
//左边界,是right不断往左移,所以会停在不是target的最后一个元素,所以需要加1
if(flag) res[0] = right + 1;
同理,右边界,是left不断往右移,所以会停在比target大的第一个元素,所以需要减1。注意在进行有边界的while之前,需要重新初始化。left = 0;right = nums.length - 1;flag = false;
所以看到一种不需要定义这个flag的方法:定义了一个变量记录左边界的first = middle;或者右边界的last = middle;因为我之前用flag是怕没找到我还返回了right + 1或者left - 1;这样就不会了。
// 两次二分查找,分开查找第一个和最后一个
// 时间复杂度 O(log n), 空间复杂度 O(1)
// [1,2,3,3,3,3,4,5,9]
public int[] searchRange2(int[] nums, int target) {
int left = 0;
int right = nums.length - 1;
int first = -1;
int last = -1;
// 找第一个等于target的位置
while (left <= right) {
int middle = (left + right) / 2;
if (nums[middle] == target) {
first = middle; //重点是定义了一个变量存当前的值
right = middle - 1;
} else if (nums[middle] > target) {
right = middle - 1;
} else {
left = middle + 1;
}
}
// 最后一个等于target的位置
left = 0;
right = nums.length - 1;
while (left <= right) {
int middle = (left + right) / 2;
if (nums[middle] == target) {
last = middle; //重点是定义了一个变量存当前的值
left = middle + 1; //重点
} else if (nums[middle] > target) {
right = middle - 1;
} else {
left = middle + 1;
}
}
return new int[]{first, last}; //不需要提前初始化数组
}
Code
class Solution {
public int[] searchRange(int[] nums, int target) {
//定义返回需要的数字
int[] res = {-1,-1};
int left = 0, right = nums.length - 1;
if(nums.length == 0) return res;
Boolean flag = false;
while(left <= right) {
int mid = left + (right - left)/2;
if(nums[mid] == target) {
//相等的时候,继续往左移才能找左边界
flag = true;
right = mid - 1;
}else if(nums[mid] > target) {
right = mid - 1;
}else {//nums[mid] < target
left = mid + 1;
}
}
//左边界,是right不断往左移,所以会停在不是target的最后一个元素,所以需要加1
if(flag) res[0] = right + 1;
//开始找右边界
left = 0;
right = nums.length - 1;
flag = false;
while(left <= right) {
int mid = left + (right - left)/2;
if(nums[mid] == target) {
flag = true;
left = mid + 1;
}else if(nums[mid] < target) {
left = mid + 1;
}else {//nums[mid] > target
right = mid - 1;
}
}
//右边界,是left不断往右移,所以会停在比target大的第一个元素,所以需要减1
if(flag) res[1] = left - 1;
return res;
}
}
不需要写两次的做法
但是有人写了做法是找到之后直接开始左移,比如[1,2,2,2,3,4],找到nums[2] = 2,开始左移,while(mid>0 && nums[–mid] == target) result[0] = mid;它是直接在当前mid索引减1开始判断的,所以这个循环结束,mid会是0。
然后while(mid<nums.length-1 && nums[++mid] == target) result[1] = mid;是从mid+1开始判断的,直接走到是target值的最后。注意,mid的范围判断一定是在前面,这样取nums[mid]才不会报错。
如果不是target就正常像二分法一样更新。
class Solution {
public int[] searchRange(int[] nums, int target) {
int l = 0, r = nums.length - 1;
int result[] = new int[]{-1, -1};
while(l <= r){
int mid = l + (r - l) / 2;
if (nums[mid] == target){
result[0] = mid;
result[1] = mid;
while(mid>0 && nums[--mid] == target) result[0] = mid;
while(mid<nums.length-1 && nums[++mid] == target) result[1] = mid;
break;
}
else if (nums[mid] < target) l = mid + 1;
else r = mid - 1;
}
return result;
}
}
这里,如果后减减会出错,在Java中,mid--
是一个后减操作,意味着它会在表达式求值后才减少mid
的值。所以在这个条件中 mid-- >= 0
,首先会检查mid
是否大于等于0,然后才会将mid
的值减1。这会导致在mid
等于0时,表达式仍然求值,然后mid
变成-1,这可能导致在数组索引中出现负数,从而引发ArrayIndexOutOfBoundsException
。
为了避免这个问题,你应该在减少mid
的值之前检查它是否大于0。你可以使用前减操作(--mid
)来确保在比较之前减少mid
的值:
while(--mid >= 0 && target == nums[mid]) {
// 逻辑处理
}
在这个修正中,--mid
将首先减少mid
的值,然后再进行比较,这样就不会出现mid
为负数的情况,从而避免了潜在的异常。
但是上面这样写,还是有问题的,因为如果起始的位置要拿到就拿不到。
所以最优解里的条件判断是最正确的:
if (nums[mid] == target){
result[0] = mid;
result[1] = mid;
// 下面两个while里面的条件要仔细记住
while(mid>0 && nums[--mid] == target) result[0] = mid;
while(mid<nums.length-1 && nums[++mid] == target) result[1] = mid;
break;
}