【力扣刷题-数组篇】34. 在排序数组中查找元素的第一个和最后一个位置的思路和Java详细代码

本文详细介绍了如何使用二分查找算法在已排序的数组中寻找目标值的第一个和最后一个出现位置,提供两种解决方案,并强调了最优解中的关键细节,包括边界判断和避免数组越界的问题。
摘要由CSDN通过智能技术生成

【力扣刷题-数组篇】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;
            }
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值