详解二分查找,包括左闭右闭,左闭右开,以及其他变种问题

详解二分查找,包括左闭右闭,左闭右开,以及其他变种问题

1. 二分查找的应用条件

要想使用二分查找,必须满足一下条件:

  • 必须为有序数组
  • 数组中的元素不能重复

2. 二分查找中的难点

二分查找看起来非常好理解,但是如果想把代码实现出来,还是有一定难度的。最大的难度在于边界的控制。

  • 例如到底是 while(left < right) 还是 while(left <= right)
  • 到底是right = middle呢,还是要right = middle - 1

要回答上述的问题,就必须搞明白二分查找中的两种方法,一种是左闭右闭区间,还有一种是左闭右开区间。我们说的这个区间,就是二分查找的区间范围。举个例子:有一个数组[1, 2, 3, 4],它的下标集合为[0, 1, 2, 3]。此时如果使用左闭右闭区间搜索,那么搜索的区间为[0, arr.length-1]。如果采用左闭右开的区间,那么搜索范围是[0, arr.length]。

这两种不同的搜索区间对应了二分查找的两种代码形式。

3. 左闭右闭区间

第一种写法,我们定义 target 是在一个在左闭右闭的区间里,也就是[left, right] (这个很重要非常重要)

区间的定义这就决定了二分法的代码应该如何写,因为定义target在[left, right]区间,所以有如下两点:

  • while (left <= right) 要使用 <= ,因为left == right是有意义的,所以使用 <=
  • if (nums[middle] > target) right 要赋值为 middle - 1,因为当前这个nums[middle]一定不是target,那么接下来要查找的左区间结束下标位置就是 middle - 1
/**
 * 搜索区间是左闭右闭的情况
 * @param nums
 * @param target
 * @return
*/
public int search(int[] nums, int target) {
    if (nums == null || nums.length == 0)
      return -1;
    // 这里使用的是左闭右闭区间
    int left = 0, right = nums.length - 1;
    // 对于左闭右闭区间 因为left == right 也是合法的,因此需要<=
    while (left <= right) {
        int mid = (left + right) / 2;
        if (nums[mid] > target) {
          // mid已经不满足条件,需要排出掉
          // 为什么要-1 而不是right = mid
          // 如果right=mid,那么实际上下一次的搜索区间为[left, mid]
          // 下一次搜索区间依然包含mid,而实际上mid已经不满足要求了
          right = mid - 1;
        } else if (nums[mid] < target) {
            left = mid + 1;
        } else {
            return mid;
        }
    }
    return -1;
}

4. 左闭右开区间

如果说定义 target 是在一个在左闭右开的区间里,也就是[left, right) ,那么二分法的边界处理方式则截然不同。

有如下两点:

while (left < right),这里使用 < ,因为left == right在区间[left, right)是没有意义的
if (nums[middle] > target) right 更新为 middle,因为当前nums[middle]不等于target,去左区间继续寻找,而寻找区间是左闭右开区间,所以right更新为middle,即:下一个查询区间不会去比较nums[middle]

/**
 * 搜索区间是左闭右开的情况
 * @param nums
 * @param target
 * @return
*/
public int search(int[] nums, int target) {
    if (nums == null || nums.length == 0)
        return -1;
    // 这里使用的是左闭右开区间
    int left = 0, right = nums.length;
    // 对于左闭右闭区间 因为left == right 是不合法的,因此需要<
    while (left < right) {
        int mid = (left + right) / 2;
        if (nums[mid] > target) {
            // mid已经不满足条件,需要排出掉
            // 为什么要right = mid 而不是right = mid -1
            // 如果right=mid,那么实际上下一次的搜索区间为[left, mid)
            // [left, mid)实际上已经把mid排出在外了
            right = mid;
        } else if (nums[mid] < target) {
            left = mid + 1;
        } else {
            return mid;
        }
    }
    return -1;
}

如何判断right = mid还是 right = mid -1

通俗的来说,因为我们通过nums[middle] > target条件已经判断了middle下标对应的元素实际上是不满足我们要查找的条件的,因此我们需要在下一次查找的区间中不能包含middle下标对应的元素,那么

  • 如果我们的搜索区间是左闭右闭的,如果mid = right,则搜索区间为[left, mid],显然包含了mid下标对应的元素,不符合题意,所以mid = right - 1
  • 如果我们的搜索区间是左闭右开的,如果mid = right,则搜索区间为[left, mid),搜索区间实际上已经把mid下标对应的元素排出在外了,满足题意

4. 二分查找变种问题

求第一个大于等于target的下标(等价于求第一个等于target的下标)

// 查找第一个大于等于target的下标 === 查找第一个等于target的下标
public int equalAndGreater(int[] array, int k) {
    if (array == null || array.length == 0) return -1;

    int left = 0, right = array.length - 1;

    while (left <= right) {
      int mid = (right + left) / 2;
      if (array[mid] < k) {
        left = mid + 1;
      } else {
        right = mid - 1;
      }
    }
    return left;
}

求第一个大于target的下标(等价于求最后一个target元素下标+1)

// 查找第一个大于target的下标-1 === target的最后下标
public int greater(int[] array, int k) {
    if (array == null || array.length == 0) return -1;
    int left = 0, right = array.length - 1;
    while (left <= right) {
      int mid = (right + left) / 2;
      if (array[mid] <= k) {
        left = mid + 1;
      } else {
        right = mid - 1;
      }
    }
    return left;
}

查找第一个小于target的值的下标

public int binarySearchOne() {
   int[] array = {0,1,2,3,4,5,6};
   int target = 3;
	int left = 0, right = array.length-1;
	while(left <= right) {
	    int mid = (left + right)/2;
    //只修改了判断的条件,相当于将大于等于归为一类。
		if(array[mid] >=  target)
            right = mid - 1;
		else
		     left = mid + 1;
	}
	return right;
}

查找第一个小于等于target值的下标

public int binarySearchOne() {
   int[] array = {0,1,2,3,4,5,6};
   int target = 3;
	int left = 0, right = array.length-1;
	while(left <= right) {
	    int mid = (left + right)/2;
    //只修改了判断的条件,相当于将大于等于归为一类。
		if(array[mid] >  target)
            right = mid - 1;
		else
		     left = mid + 1;
	}
	return right;
}

我们可以看出来,这几个代码实际修改的地方就只有array[mid]和target的判断条件。实际上,这个条件和要求的条件正好为补集。

  • 求大于target的,就是array[mid]小于等于target
  • 求大于等于target的,就是array[mid]小于target
  • 求小于target的,就是array[mid]大于等于target
  • 求小于等于target的,就是array[mid]大于target

相关博客资料https://blog.csdn.net/qq_29611345/article/details/103080287

你可能感兴趣的:(算法题,java,算法,数据结构)