二分查找

题目

力扣704题目链接

给定一个 n 个元素有序的(升序)整型数组 nums 和一个目标值 target ,写一个函数搜索 nums 中的 target,如果目标值存在返回下标,否则返回 -1。

示例 1:

输入: nums = [-1,0,3,5,9,12], target = 9     
输出: 4       
解释: 9 出现在 nums 中并且下标为 4     

示例 2:

输入: nums = [-1,0,3,5,9,12], target = 2     
输出: -1        
解释: 2 不存在 nums 中因此返回 -1        

提示:

  • 你可以假设 nums 中的所有元素是不重复的。
  • n 将在 [1, 10000]之间。
  • nums 的每个元素都将在 [-9999, 9999]之间。

初见思路

虽然这套题有明确的考察点,但是我习惯拿到题先用最笨的办法实现一次,然后再改进算法

这题整体还是比较简单的,如果用常规的思路也很容易做出来

例如:遍历数组,找到符合条件的值,然后返回其下标,如果条件均不满足,则返回默认下标值(-1)

class Solution {
    public int search(int[] nums, int target) {
        int index = -1;
        for(int i = 0; i < nums.length; i++){
            if(nums[i] == target){
                index = i;
            }
        }
        return index;
    }
}

那么实际上,本题考察的主要是“查找”数组的过程

显然,遍历的方式对于二分法来说,是非常低效的,因此肯定是要用二分法来写的

常规思路

在升序数组nums中寻找目标值target,对于特定下标 i,比较nums[i]和target的大小

  • 如果nums[i] = target,则下标 i 即为要寻找的下标;
  • 如果nums[i] > target,则 target 只可能在下标 i 的左侧;
  • 如果nums[i] < target,则target只可能在下标 i 的右侧。

基于上述事实,可以在有序数组中使用二分查找寻找目标值。【ps:如果数组是乱序,那么有你可能会找出多个值】

二分法的解题流程如下:
1、定义一个查找的范围,一般用left和right代表左右边界

2、在该范围内取中点middle,比较nums[mid]和target

3、根据比较结果进行返回或继续对某侧进行查找

解题模板

实际上二分法有两种写法,分别是左闭右闭和左闭右开【即是否包含边界点本身】,模板只记一种就行,另外的补充再说

Java版
class Solution {
    public int search(int[] nums, int target) {
        //①定义左右边界
        int left = 0, right = nums.length - 1;
        //②写一个条件循环*
        while (left <= right) {
            //计算区间的中点mid
            int mid = (right - left) / 2 + left;//用减是为了防止数据溢出,加left是为了定位到当前区间
            if (nums[mid] == target) {
                return mid;
            } else if (nums[mid] > target) { //此时,target只能在区间中点的左半部分,因此右边界改变,变为左半部分的右边界
                right = mid - 1;
            } else if(nums[mid] < target) {//target只能在区间中点的右半部分,因此左边界改变,变为右半部分的左边界
                left = mid + 1;
            }
        }
        return -1;//找不到返回-1
    }
}

注释:

边界问题

在上述写法中有两处值得关注的边界,

一处是while的条件left <= right【什么时候要写<、什么时候写<=】,一处是边界改变时的mid - 1【什么时候写mid,什么时候写mid-1】

这两个边界问题都只取决于你决定采用哪种二分法的写法,在一开始我们就必须先确定要用左闭右闭还是左闭右开,然后后面再写的时候要贯彻执行,根据区间的定义去选择边界

以左闭右闭[left, right]为例

当忘了while里应该写<=还是<时,可以想一想一开始确定的区间

比如,如果我写left <= right,对于[left, right]来说是不是合法的呢?【[1,1],1到1的区间且包含1,虽然只有一个元素但至少不非法】

显然还是可以这么写的,所以在左闭右闭时应该写left <= right

类似的,left <= right对于[left, right)则不合法【[1,1),即包含1又不含1?】

那么mid呢?

假设目前你的nums[mid] > target,那么这个target一定不会出现在当前mid的右边区间了

所以下一步应该从左边区间再查找

这时候应该将当前的右边界调整到左边区间上来,并且需要排除掉当前的nums[mid]

因此下次查找的右边界的下标应该是right = mid - 1

如果不减1就相当于把一个不属于下次查找区间的数放到区间中了,对于区间来说是非法的,自然会出错

类似的,right = mid - 1对于[left, right)则不合法,因为[left, right)在下次查找中本来就不包含right,所以应该直接写right = mid【若nums[mid] < target,则[left, right)的left还是需要写成left = mid - 1,因为left在[left, right)中是被包括在内的】

Python版
class Solution:
    def search(self, nums: List[int], target: int) -> int:
        left, right = 0, len(nums) - 1
        while left <= right:
            mid = (right - left)//2 + left
            if nums[mid] == target:
                return mid
            elif nums[mid] < target:
                left = mid + 1
            else:
                right = mid - 1
        return -1

拓展练习

LeetCode35搜索插入位置

思路就是二分查找,这里还是选左闭右闭的方式去写

Java版

class Solution {
    public int searchInsert(int[] nums, int target) {
        int left = 0;
        int right = nums.length - 1;
        while(left <= right){
            int mid = (right - left)/2 + left;
            if(nums[mid] == target){
                return mid;
            }else if(nums[mid] < target){
                left = mid + 1;
            }else{
                right = mid - 1;
            }
        }
        //此处可以同时处理四种情况下的返回值
        //1、目标值等于数组中某一个元素  return middle;
        //2、目标值插入数组中的位置 [left, right],return  right + 1
        //3、目标值在数组所有元素之后的情况 [left, right],这是右闭区间,所以  return right + 1
        //4、目标值在数组所有元素之前  [0, -1]【没想通】
        return right+1;
    }
}

Python版

class Solution:
    def searchInsert(self, nums: List[int], target: int) -> int:
        left, right = 0, len(nums)-1
        while left <= right:
            mid = (right - left)//2 + left
            if nums[mid] == target:
                return mid
            elif nums[mid] < target:
                left = mid + 1 
            else:
                right = mid - 1
                
        return right+1
LeetCode34在排序数组中查找元素的第一个和最后一个位置