[数据结构与算法] 二分查找II —变形问题

几种二分查找的变形问题。

🧨🧨“十个二分九个错”。注意:终止条件、区间上下界更新方法、返回值选择。🧨🧨

常见问题

下述变形问题的前提是数据均以从小到大排序。

变形一:查找第一个值等于给定值的元素🎈

二分查找最简单的一种即有序数据集合中不存在重复的数据,在其中查找值等于某个给定值的数据。 将这个问题修改为:有序数据集合中存在重复的数据,找到第一个值等于给定值的数据,如下有序数组,其中,a[5],a[6],a[7]的值都等于 8,是重复的数据,要查找第一个等于 8 的数据,也就是下标是 5 的元素。—— 最简单的二分查找需要进行修改
数组元素

// 方法一
/*
	目标:找到第一个元素为value的值
	具体的实现逻辑:当 mid == value 时,高位还会往左移继续去找第一个,那么势必会带来的问题是即使找到了第一个值为value的元素,
	高位还是会往左移动,但是下一次判断的时候数组中已经没有值为value的值了,这时候代码会继续循环一直到低位加一后大于高位,这时
	候会跳出循环,而此时的低位正好是高位上一个经过的值为value的第一个元素,最后的 >n 判断是为了处理第一次就找到value的边界问
	题。 
	二分查找在最差情况下的时间复杂度为logn,这段代码因为会一直循环直到low>high,所以它的时间复杂度稳定是logn。
	边界控制需要注意。
*/
public int binarySearch(int[] a, int n, int value) {
  int low = 0;
  int high = n - 1;
  while (low <= high) {
    int mid = low + ((high - low) >> 1);
    if (a[mid] >= value) {
      high = mid - 1;
    } else {
      low = mid + 1;
    }
  }

  if (low < n && a[low] == value) return low;
  else return -1;
}
// 方法二
/*
	求解中间位置的值不对两个位置值相加防止数据溢出,同时使用位运算而不是用除法来加快处理速度。
*/
public int binarySearch(int[] a, int n, int value) {
  int low = 0;
  int high = n - 1;
  while (low <= high) {
    int mid =  low + ((high - low) >> 1);
    if (a[mid] > value) {
      high = mid - 1;
    } else if (a[mid] < value) {
      low = mid + 1;
    } else {
      if ((mid == 0) || (a[mid - 1] != value)) return mid;
      else high = mid - 1;
    }
  }
  return -1;
}
  • a[mid]跟要查找的 value 的大小关系有三种情况:大于、小于、等于。
  • 对于 a[mid] > value 的情况,需要更新 high= mid-1;
  • 对于 a[mid]<value 的情况,需要更新 low=mid+1。
  • 当 a[mid] = value 时进行如下操作:
    • 如果查找的是任意一个值等于给定值的元素,当 a[mid]等于要查找的值时,a[mid]就是要找的元素。
    • 如果求解的是第一个值等于给定值的元素,当 a[mid]等于要查找的值时,需要确认当前位置是不是第一个值等于给定值的元素。
      • 如果 mid == 0,该元素已经是数组的第一个元素,则肯定是要找的结果;
      • 如果 mid != 0,但 a[mid] 的前一个元素 a[mid-1] != value,则说明 a[mid] 是要找的第一个值等于给定值的元素。
      • 如果 a[mid] 前面的一个元素 a[mid-1] == value,那说明此时的 a[mid] 不是要查找的第一个值等于给定值的元素。则更新 high=mid-1,因为要找的元素会出现在 [low, mid-1] 之间。

变形二:查找最后一个值等于给定值的元素🎈

public int binarySearch(int[] a, int n, int value) {
  int low = 0;
  int high = n - 1;
  while (low <= high) {
    int mid =  low + ((high - low) >> 1); // 求中值
    if (a[mid] > value) {
      high = mid - 1;
    } else if (a[mid] < value) {
      low = mid + 1;
    } else {
      /*
      如果 a[mid] 这个元素已经是数组中的最后一个元素了,那它肯定是要找的结果;
      如果 a[mid] 的后一个元素 a[mid+1] != value,说明 a[mid]就是要找的最后一个值等于给定值的元素。
      如果a[mid]后面的一个元素 a[mid+1] == value,说明当前的这个 a[mid]并不是最后一个值等于给定值的元素,更新 low=mid+1,因为要找的元素肯定出现在[mid+1, high]之间。
      */
      if ((mid == n - 1) || (a[mid + 1] != value)) return mid;
      else low = mid + 1;
    }
  }
  return -1;
}

变形三:查找第一个大于等于给定值的元素🎈

在有序数组中,查找第一个大于等于给定值的元素。比如,数组序列为:3,4,6,7,10。如果查找第一个大于等于 5 的元素,那就是 6。

public int bsearch(int[] a, int n, int value) {
  int low = 0;
  int high = n - 1;
  while (low <= high) {
    int mid =  low + ((high - low) >> 1);
    /*
    对于 a[mid] < value,那要查找的值在 [mid+1, high] 之间,更新 low=mid+1。
    对于 a[mid] >= value,要查看下这个 a[mid] 是不是 第一个值大于等于给定值的元素 。
    	如果 a[mid] 前面已经没有元素,或者 前面一个元素 < value,那 a[mid]就是要找的元素。
    	如果 a[mid-1] >= value,则要查找的元素在[low, mid-1]之间,更新high = mid-1。
    */
    if (a[mid] >= value) {
      if ((mid == 0) || (a[mid - 1] < value)) return mid;
      else high = mid - 1;
    } else {
      low = mid + 1;
    }
  }
  return -1;
}

变形四:查找最后一个小于等于给定值的元素🎈

在有序数组中,查找最后一个小于等于给定值的元素。比如,数组序列为:3,5,6,8,9,10。最后一个小于等于 7 的元素就是 6。

public int bsearch7(int[] a, int n, int value) {
  int low = 0;
  int high = n - 1;
  while (low <= high) {
    int mid =  low + ((high - low) >> 1);
    if (a[mid] > value) {
      high = mid - 1;
    } else {
      if ((mid == n - 1) || (a[mid + 1] > value)) return mid;
      else low = mid + 1;
    }
  }
  return -1;
}

补充题目🧨🧨

33. 搜索旋转排序数组

此处有分析。

34. 在排序数组中查找元素的第一个和最后一个位置

  • 由于数组已经排序,整个数组是单调递增的,可以利用二分法来加速查找的过程。
  • 考虑target 开始和结束位置,要找的就是数组中「第一个等于 target 的位置」(记为 startIndex)和「第一个大于target 的位置减一」(记为 endIndex)。
  • 二分查找中,startIndex 即为在数组中寻找第一个大于等于 target 的下标,寻找 endIndex 即为在数组中寻找第一个大于target 的下标,然后将下标减一。
  • 两者的判断条件不同,为了代码的复用,定义 binarySearch(nums, target, isFirstIndex) 表示在 nums 数组中二分查找target 的位置,如果 isFirstIndex为 true,则查找第一个大于等于 target 的下标,否则查找第一个大于 target 的下标。
  • 因为target 可能不存在数组中,需要重新校验得到的两个下标 startIndex 和 endIndex,查看是否符合条件,如果符合条件就返回 [startIndex, endIndex],不符合就返回 [−1,−1]。
class Solution {
    public int[] searchRange(int[] nums, int target) {

        // 要降低查找的复杂度,则选取二分查找的方法
        // 要找到开始和结束的位置,使用布尔值控制是否是查找第一个出现的位置
        int startIndex = binarySearch(nums, target, true);
        int endIndex = binarySearch(nums, target, false) - 1;
        // 判定数据是否合法
        if((startIndex <= endIndex) && (endIndex < nums.length) && (nums[startIndex] == target) && (nums[endIndex] == target)){
            return new int[] {startIndex, endIndex};
        }

        return new int[] {-1,-1};
    }

    // 对于&,&操作符两端的表达式都要执行。
    // 对于&&, 假如说&&左端的表达式的值为false,那么&&右端的表达式就不会执行,此时已经能够判断整个表达式的结果为false,
    // 这样做可以少执行一些语句,提高效率;只有当左端的表达式的为真时,才需要判断右端的表达式。
    // 利用一个布尔型值来控制是否是查找第一个target出现的位置
    public int binarySearch(int[] nums, int target, boolean isFirstIndex){
        // 初始化起始,末尾和中间值的下标
        int mid = 0;
        int start = 0;
        int end = nums.length - 1;
        int indexAns = nums.length;
        
        while(start <= end){
            mid = start + ((end - start) >> 1);

            if(nums[mid] > target || (isFirstIndex && (nums[mid] >= target))){
                end = mid - 1;
                indexAns = mid;
            }else{
                start = mid + 1;
            }
        }

        return indexAns;
    }
}
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值