查找算法/搜索 | 二分法(python)

Hi,大家好,我是半亩花海。近期在学习算法与数据结构相关知识,纯纯小白,欢迎一起交流呀。打算从算法学起,首先学习查找算法(搜索)中的二分法,我使用的是 python 语言进行学习。本算法学习参考了很多博主的文章,比如:算法第三期——二分法(Python)_python二分法-CSDN博客、玩转二分法(python版)——leetcode二分法题总结【简单易懂】_len(nums) - 1-CSDN博客、Python实现二分法搜索_二分法python-CSDN博客 以及 LeetCode 等等。


目录

一、二分法概述

1.1 理论背景:非线性方程的求根问题

1.2 使用条件

1.3 复杂度分析

二、二分法求解方法

2.1 基本思想

2.2 代码模板

2.2.1 闭区间:[left, right]

2.2.2 左闭右开区间:[left, right)

2.2.3 开区间:(left, right)

三、二分法例题

1. LeetCode[704.二分查找]【简单】

方法一:暴力法

方法二:二分查找法

2. LeetCode[69.x的平方根]【简单】

方法:二分查找法

3. LeetCode[35.搜索插入位置]【简单】

方法:二分查找法

4. LeetCode[34.在排序数组中查找元素的第一个和最后一个位置]【中等】

方法一:暴力法

方法二:二分查找法 


一、二分法概述

在一个有序序列中查找某个元素,在之前我们可以使用暴力法来遍历序列,直至找到该元素,复杂度是 O(n),但其实可以利用序列有序的特征进行折半查找。

  • 二分法定义:把一个长度为 n有序序列O(n) 的查找时间,优化到了 O(logn)
  • 二分法本质:折半搜索
  • 二分法效率:很高,时间复杂度为 O(logn)(其实不太严谨,因为需要考虑底数,那就要看分治的复杂度:二分法底数为 2,则为复杂度为  O(log_{2}n);三分法底数为 3,则为 O(log_{3}n) ... 以此类推。当然不写底数也行,但是得知道它有底数)。
  • 二分法实例——猜数游戏:若 n=1000 万,只需要猜 \log_{2}10^{7}\approx 24 次。

1.1 理论背景:非线性方程的求根问题

  • 满足方程:f(x)=0 的数 x 称为方程的根。
  • 非线性方程:指 f(x) 中含有三角函数、指数函数或其他超越函数。很难或者无法求得精确解。二分法是一种求解的方法。

1.2 使用条件

  • 上下界 [a, b] 确定
  • 函数在 [a, b] 内单调

1.3 复杂度分析

  • n 次二分后,区间缩小到 \frac{b-a}{2^{n}}
  • 给定 a, b 和精度要求 \xi,可以算出二分次数 n,即满足: \frac{b-a}{2^{n}}< \xi
  • 二分法的复杂度:
    • 时间复杂度: O(logn),其中 n 是数组的长度。
    • 空间复杂度: O(1)
  • 示例:如果函数在区间 [0,10^{5}] 内单调变化,要求根的精度是 10^{-8},那么二分次数是 44 次。因为:\frac{10^{5}-0}{2^{n}}< 10^{-8} = > 2^{n}> 10^{13} = > n> log_{2}10^{13} = > n\approx 44

二、二分法求解方法

2.1 基本思想

首先把循环可以进行的条件写成 while left <= right,在退出循环的时候,一定有 left == right 成立,此时返回 nums[mid] 中对应的 mid 即可。

  • 深层次的思想是“夹逼法”或者称为“排除法”——二分查找算法的基本思想
  • “排除法”:在每一轮循环中排除一半以上的元素,于是在对数级别的时间复杂度内,就可以把区间“夹逼”只剩下 1 个数,而这个数是不是我们要找的数,单独做一次判断就可以了。

2.2 代码模板

下面的代码包含闭区间左闭右开区间开区间三种写法。选择自己喜欢的一种写法即可。

  • binary_search 返回最小的满足 nums[i] >= target 的 i
  • 如果数组为空,或者所有数都小于 target,则返回 len(nums)
  • 要求 nums 是非递减的,即 nums[i] <= nums[i + 1]

2.2.1 闭区间:[left, right]

# 闭区间写法
def binary_search1(nums, target):
    left, right = 0, len(nums) - 1  # 闭区间 [left, right]
    while left <= right:  # 区间不为空
        # 循环不变量:
        # nums[left-1] < target
        # nums[right+1] >= target
        mid = (left + right) // 2
        if nums[mid] < target:
            left = mid + 1  # 范围缩小到 [mid+1, right]
        else:
            right = mid - 1  # 范围缩小到 [left, mid-1]
    return left

2.2.2 左闭右开区间:[left, right)

# 左闭右开区间写法
def binary_search2(nums, target):
    left = 0
    right = len(nums)  # 左闭右开区间 [left, right)
    while left < right:  # 区间不为空
        # 循环不变量:
        # nums[left-1] < target
        # nums[right] >= target
        mid = (left + right) // 2
        if nums[mid] < target:
            left = mid + 1  # 范围缩小到 [mid+1, right)
        else:
            right = mid  # 范围缩小到 [left, mid)
    return left  # 返回 left 还是 right 都行,因为循环结束后 left == right

2.2.3 开区间:(left, right)

# 开区间写法
def binary_search3(nums: List[int], target: int) -> int:
    left, right = -1, len(nums)  # 开区间 (left, right)
    while left + 1 < right:  # 区间不为空
        mid = (left + right) // 2
        # 循环不变量:
        # nums[left] < target
        # nums[right] >= target
        if nums[mid] < target:
            left = mid  # 范围缩小到 (mid, right)
        else:
            right = mid  # 范围缩小到 (left, mid)
    return right

三、二分法例题

基本模板就是这样,接下来就是实践,积累经验。目前做过的 LeetCode 题有:704、69、35、34(后面会继续写153、33、81、154、4)。推荐按照这个顺序做题(难度从易到难)。

1. LeetCode[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

思路及题解:

方法一:暴力法

def binary_search(nums, target):
    for i in range(len(nums)):
        if nums[i] == target:
            return i
    return -1

# nums = [-1, 0, 3, 5, 9, 12]
# target1 = 9
# target2 = 2
'''使用input()函数输入:
nums = eval(input("nums = "))
target1 = int(input("target1 = "))
target2 = int(input("target2 = "))
'''
# print(binary_search(nums, target1))  # 输出:4
# print(binary_search(nums, target2))  # 输出:-1

方法二:二分查找法

(1)思路

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

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

基于上述事实,可以在有序数组中使用二分查找寻找目标值,分为以下三步:

定义查找的范围 [left,right] (这里的 left 和 right 是索引),初始查找范围是整个数组。

二分查找的条件查找范围不为空,即 left\leq right。如果 target 在数组中,二分查找可以保证找到 target,返回 target 在数组中的下标。如果 target 不在数组中,则当 left>right 时结束查找,返回 -1。由于每次查找都会将查找范围缩小一半,因此二分查找的时间复杂度是 O(logn),其中 n 是数组的长度。

每次取查找范围的中点 mid,比较 nums[mid] 和 target 的大小。

中间索引 mid 有两种写法:

  • mid = (left+right)//2
  • mid = left+(right-left)//2

由于整数运算没有溢出问题,因此通常两种写法都可以使用。第二种写法更为安全,以确保在边界条件下也能正常工作,特别是当 left 和 right 都很大的时候。

如果 nums[mid] 和 target 的大小相等,则 mid 即为要寻找的下标;如果两者不相等,则根据 nums[mid] 和 target 的大小关系将查找范围缩小一半:

  • nums[mid]\geq target 时:target 在 mid 的左边,新的搜索区间是左半部分,left 不变,更新 right = mid
  • nums[mid]< target 时:target 在 mid 的右边,新的搜索区间是右半部分,right 不变,更新 left = mid +1

代码执行完毕直至 left= right,则此时两者相等,即此时为 target 所处的位置。

(2)题解

class BinarySearch(object):
    def binary_search(self, nums, target):
        left, right = 0, len(nums) - 1
        while left <= right:
            mid = (right - left) // 2 + left
            # mid = (left + right) // 2
            if nums[mid] == target:
                return mid
            elif nums[mid] > target:
                right = mid - 1
            else:
                left = mid + 1
        return -1

# search = BinarySearch()
# nums = [-1, 0, 3, 5, 9, 12]
# target = 9
# print(search.binary_search(nums, target))  # 输出:4

2. LeetCode[69.x的平方根]【简单】

题目:

给你一个非负整数 x计算并返回 x 的 算术平方根 

由于返回类型是整数,结果只保留整数部分,小数部分将被舍去 。

注意:不允许使用任何内置指数函数和算符,例如 pow(x, 0.5) 或者 x ** 0.5 。

示例 1:

  • 输入:x = 4
  • 输出:2

示例 2:

  • 输入:x = 8
  • 输出:2
  • 解释:8 的算术平方根是 2.82842..., 由于返回类型是整数,小数部分将被舍去。

思路及题解:

方法:二分查找法

(1)思路

由于 x 平方根的整数部分 ans 是满足 k^2 \leq x 的最大 k 值,因此我们可以对 k 进行二分查找,从而得到答案。

二分查找的下界为 0,上界可以粗略地设定为 x。在二分查找的每一步中,我们只需要比较中间元素 mid 的平方与 x 的大小关系,并通过比较的结果调整上下界的范围。由于我们所有的运算都是整数运算,不会存在误差,因此在得到最终的答案 ans 后,也就不需要再去尝试 ans+1 了。

(2)题解

class Solution(object):
    def mySqrt(self, x):
        left, right, ans = 0, x, -1
        while left <= right:
            mid = (left + right) // 2
            if mid * mid <= x:
                ans = mid
                left = mid + 1
            else:
                right = mid - 1
        return ans

# mysqrt = Solution()
# x = 8
# print(mysqrt.mySqrt(x))  # 输出:2

3. LeetCode[35.搜索插入位置]【简单】

题目:

给定一个排序数组和一个目标值,在数组中找到目标值,并返回其索引。如果目标值不存在于数组中,返回它将会被按顺序插入的位置

请必须使用时间复杂度为 O(logn) 的算法。

示例 1:

  • 输入:nums = [1,3,5,6], target = 5
  • 输出:2

示例 2:

  • 输入:nums = [1,3,5,6], target = 2
  • 输出:1

示例 3:

  • 输入:nums = [1,3,5,6], target = 7
  • 输出:4

思路及题解:

方法:二分查找法

(1)思路

考虑这个插入的位置 pos,它成立的条件为:

nums[pos-1]< target\leq nums[pos]

其中 nums 代表排序数组。由于如果存在这个目标值,我们返回的索引也是 pos,因此我们可以将两个条件合并得出最后的目标:「在一个有序数组中找第一个大于等于 pos 的下标」。

问题转化到这里,直接套用二分法即可,即不断用二分法逼近查找第一个大于等于 target 的下标 。下文给出的代码是笔者习惯的二分写法,ans 初值设置为数组长度可以省略边界条件的判断,因为存在一种情况是 target 大于数组中的所有数,此时需要插入到数组长度的位置。

(2)题解

class Solution(object):
    def searchInsert(self, nums, target):
        left, right = 0, len(nums) - 1
        while left <= right:
            mid = left + (right - left) // 2
            if nums[mid] == target:
                return mid
            elif nums[mid] < target:
                left = mid + 1
            else:
                right = mid - 1
        return left

# solution = Solution()
# nums = [1, 3, 5, 6]
# target = 2
# print(solution.searchInsert(nums, target))  # 输出:1

4. LeetCode[34.在排序数组中查找元素的第一个和最后一个位置]【中等】

题目:

给你一个按照非递减顺序排列的整数数组 nums,和一个目标值 target。请你找出给定目标值在数组中的开始位置和结束位置

如果数组中不存在目标值 target,返回 [-1, -1]。你必须设计并实现时间复杂度为 O(logn) 的算法解决此问题。

示例 1:

  • 输入:nums = [5,7,7,8,8,10], target = 8
  • 输出:[3,4]

示例 2:

  • 输入:nums = [5,7,7,8,8,10], target = 6
  • 输出:[-1,-1]

示例 3:

  • 输入:nums = [], target = 0
  • 输出:[-1,-1]

思路及题解:

方法一:暴力法

(1)思路

定义 leftright 初始值为 -1,利用数组有序的特点从头到尾遍历列表,先寻找 left 再寻找 right,最后return [left, right]

② 在遍历的开始,检查遍历到的元素是否等于 target,遇到刚好等于 target 的时候,记录当前的位置;

③ 接着遍历,检查遍历到的元素是否不等于 target,遇到刚好不等于 target 的时候,记录当前位置的前一个位置即可。

(2)复杂度分析

  • 时间复杂度:O(n),其中 n 为数组的长度。
  • 空间复杂度:O(1),只用到常数个临时变量。

(3)题解

def searchRange(nums, target):
    if not nums:  # 数组为空
        return [-1, -1]
    left, right = -1, -1
    for i in range(len(nums)):
        if nums[i] == target:
            if left == -1:
                left = i
            right = i
    return [left, right]

# nums = [5, 7, 7, 8, 8, 10]
# target = 8
# print(searchRange(nums, target))  # 输出:[3, 4]

方法二:二分查找法 

(1)思路

  • 目标元素 target 在有序数组中很可能存在多个;
  • 使用二分查找方法看到的处在中间位置的元素的值 nums[mid] 恰好等于目标元素 target 的时候,还需要继续查找下去;
  • 此时比较容易陷入的误区是线性查找,正确的做法是继续二分查找。

初始化 l, r 左右指针,循环条件设为“当 l < r ”。这样设立是因为跳出循环的时候总是有 l=r不用思考是 l 还是 r

取中值。取中左还是中右,这就需要看情况了。 如果我们想要找目标值的起始下标,那么当 nums[mid] 大于目标值 target(或者找到这个目标值的时候),右边界都要缩小。即当 nums[mid] \geq target 的时候,r = mid

注意:不能以 mid-1 这样的形式缩小,否则当找到起始下标的时候还得再减 1,就会出现问题。因为不可以左右两个都 =mid,必须有一个 =mid+1,否则会陷入死循环。所以在其他情况下,左边界 =mid+1

mid 取左中还是右中,要以你选择哪个是 mid+1 来定。对于第一个循环,这里我们选了 mid+1 的是左边 l,所以我们要取左中值。否则会陷入死循环。

(2)题解

class Solution(object):
    def searchRange(self, nums, target):
        l, r = 0, len(nums) - 1  # 取起始下标
        while l < r:
            mid = (l + r) // 2
            if nums[mid] >= target:
                r = mid
            else:
                l = mid + 1

        if not nums or nums[l] != target:  # 数组为空或没找到
            return [-1, -1]

        a, b = l, len(nums) - 1  # 取结束下标
        while a < b:
            mid = (a + b + 1) // 2
            if nums[mid] <= target:
                a = mid
            else:
                b = mid - 1

        return [l, a]

# solution = Solution()
# nums = eval(input('nums = '))
# target = int(input('target = '))
# print(solution.search_Range(nums, target))  # 输出:[3, 4]

34小结:个人感觉这个题对于初学者的我来说还是有点难理解的,需要反复学习。 

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/403950.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

【QT-lineEidte动画效果

QT-lineEidte动画效果 一、演示效果二、核心代码三、下载链接 一、演示效果 二、核心代码 #ifndef DynamicUnderlineLineEdit_H #define DynamicUnderlineLineEdit_H#include <QWidget> #include <QLineEdit> #include <QPainter> #include <QPaintEvent…

【学习iOS高质量开发】——协议与分类

文章目录 一、通过委托与数据源协议进行对象间通信1.委托模式2.要点 二、将类的实现代码分散到便于管理的数个分类之中1.如何实现2.要点 三、总是为第三方类的分类名称加前缀1.为什么总是为第三方类的分类名称加前缀2.要点 三、勿在分类中声明属性1.勿在分类中声明属性的原因2.…

【最新Dubbo3深入理解】Dubbo3中的SPI机制以及IOC、AOP

欢迎关注公众号&#xff08;通过文章导读关注&#xff1a;【11来了】&#xff09;&#xff0c;及时收到 AI 前沿项目工具及新技术的推送&#xff01; 在我后台回复 「资料」 可领取编程高频电子书&#xff01; 在我后台回复「面试」可领取硬核面试笔记&#xff01; 文章导读地址…

MFC 配置Halcon

1.新建一个MFC 工程&#xff0c;Halcon 为64位&#xff0c;所以先将工程改为x64 > VC 目录设置包含目录和库目录 包含目录 库目录 c/c ->常规 链接器 ->常规 > 链接器输入 在窗口中添加头文件 #include "HalconCpp.h" #include "Halcon.h"…

canvas水波纹效果,jquery鼠标水波纹插件

canvas水波纹效果&#xff0c;jquery鼠标水波纹插件 效果展示 jQuery水波纹效果&#xff0c;canvas水波纹插件 HTML代码片段 <div class"scroll04wrap"><h3>发展历程</h3><div class"scroll04"><p>不要回头&#xff0c;一…

移动硬盘误删的文件还能找回来吗?1分钟弄清答案!

“想问问大家如果移动硬盘里保存了很多文件&#xff0c;但上次使用时不小心将部分文件删除了&#xff0c;还有机会找回误删的文件吗&#xff1f;应该怎么进行误删文件的恢复呢&#xff1f;” 在数字化时代&#xff0c;移动硬盘成为了我们存储和传输数据的重要工具。然而&#x…

外泌体相关基因肝癌临床模型预测——2-3分纯生信文章复现——02.数据格式整理(1)

内容如下&#xff1a; 1.外泌体和肝癌TCGA数据下载 2.数据格式整理 3.差异表达基因筛选 4.预后相关外泌体基因确定 5.拷贝数变异及突变图谱 6.外泌体基因功能注释 7.LASSO回归筛选外泌体预后模型 8.预后模型验证 9.预后模型鲁棒性分析 10.独立预后因素分析及与临床的…

【Appium UI自动化】pytest运行常见错误解决办法

通过Appium工具录制代码在pycharm上运行报错&#xff1a; 错误一&#xff1a; 1.提示 setup() 方法运行 error failed 解决办法&#xff1a;未创建 init __ 方法&#xff0c;创建一个空的__init.py文件就解决了。 原因&#xff1a; 错误二&#xff1a; 2.运行代码&#xff…

Rabbitmq入门与应用(四)-RabbitMQ常见模式

RabbitMQ常见Queue模式 简单模式 点对点模式&#xff0c;一个生产者一个消费者 生产者将消息发送到队列&#xff0c;消费者从队列中获取消息&#xff0c;队列是存储消息的缓冲区。 查看管理端效果 序列化解决方案 基于java序列化基于Json Bean public MessageConverter mess…

【Java从入门到精通】Java Character 类

Java Character 类 Character 类用于对单个字符进行操作。 Character 类在对象中包装一个基本类型 char 的值 实例 char ch a;// Unicode 字符表示形式 char uniChar \u039A; // 字符数组 char[] charArray { a, b, c, d, e }; 然而&#xff0c;在实际开发过程中&#xf…

利用Python email的MIMEBase及MIMEApplication发送邮件附件

**MIMEBase、 MIMEApplication背景知识点总结** MIMEBase 和 MIMEApplication 都是用于创建 MIME 消息的类&#xff0c;但它们之间有一些异同点。 异同点如下&#xff1a; 用途&#xff1a; MIMEBase 是一个基类&#xff0c;用于表示任何类型的 MIME 消息部分。它提供了一种通…

Video generation models as world simulators-视频生成模型作为世界模拟器

原文地址&#xff1a;Video generation models as world simulators 我们探索在视频数据上进行大规模生成模型的训练。具体来说&#xff0c;我们联合训练文本条件扩散模型&#xff0c;同时处理不同持续时间、分辨率和长宽比的视频和图像。我们利用一个在视频和图像潜在编码的时…

Redis信创平替之TongRDS(东方通),麒麟系统安装步骤

我的系统: 银河麒麟桌面系统V10(SP1)兆芯版 1.先进入东方通申请使用 2.客服会发送一个TongRDS包与center.lic给你(我这里只拿到.tar.gz文件,没有网上的什么安装版) 3.上传全部文件到目录中 4.服务节点安装,并启动 tar -zxvf TongRDS-2.2.1.2_P3.Node.tar.gz cd pmemdb/bin/…

c++实现栈和队列类

c实现栈和队列类 栈(Stack)Stack示意图Stack.cpp 队列(queue)queue 示意图queue.cpp 栈(Stack) Stack示意图 Stack.cpp #pragma once #include "ListStu.cpp"template<typename T> class Stack { public: /* * void push(T& tDate)* 参数一 &#xff1a;…

【Linux从青铜到王者】 基础IO

本篇重点&#xff1a;文件描述符&#xff0c;重定向&#xff0c;缓冲区&#xff0c;磁盘结构&#xff0c;文件系统&#xff0c;inode理解文件的增删查改&#xff0c;查找一个文件为什么一定要有路径&#xff0c;动静态库&#xff0c;有的时候为什么找不到库&#xff0c;动态库的…

SQL面试题及答案

介绍 在快节奏的数据管理和信息技术世界中,导航和操作结构化数据的能力是一项非常重要的技能。SQL,即结构化查询语言,是关系数据库的基石,掌握这种语言的专业人员的需求量很大。SQL 面试在科技行业很常见,潜在的候选人会接受测试以展示他们的知识和解决问题的能力。为了帮…

酷开科技丨新年新玩法!酷开系统壁纸模式给客厅“换”新

甲辰龙年即将到来&#xff0c;新年新家新气象&#xff0c;快到酷开系统壁纸模式中挑选一款喜欢的壁纸&#xff0c;为新的一年增添一份美好和喜悦吧&#xff01; 酷开科技将更多的电视新玩法带给你&#xff0c;让你的电视成为家庭中的焦点&#xff01;酷开系统壁纸模式&#xf…

【前端素材】推荐优质后台管理系统APP Zina平台模板(附源码)

一、需求分析 当我们从多个层次来详细分析后台管理系统时&#xff0c;可以将其功能和定义进一步细分&#xff0c;以便更好地理解其在不同方面的作用和实际运作。 1. 功能层次 a. 用户管理功能&#xff1a; 用户注册和登录&#xff1a;管理用户账户的注册和登录过程。权限管…

easyui 手风琴Accordion 面板的高度设置

今天接到一个新的小需求&#xff0c;如下图&#xff0c;当预算表单只有一个时&#xff0c;要求不显示预算表单这块的内容。 考虑到页面创建时用到了表单的回调和点击方法&#xff0c;所以不能单纯的移除&#xff0c;移除右侧表格的创建会报错&#xff0c;所以只能隐藏。 隐藏…

突破挑战:利用沃尔玛跨境智星实现批量注册与下单

在进行沃尔玛测评时&#xff0c;确保环境安全至关重要。每个账号都应使用独立的运行环境&#xff0c;这样可以避免浏览器指纹、字体以及浏览器数据之间的关联问题。此外&#xff0c;账号的资料也需要特别注意。最好是自己注册自己下单&#xff0c;这样可以确保所有资源都掌握在…