二分查找(折半查找)详解

前提

区间(数组或其他)是单调(有序)的。

更多精彩内容,请关注微信公众号:程序员小熊

主要思路

将区间分成三部分,[low, mid)、[mid, mid](只有一个值即中点 (mid))和 (mid, high] ,比较中点 mid 对应的元素值与目标元素值 target 的大小关系,等于 target,代表找到了,直接返回,否则就在(小于target)前半区间 [low, mid) 或(大于target)后半区间(mid, high] 中查找。

具体操作如下:

  1. 判断区间中点 mid  对应的元素的值是否等于目标元素 target 的值,相等则代表查找到,直接返回结束查找;
  2. 不相等,则比较 mid 对应的元素的值与目标元素 target 的值的大小关系,如果小于 target ,就在后半区间 (mid, high] 中查找(将 low 赋值为 mid 加 1);
  3. 否则就在前半区间 [low, mid) 中查找(将 high 赋值为 mid 减 1);
  4. 重复这个过程,直到找到目标元素或者查找区间/范围为空。

二分查找模板

非递归版本

int binarySearch(int* nums, int numsSize, int target) {
    if (nums == NULL || numsSize <= 0) {
        return -1;
    }

    int low = 0, high = numsSize - 1;
    while (low <= high) {                       // 这里取左闭右闭区间,即[left, right]
        int mid = low + ((high - low) >> 1);    // 防止整型溢出
        if (nums[mid] == target) {              // 找到跟目标值相等的元素,直接返回下标
            return mid;
        } else if (nums[mid] > target) {        // 数组元素值大于目标值,取左侧 [left, mid) 查找
            high = mid - 1;
        } else if (nums[mid] < target) {        // 元素值小于目标值,取右侧 (mid, right] 查找
            low = mid + 1;                      // 写成 else if 主要是为了把判断条件描述清晰
        }
    }

    return -1;
}

递归版本

int binarySearch(int* nums, int low, int high, int target) {
    if (low > right) {
        return -1;
    }

    int mid = low + ((high - low) >> 1);
    if (nums[mid] > target) {
        return binarySearch(nums, low, mid - 1, target);
    } else if (nums[mid] < target) {
        return binarySearch(nums, mid + 1, high, target);
    }  else if (nums[mid] == target) {
        return mid;
    }    
}

注意点

  1. mid 的取值,为何是 low + ((high - low) >> 1) 或者 low + (high - low) / 2 而不是 (high + low) / 2
    因为 int 类型最大表示范围是 2147483647 ,那么对于两个都接近 2147483647 的数字而言,它们相加的结果将会溢出,变成负数。所以如果想避免出现整数溢出的风险,使用前面两个替代

  2. while 循环的条件到底是 <= 还是 <
    high 初始值不同,两个可能出现在不同功能的二分查找中。主要区别:
    前者的查找区间两端都是闭区间 [low, high] ,high = numsSize - 1 。
    后者的查找区间是左闭右开区间 [low, high) ,high = numsSize。

  3. while 循环何时终止
    当搜索区间为空或者目标元素找到,就结束循环。只考虑搜索区间的话,有如下两种情况:
    while (low <= high) 终止条件是 low == high + 1 ,即[high + 1, high],此时搜索区间为空,结束循环;
    while (low < high) 终止条件是 low == high ,即[high, high],此时搜索区间不为空,因为还存在一个元素 high ,暂时还不能结束循环。

当有多个目标元素的时候,如何查找左右边界?

例如在数组 nums = [5, 7, 7, 8, 8, 10] 中查找 target = 8 的左右边界,左侧边界下标为 3,右边界下标为 4 ,这是如何查找到的呢?

查找左侧边界

主要思想

数组的中间下标对应的元素值等于目标值 target (即nums[mid] == target)时,不立即返回(数组中可能存在多个元素的值等于目标值),而是缩小搜索区间的上界 high ,不断向左收缩,最后达到锁定左侧边界的目的
如下图所示:

由于 nums[mid] = 7 < target, 所以需要在右半区间((mid, high])中查找,即 low = mid + 1, mid = low + ((high - low) >> 1)

此时,由于 nums[mid] = 8 == target, 不立即返回而是缩小搜索区间上界,即 high = mid - 1, mid = low + ((high - low) >> 1)

由于 nums[mid] = 8 == target, 继续搜索区间上界,即high = mid - 1,此时 low > high ,跳出循环(low = high + 1)

代码

int GetTargetFirstPosition(int* nums, int numsSize, int target) {
    int low = 0, high = numsSize - 1;
    while (low <= high) {
        int mid = low + ((high - low) >> 1);
        /* 找到目标值 target 时,不立即返回,缩小搜索区间的上界 high ,不断向左收缩,锁定左侧边界 */
        if (nums[mid] == target) {
            /* 在区间 [low, mid - 1] 中查找 */
            high = mid - 1;
        } else if (nums[mid] > target) {
            high = mid - 1;
        } else if (nums[mid] < target) {
            low = mid + 1;
        }
    }
    /* 循环结束条件 low = high + 1 ,由于 low 的取值范围是 [0, numsSize],如果目标值 target
       比所有数都大或者没被找到(例如在数组 [5,7,7,8,8,10] 中查找 11 或 6),返回 -1 */
    if (low == numsSize || nums[low] != target) {
        return -1;
    }

    return low;
}

查找右侧边界

主要思想

同理,在找到目标值 target 时,不立即返回,增大搜索区间的下界 low ,不断向右收缩,最后达到锁定右侧边界目的

代码

int GetTargetLastPosition(int* nums, int numsSize, int target) {
    int low = 0, high = numsSize - 1;
    while (low <= high) {
        int mid = low + ((high - low) >> 1);
        if (nums[mid] == target) {
            low = mid + 1;
        } else if (nums[mid] > target) {
            high = mid - 1;
        } else if (nums[mid] < target) {
            low = mid + 1;
            
        }
    }
    /* 目标值 target 比所有数都小或者没被找到 */
    if (low == 0 || nums[high] != target) {
        return -1;
    }

    return high;
}

更多精彩文章,请关注微信公众号:程序员小熊

猜你喜欢

转载自blog.csdn.net/Tanyongyin/article/details/109884006