深入解析二分查找

在写代码的过程中经常会用到二分查找,不论是刷题还是写业务代码。虽然它的思想简洁,作用强大,但不一定你就能很好的掌握它。过去一直被二分查找和二分查找的变种的边界条件困扰,今天一并整理他们的边界条件。

1. 二分查找

int binary_search_equal(int *nums, int size, int target){
  int temp;
  int left = 0, right = size - 1;
  while(left<= right){
    temp = (left + right) / 2;
    if(nums[temp] == target)
      return temp;
    else if(nums[temp] < target)
      left = temp + 1;
    else
      right = temp - 1;
  }
  return -1;
}

    代码如上。从上面可以看到两个边界条件

    1. 循环条件必须是left <= right。(此处如果去掉 = 会导致最后一个元素无法遍历到)

    2. 每次判断不相等的时候就让 left = temp + 1 或者 right = temp – 1;

2. 二分查找的变种

2.1 查找第一个相等的元素

int binary_serch_equal_first(int *nums, int size, int target){
    int temp;
    int left = 0, right = size - 1;
    while(left <= right){
        temp = (left + right) / 2;
        if(nums[temp]  >= target)
            right = temp - 1;
        else
            left = temp + 1;
    }
    if(left < size && nums[left] == target)
        return left;
    return -1;
}

  代码如上。按照经典二分查找,只是在遇到元素大于等于target时继续从左边找。小于就从右边找。从上面可以看到两个边界条件

    1. 循环条件必须也是left <= right。(此处如果去掉 = 会导致最后一个元素无法遍历到)

    2. 每次遇到元素大于等于target时继续从左边找。小于就从右边找。

    3. 最后处理不存在的情况。

2.2 查找最后一个与key相等的元素

int binary_serch_equal_last(int *nums, int size, int target){
    int temp;
    int left = 0, right = size - 1;
    while(left <= right){
        temp = (left + right) / 2;
        if(nums[temp]  <= target)
            left = temp + 1;
        else
            right = temp - 1;
    }
    if(right > 0 && nums[right] == target)
        return right;
    return -1;
}

  代码如上。思路其实和查找第一个与key相等的元素差不多。只是初始化结果为-1。每次遇到相等的值,就更新结果为为之前结果和当前下标的较大值,并从右边继续查找。

2.3 查找第一个大于key的元素

int binary_search_gt_first(int *nums, int size, int target){
    int temp;
    int left = 0, right = size - 1;
    while(left <= right){
        temp = (left + right) / 2;
        if(nums[temp] <= target)
            left = temp + 1;
        else
            right = temp - 1;
    }
    if(nums[left] > target)
        return left;
    return -1;
}

代码如上。遍历部分和经典二分查找是一样的,在判断部分,小于等于就在右半部分找,大于就在左半部分找。最后会收敛到一个下标。最后对这个下标判断一下就OK了。

 2.4 查找最后一个小于key的元素

int binary_search_lt_last(int *nums, int size, int target){
    int temp;
    int left = 0, right = size - 1;
    while(left <= right){
        printf("%d %d\n", left, right);
        temp = (left + right) / 2;
        if(nums[temp] >= target)
            right = temp - 1;
        else
            left = temp + 1;
    }
    if(nums[right] < target)
        return right;
    return -1;
}

代码如上。遍历部分和经典二分查找是一样的。判断部分,对于大于等于的元素就在左边找,对于小于的元素就在右边找,最后收敛的值在right。最后返回right即可。

3. 总结

由上面可以总结出二分查找及其变种的大概模板

int binary_search(int *nums, int size, int target){
    int temp;
    int left = 0, right = size - 1;
    while(left <= right){
        temp = (left + right) / 2;
        if(nums[temp] OP target){
            //right = temp - 1;
        } 
        else{
            //left = temp + 1;
        }
    }
    return xx;
}

3.1 首先判断返回left 还是right

从上面的模板可以看处,循环的条件是left <= right。那么最后跳出循环应该有right = left – 1。且right和left应该卡在临界值的两边,用上面的例子来判断,数组为 [1,2,3,3,4,4,4,6,7,9]:

1. 第一个等于7 跳出循环时right = 7,对应的值为6; left = 8 对应的值是 7 所以返回left。可能有人在这里和我有一样的疑惑,为什么不可以right = 8,对应的值为7,left=9, 对应的值为9呢?其实你只要想象一下什么叫做卡在临界值两边,如果例子数组中7后面跟的不是9 也是7呢?这样第二种可能是不是就行不通,因为它没有卡在临界值的两边。

2. 最后一个等于4 跳出循环时right = 6 对应的值为 4 left = 7 对应的值为6 所以应该返回 right。

以此类推可以分析其他变种。

3.2 其次判断比较条件

个人觉得这部分比上条判断简单一些。此处举例简单分析一下。

1. 第一个等于    如果当前元素大于目标值,肯定要从左边找,这是毫无疑问的。如果小于,肯定就要从右边找。等于的话由于不确定是不是第一个,所以要继续到左边找。翻译后的代码可以参见 2.1。

2. 最后一个小于  如果当前元素大于等于目标值,肯定不是我们要找的,需继续到左边找。如果小于就要到右边找。翻译后的代码可以参见 2.4。

3.3 最后的下标检查

虽然前面已经完成了大部分内容,但输入为边界值时最后的left 或 right 值 可能还是不符合要求。这个时候我们可以利用上面的第一点去分析最后的边界要怎么处理。这里同样举例说明 ,数组同上,为 [1,2,3,3,4,4,4,6,7,9]。

1. 第一个等于 当key = 0 时,right = -1,left = 0。按之前的直接返回 left 不符合要求,因为对应值为1,所以需要检查left对应的值是否等于target,等于就返回left,不等于说明不存在,返回-1。当key = 10 时 left = 9, right = 10, 此时直接返回 left 会超出数组下标,所以还要判断一下left是否超过范围,超过范围说明不存在符合条件的值。

2. 最后一个小于 按3.1 分析知是返回right。当key = 1时,right = -1, left = 0 对应值为1 此时返回 right 符合要求。当key = 10 时,right = 9,left = 10。此时返回right 也无问题。所以直接返回right即可,无需其他处理。

其实这一部分借助debug工具分析可能更方便,随便选个数组,选几个正常值和边界值输入也能很快发现问题所在。但深入了解也有助于我们去了解问题的本质。

二分查找这部分写到这里基本就结束了,希望能给那些和我一样只了解二分查找基本思想写起来却bug不断的人一些帮助!以后也会继续更新这方面的一些笔记。

点赞