详解二分搜索

一、应用场景

  1. 寻找一个数是否在数组中
  2. 寻找该数在数组中的最左边界
  3. 寻找该数在数组中的最右边界

二、算法框架

int binarySearch(int[] nums, int target) {
    //数组判断
    if (nums == null || nums.length == 0) {
        return -1;
    }
    int left = 0;
    //区间选择:左闭右闭|左闭右开
    int right = ...;
    //区间不同,判断条件不同,不过要求是不能漏过数组中的每个元素
    //采用多个 if else 为了保证逻辑清晰
    while (...){
        //防止 left 与 right 过大,相加之后直接整数溢出
        int mid = left + (right - left) / 2;
        //不同目的进行不同的操作
        if (nums[mid] > target) {
            right = ...;
        } else if (num[mid] == target) {
            ...;
        } else if (num[mid] < target) {
            left = ...;
        }
    }
    //循环外的操作
    ...;
    return ...;
}

三、具体算法实现

  1. 寻找一个数是否在数组中
    这个场景是最简单的,搜索一个数是否在数组中,存在的话返回数组下标索引,不存在的话返回-1。
    int binarySearch(int[] nums, int target) {
        //数组判断
        if (nums == null || nums.length == 0) {
            return -1;
        }
        int left = 0;
        //左闭右闭
        int right = nums.length - 1;
        //注意循环条件
        while (left <= right){
            int mid = left + (right - left) / 2;
            if (nums[mid] > target) {
                right = mid - 1;
            } else if (num[mid] == target) {
                return mid;
            } else if (num[mid] < target) {
                left = mid + 1;
            }
        }
        //退出循环没有返回表示在数组中不存在
        return -1;
    }
  1. 寻找该数在数组中的最左边界
    该算法有两种实现方法,主要是区别在区间的闭合情况:左闭右闭左闭右开

左闭右闭

    //左闭右闭,最左边界
    int binarySearch(int[] nums, int target) {
        //数组判断
        if (nums == null || nums.length == 0) {
            return -1;
        }
        int left = 0;
        //左闭右闭
        int right = nums.length - 1;
        //注意循环条件
        while (left <= right){
            int mid = left + (right - left) / 2;
            if (nums[mid] > target) {
                right = mid - 1;
            } else if (num[mid] == target) {
                //不直接返回,向左侧边界逼近
                right = mid - 1;
            } else if (num[mid] < target) {
                left = mid + 1;
            }
        }
        //对边界情况进行处理
        if (nums[left] != target || left == nums.length) {
            return -1;
        }
        return left;
    }

左闭右开

//左闭右闭,最左边界
int binarySearch(int[] nums, int target) {
    //数组判断
    if (nums == null || nums.length == 0) {
        return -1;
    }
    int left = 0;
    //左闭右开
    int right = nums.length;
    //注意循环条件
    while (left < right){
        int mid = left + (right - left) / 2;
        if (nums[mid] > target) {
            right = mid;
        } else if (num[mid] == target) {
            //不直接返回,向左侧边界逼近
            right = mid;
        } else if (num[mid] < target) {
            left = mid + 1;
        }
    }
    //对边界情况进行处理
    if (nums[left] != target || left = nums.length) {
        return -1;
    }
    return left;
}

处理边界情况

边界情况分为两种:

  • target比数组中所有数值都大;
  • target比数组中所有数值都小。

我们具体来看一下两种边界情况:

target比数组中所有数值都大

左闭右闭

左闭右闭-target大于nums中所有元素

左闭右开

左闭右开-target大于nums中所有元素

target=7时,返回-1的条件均为left=nums.length

target比数组中所有数值都小

左闭右闭

左闭右闭-target小于nums中所有元素

左闭右开

左闭右开-target小于nums中所有元素

target=-1时,返回-1的条件均为nums[left] != target

如果不处理以上两种情况,返回的left下标(前者返回nums.length,后者返回0)就是错误的。

  1. 寻找该数在数组中的最右边界
    该情况也有两种实现:左闭右闭左闭右开
    左闭右闭
    //左闭右闭,最右边界
    int binarySearch(int[] nums, int target) {
        //数组判断
        if (nums == null || nums.length == 0) {
            return -1;
        }
        int left = 0;
        //左闭右开
        int right = nums.length - 1;
        //注意循环条件
        while (left <= right){
            int mid = left + (right - left) / 2;
            if (nums[mid] > target) {
                right = mid - 1;
            } else if (num[mid] == target) {
                //不直接返回,向右侧边界逼近
                left = mid + 1;
            } else if (num[mid] < target) {
                left = mid + 1;
            }
        }
        //对边界情况进行处理
        if (right < 0 || nums[right] != target) {
            return -1;
        }
        return right;
    }

注意:代码中的判断条件不可颠倒,否则会发生数组越界

    if (right < 0 || nums[right] != target) {
        return -1;
    }

左闭右开

    //左闭右开,最右边界
    int binarySearch(int[] nums, int target) {
        //数组判断
        if (nums == null || nums.length == 0) {
            return -1;
        }
        int left = 0;
        //左闭右开
        int right = nums.length;
        //注意循环条件
        while (left < right){
            int mid = left + (right - left) / 2;
            if (nums[mid] > target) {
                right = mid;
            } else if (num[mid] == target) {
                //不直接返回,向右侧边界逼近
                left = mid + 1;
            } else if (num[mid] < target) {
                left = mid + 1;
            }
        }
        //对边界情况进行处理
        if (right == 0 || nums[right - 1] != target) {
            return -1;
        }
        return right - 1;
    }

处理边界情况

边界情况分为两种:

  • target比数组中所有数值都大;
  • target比数组中所有数值都小。
    我们具体来看一下两种边界情况:
target比数组中所有数值都大

左闭右闭

左闭右闭-target大于nums中所有元素

target=7时,返回-1的条件为nums[right]!=target
左闭右开

左闭右开-target大于nums中所有元素

target=7时,返回-1的条件为nums[right-1]!=target

target比数组中所有数值都小

左闭右闭

左闭右闭-target小于nums中所有元素

target=-1时,返回-1的条件为right < 0
左闭右开

左闭右开-target小于nums中所有元素

target=-1时,返回-1的条件为right = 0
注意:两种情况下返回-1的条件不同
如果不处理以上两种情况,返回的下标就是错误的。

逻辑总结

在查找最左边界和最右边界的时候通常使用左闭右开的方式,我们来总结一下导致代码细节发生变化的因果关系:

  1. 最基本的⼆分查找算法:

    因为我们初始化 right = nums.length - 1
    所以决定了我们的「搜索区间」是 [left, right]
    所以决定了 while (left <= right)
    同时也决定了 left = mid+1 和 right = mid-1
    因为我们只需找到⼀个 target 的索引即可
    所以当 nums[mid] == target 时可以⽴即返回

  2. 寻找左侧边界的⼆分查找:

    因为我们初始化 right = nums.length
    决定了我们的「搜索区间」是 [left, right)
    所以决定了 while (left < right)
    同时也决定了 left = mid + 1 和 right = mid
    我们需找到 target 的最左侧索引
    所以当 nums[mid] == target 时不要⽴即返回
    ⽽要收紧右侧边界向左侧边界压缩以锁定左侧边界

  3. 寻找右侧边界的⼆分查找:

    因为我们初始化 right = nums.length
    所以决定了我们的「搜索区间」是 [left, right)
    所以决定了 while (left < right)
    同时也决定了 left = mid + 1 和 right = mid
    因为我们需找到 target 的最右侧索引
    所以当 nums[mid] == target 时不要⽴即返回
    ⽽要收紧左侧边界向右侧边界压缩以锁定右侧边界
    因为收紧左侧边界时必须 left = mid + 1
    所以最后⽆论返回 left 还是 right,必须减⼀

你可能感兴趣的:(详解二分搜索)