AlgoWiki学习——二分查找

本文是学习「每日一题」组织AlgoWiki的二分查找的学习笔记,本篇二分查找是由weiwei大佬贡献,原文地址:「https://ojeveryday.github.io/AlgoWiki/#/BinarySearch/README」。「每日一题」组织是一起刷题的大家庭,群里有LeetCode网站精品题解的作者weiwei大佬、甜姨和群主。如果有兴趣加入「每日一题」组织,请访问网址:「https://group.ojeveryday.com/#/check」。

二分查找的应用

  • 在有序或者半有序(旋转有序或者是山脉)的数组中查找元素
  • 确定一个有范围的整数
  • 需要查找的目标元素满足某个特定的性质。

二分查找模板

在weiwei大佬的二分查找中,有三个版本的「二分查找」的模板。

  1. 第一个版本的模板,最为好理解,但在实际刷题过程中需要特殊处理一些边界问题,容易出错。
  2. 第二个版本是weiwei大佬较为推荐的版本,在刷题过程中理解其思想后,会觉得非常自然。
  3. 第三个版本是模板二的避免踩坑版本,在理解了第二个版本后,本版本也就不再话下了。

ps:在第二个版本学习过程中,需要考虑的细节较少,可以处理一些比较复杂的问题。但是在初学过程中,可能会陷入死循环,可以通过debug的方式查找死循环的位置,并进一步理解。

模板一

在学习模板一过程中,结合「力扣」题目二分查找进行学习。

思路

在一个有序的数组中查找某一个元素,就好比「猜价格」游戏,当价格猜高之后,我们适当降低价格,当价格猜低之后,则适当抬高价格。

将待搜索的区间,左边界设置为left,右区间设置为right。将带搜索的区间[left, right]可以分为以下三部分:

  • mid位置,仅包含一个元素;
  • [left, mid-1]之间的所有元素;
  • [mid+1, right]之间的所有元素。

于是二分查找就是根据mid=(left+right)/2位置的元素nums[mid]的值与target的关系,来不断改变left或者right的值。

  • 如果nums[mid] == target,则返回mid
  • 如果nums[mid] < target,因为数组有序,则mid位置左侧的所有元素的值均小于target,则令left = mid + 1;
  • 如果nums[mid] > target,,因为数组有序,则mid位置右侧的所有元素的值均大于target,则令right = mid - 1
Java参考代码
class Solution {
    public int search(int[] nums, int target) {
        // 用于特值判断
        int length = nums.length;
        if (length <= 0) {
            return -1;
        }
        int left = 0;
        int right = length - 1;

        while (left <= right) {
            // 为了防止left + right 整形溢出,写成如下形式:left + (right - left) / 2 《=》 (left + right) / 2
            int mid = left + (right - left) / 2;
            if (nums[mid] == target) {
                return mid;
            } else if (nums[mid] < target) {
                // 下一轮搜索区间:[mid + 1, right]
                left = mid + 1;
            } else {
                // 下一轮搜索区间:[left, mid - 1]
                right = mid - 1;
            }
        }

        return -1;
    }
}

考虑不仔细是学习「二分查找」容易出错的地方,这里切记不要跳过某些步骤,需要想清楚每一行代码、每一个步骤的作用:

  • 二分查找是一个典型的「减治思想」的应用,通过不断改变left或者right的值,将每次搜索的区间不断变小,达到「缩减问题规模」的目的。
  • while循环中使用的条件为(left <= right),当left == right时,所需要查找的区间中仅存在一个元素,此时也必须去查找下去。
  • 由于(left + right)的值存在整型范围溢出的可能,所以将mid = (left + right) / 2变化为mid = left + (right - left) / 2,由数学推导可知两式等价。

模板二

这个模板二也是weiwei大佬的力推版本,这个版本在编写时需要考虑的细节比较少,不容易出现错误。

参考模板一
public int search(int[] nums, int left, int right, int target) {
    while (left < right) {
        // 选择中位数时,向下取整
        int mid = left + (right - left) / 2;
        if (check(mid)) {
            // 下一轮搜索区间:[mid + 1, right]
            left = mid + 1;
        } else {
            // 下一轮搜索区间:[left, mid]
            right = mid;
        }
    }

    // 当退出循环时,仅有一个位置没有看到。即left = right的位置
    // 视情况,是否需要单独判断 left (或者 right)这个下标的元素是否符合题意
    return -1;
}
参考模板二
public int search(int[] nums, int left, int right, int target) {
    while (left < right) {
        // 选择中位数时,向上取整
        int mid = left + (right - left + 1) / 2;
        if (check(mid)) {
            // 下一轮搜索区间:[left, mid - 1]
            right = mid - 1;
        } else {
            // 下一轮搜索区间:[mid, right]
            left = mid;
        }
    }

    // 当退出循环时,仅有一个位置没有看到。即left = right的位置
    // 视情况,是否需要单独判断 left (或者 right)这个下标的元素是否符合题意
    return -1;
}

理解代码模板的要点:

  • 核心思想:把待搜索的目标元素放在最后去判断,每次循环排除掉不存在目标元素的区间,目的时确定下一轮搜索的区间;
  • 特征:从循环条件while (left < right)来看,当区间中仅有两个元素时,依然可以进行循环操作。也就是说,当退出循环时,一定有left == right,这一点在确定元素下标位置时,极其有用
  • 由于我们每次排除不存在目标的区间,所以每次只需要判断nums[mid]何时不是目标元素,进而改变leftright的值,缩小搜索区间。

注意事项:

  • 在二分查找时,先考虑什么时候不是解,这是根据经验来看的。
  • 在使用模板二的过程中,可能会出现死循环的问题,此时需要进一步理解算法,可以通过debug的方式查看和思考出现死循环的原因。进而理解何时需要在计算中位数时,在括号里面加1,何时不用加1。
  • 在退出循环后,需要根据实际情况来决定是否需要对leftright的值进行特殊判断,这一步叫「后处理」。在一些问题中,排除了其他不符合要求的元素后,剩下的那1个元素就一定是目标元素。
  • 在使用熟练后,看到left = mid后,它对应取中位数的方法一定是int mid = left + (right - left + 1) / 2
参考例题-「力扣」搜索插入位置

在本题中查找一个目标值,并返回其下标;如果不存在目标值,则返回插入位置的下标值。这也是典型的二分查找类型题目,使用模板2来解决问题。每次排除不符合的区间,逐步缩小区间范围。参考代码如下所示。在代码中需要理解为何在nums[mid] < target时,直接将left = mid + 1,因为在题目中需要找到target的位置或者插入位置,小于目标值的元素所在的位置一定不是我们要找的下标。

class Solution {
    public int searchInsert(int[] nums, int target) {
        int length = nums.length;
        if (nums.length <= 0) {
            return 0;
        }

        // 特殊边界判断
        if (nums[length - 1] < target) {
            return length;
        }

        int left = 0;
        int right = length - 1;

        while (left < right) {
            // 使用右移代替除法
            int mid = (left + right) >>> 1;
            if (nums[mid] < target) {
                left = mid + 1;
            } else {
                right = mid;
            }
        }

        // 退出循环后,一定有left == right
        return left;
    }
}

复杂度分析:

  • 时间复杂度:O(logN),N为数组的长度,因为我们每次将搜索的区间减小为原来的一半,所以时间复杂度是对数级别的。
  • 空间复杂度:O(1),仅使用了mid存储中间位置,为一个常量。

模板三

在weiwei大佬的二分查找文档中,还列举了第三种模板。模板三与模板二很相似,但是需要考虑更多的细节。

public int search(int[] nums, int left, int right, int target) {
    while (left + 1 < right) {
        // 选择中位数时下取整
        int mid = left + (right - left) / 2;
        if (nums[mid] == target){
            return mid; 
        } else if (nums[mid] < target) {
            left = mid;
        } else {
            right = mid;
        }
    }

    if (nums[left] == target) {
        return left;
    }
    if (nums[right] == target) {
        return right;
    }
    return -1;
}

通过观察模板三可以发现与模板二之间的差别:

  • 循环条件的改变:while (left + 1 < right),在退出循环时一定有left + 1 == right,也就是在退出循环后,区间中含有[left,right]两个元素。
  • 在循环条件中left + 1 < right的写法,语义性不如left < rightleft <= right
  • 在退出循环后,由于区间中剩余left与right两个元素,需要进行「后处理」。有些时候还会有先考虑 left 还是 right 的区别。

精品例题

题型1:在有序数组里查找 lower_bound 和 upper_bound
  • lower_bound:查找第一个大于或等于 target 的数字;
  • upper_bound:查找第一个大于 target 的数字。
题型2:确定一个有范围的整数

在确认一个有范围的整数时,自然数本身就是有序的,自然可以使用二分查找进行问题的处理。

参考例题:「力扣」二分查找-69. x 的平方根
题型3:需要查找的目标元素满足某个特定的性质

注意:这种类型的题目,一般判断条件都不是简单的表达式,需要抽象出来为一个单独的函数进行处理。

参考例题:「力扣」二分查找-875. 爱吃香蕉的珂珂

精品例题

题型1:在有序数组里查找元素

需要注意「二分查找」不是只能应用到「有序数组中」,只要是「减治思想」都可以进行应用。

题目tips参考代码
34. 在排序数组中查找元素的第一个和最后一个位置非常是和实用模板2来进行求解力扣「在排序数组中查找元素的第一个和最后一个位置
153. 寻找旋转排序数组中的最小值必做题力扣「寻找旋转排序数组中的最小值
33. 搜索旋转排序数组必做题力扣「二分查找-33. 搜索旋转排序数组
1095. 山脉数组中查找目标值三次二分查找寻找山脉数组目标值力扣「二分查找-「力扣」1095. 山脉数组中查找目标值

未完待续~~

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值