• 【LeetCode 算法专题突破】二分查找(⭐)


    前言

    我刷过不少算法题目,也得过算法竞赛的奖状,但是并没有成体系的总结,或者说学习算法的类型,所以我决定把常见的算法进行一次归类,然后总结每个经典类型的算法的知识重点,加强算法能力,完善算法体系,也希望能对你有所帮助~

    1. 二分经典模板题目

    力扣链接:704. 二分查找

    题目描述

    题目描述
    这道题目是一道非常经典的二分查找的模板题,可以说,所有二分算法入门都离不开这道题目,题目的要求很简单,就是从一段有序的数组中查找 target 值。

    代码:

    func search(nums []int, target int) int {
        left, right := 0, len(nums)-1
        for left <= right {
            mid := left+(right-left)/2
            if nums[mid] < target {
                left = mid+1
            } else if nums[mid] > target {
                right = mid-1
            } else {
                return mid 
            }
        }
        return -1
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14

    我的习惯是每做完一道题目,要能从题目中学习到一些什么,也许是熟练度,也许是对这个算法的理解加深了,那做完这道题目我学到了什么?

    我加深了对二分查找排除区间的理解:

    当 mid 位置的值小于 target(nums[mid] < target ),证明左半边的区间,包括 mid 位置,不可能存在 target 值,所以左区间 left = mid+1;

    当 mid 位置的值大于 target(nums[mid] > target),证明右半边的区间,包括 mid 位置,不可能存在 target 值,所以右区间 right = mid-1;

    当 mid 位置等于 target,那当然就证明找到了,可以 return 了。

    我个人认为这道题目的核心思想,或者说二分的一个思想,就体现在这里

    2. 在排序数组中查找元素的第一个和最后一个位置

    现在你已经学会二分了,来一道变式题试试吧~

    题目链接:34. 在排序数组中查找元素的第一个和最后一个位置

    题目描述


    这道题的难点其实就是怎么求到 target 值的起点和终点,那我们只需要用两次二分查找,分别查找他的起点和终点就行啦

    代码

    func searchRange(nums []int, target int) []int {
        left, right, begin, end := 0, len(nums)-1, -1, -1
        // 求左区间
        for left <= right {
            mid := left+(right-left)/2
            if nums[mid] > target {
                right = mid-1
            } else if nums[mid] < target {
                left = mid+1
            } else {
                begin = mid
                right--
            }
        }
        // 恢复 left 和 right
        left, right = 0, len(nums)-1
        // 求右区间
        for left <= right {
            mid := left+(right-left)/2
            if nums[mid] > target {
                right = mid-1
            } else if nums[mid] < target {
                left = mid+1
            } else {
                end = mid
                left++
            }
        }
        return []int{begin, end}
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30

    这道题目的二分查找使用的方法和模板题是一模一样的,但是为了找到左右区间,我们在遇到 nums[mid] == target 情况,稍微进行了一些调整:

    在找左区间的时候,我们记录下 begin 之后,让 right–,这样 mid 的位置也会不断向左,直到 nums[mid] != target,这样 begin 的值就是左区间

    在找右区间的时候,我们记录下 end 之后,让 left++,这样 mid 的位置也会不断向右,直到 nums[mid] != target,这样 end 的值就是右区间

    3. 有效的完全平方数

    现在你已经学会在有序数组中使用二分查找了,那,如果题目没有给你有序数组,也没有提示你时间复杂度要求呢?作为一个算法菜鸟,我的办法就是:多刷题,见多识广(说人话:你刷过类似的题目不就会做了)

    题目链接:367. 有效的完全平方数

    题目描述


    抛开题目不谈,如果要你来求完全平方数,你会怎么做?那肯定是用原数折半之后乘一下看看对不对的上,对不上就继续折半,为什么这样做?因为这样效率很高~ 既然如此,那我们为什么不用二分做来试试

    代码

    func isPerfectSquare(num int) bool {
        left, right := 0, num
        for left <= right {
            mid := left+(right-left)/2
            sqrt := mid*mid
            if sqrt < num {
                left = mid+1
            } else if sqrt > num {
                right = mid-1
            } else {
                return true
            }
        }
        return false
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15

    咱也没有细想代码的细节,就是简单的把二分算法套进了这个场景,现在来看,以后如果遇到类似的问题,也可以用二分~

    4. 寻找峰值

    那接下来咱们就多刷一点题目,看看哪里还能用二分查找算法,增加我们的二分熟练度和能力~

    题目链接:162. 寻找峰值

    题目描述


    这道题我们连 target 值都没有,他甚至也不是一个有序的序列,我们能用二分算法来做吗?来看看代码

    代码

    func findPeakElement(nums []int) int {
        left, right := 0, len(nums)-1
        for left < right {
            mid := left+(right-left)/2
            if nums[mid] < nums[mid+1] {
                left = mid+1
            } else if nums[mid] > nums[mid+1] {
                right = mid
            }
        }
        return right
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    乍一看,我们好像无从下手,那就慢慢分析,看看该怎么解决。

    题目要求我们要查找数组中任意一个峰值,那我们可以怎么找到峰值呢?我们可以分析到,其实这个数组无非就分为连个区间,一个是递增区间,一个是递减区间,而峰值就是两个区间交替的标志

    假设我们随机找一个点,如果他比右边位置的值小,那就证明他在一个递增的区间;如果他比右边的位置的值大,那就证明他在一个递减的区间

    通过这个性质,我们使用二分算法,当 nums[mid] < nums[mid+1] 时,在一个递增的区间,山顶必定在 mid+1 以及之后的位置;当 nums[mid] > nums[mid+1] 时,在一个递减的区间,山顶则可能在 mid 或者是之前的位置(这里要注意了,山顶有可能就是 mid 位置)

    所以 right = mid,而不需要 -1 的操作。也因为这里不需要 -1,当 left == right 的时候,如果我们已经找到山顶了,那该怎么跳出循环?

    这里我们可以选择特判一下,也可以把循环条件改成 left < right。有些时候,模板也不是一定要定死的,我们是可以根据实际情况进行调整的。这也我们做完这道题目学到的东西,又或者说积累到的经验。

    5. 寻找旋转排序数组中的最小值

    咱们继续来做一些不同场景下的二分的应用~

    题目链接:153. 寻找旋转排序数组中的最小值

    题目描述


    这道题目的解题思路其实和上一道题类似,我们要根据题目给的条件,找到一个能够比较的参照物,或者说一个参照系。来看代码

    代码

    func findMin(nums []int) int {
        n := len(nums)-1
        left, right := 0, n
        for left < right {
            mid := left+(right-left)/2
            if nums[mid] > nums[n] {
                left = mid+1
            } else if nums[mid] < nums[n] {
                right = mid
            }
        }
        return nums[left]
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

    我们还是从题目条件出发,题目给出的是一个旋转过的升序数组,你发现,这又不是一个有序的数组,这该咋用二分来解呢?

    分析一下,有序数组旋转之后,他会分成两个递增的区间,一个区间大,一个区间小,我们可以拿区间的最大值作为参照物

    举个例子,这个数组最右边的值,他只有两种可能,1. 是小区间的最大值,2. 大区间的最大值(这种情况下,这个数组就是没有旋转过的情况,那我们可以先忽略特殊情况(这算是我的一点做题的技巧,因为特殊情况可以到时候想着怎么特判))

    题目要求我们找到最小元素,也就是我们只需要找到小区间,再在小区间找到区间内最小元素即可,那就:

    当 nums[mid] > nums[n] 时,mid 位置大于小区间的最大值,证明 mid 位置在大区间,所以让 left = mid+1;
    当 nums[mid] < nums[n] 时,mid 位置小于小区间的最大值,证明 mid 位置在小区间,所以让 right = mid;因为我们要找的值就在小区间,所以不能 -1

    然后我们在模拟一下特殊情况,当 nums[mid] < nums[n] 时,n 位置是大区间的最大值,让 right = mid 会导致最后的出现 left = right 的情况,那我们就把循环的条件调整为:left < right

    又是一道变式的二分题目,其实我们只要分析出,以什么作为参照系来排除区间的位置,那之后的工作就很容易了~

    6. 点名

    那咱们就趁热打铁,再来最后一道,增强一下信心,以后二分题目就不用愁啦~

    题目链接:LCR 173. 点名

    题目描述


    这道题我其实一开始也想不出来,选这道题的目的其实也是想说明,算法积累的重要性,见多识广,才能思路开阔,临危不乱。来看代码:

    代码

    func takeAttendance(records []int) int {
        left, right := 0, len(records)-1
        for left < right {
            mid := left+(right-left)/2
            if records[mid] == mid {
                left = mid+1
            } else {
                right = mid
            }
        }
        if records[right] == right {
            return right+1
        }
        return right
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15

    分析题目我们看到,数组是从 0 开始一个升序数组,所以我们可以发现,数组中的数字是和下标是一一对应的,但是缺失了数字之后,数组的数就会比下标大,这样,我们又能将这个数组分为两个区间

    我们画个图来看,会更加的清晰:

    正常的区间是一一对应的,而失去数字的区间都是对不上的,如果对的上,也就是 records[mid] == mid,就让 left = mid+1;如果对不上,就让 right = mid,因为答案可能就在这个区间

    现在就只剩下一个特殊情况了,如果消失的是最后一个数呢?那我们就最后特判一下就行

    总结

    相信你刷完这六道题目之后,二分已经不在话下了~

    再接再厉,去迎接新的挑战吧~

    如果哪天对二分又不熟悉了,也可以回来再刷一刷,练练手感~

    最后,祝你今后刷题愉快~

  • 相关阅读:
    基于Hadoop协同过滤的电子商务商品推荐(购买组合)系统
    〖Python 数据库开发实战 - MySQL篇㉖〗- 数据删除操作 - DELETE语句
    每日OJ题_多源BFS①_力扣542. 01 矩阵(多源BFS解决最短路原理)
    【ABAP】SAP发送消息至RabbitMQ
    本地部署 ChatTTS
    【web渗透思路】任意账号的注册、登录、重置、查看
    第一次微生物学实验
    GPT-4.5 Turbo:意外曝光且可能在六月份推出
    SAP NetWeaver ABAP 服务器的数据库表database table暴露为CDS View
    基于LEX的词法分析实验
  • 原文地址:https://blog.csdn.net/Locky136/article/details/133382439