二分查找笔记

news2024/11/23 16:43:52

1.1 什么是算法?

定义

在数学和计算机科学领域,算法是一系列有限的严谨指令,通常用于解决一类特定问题或执行计算

In mathematics and computer science, an algorithm (/ˈælɡərɪðəm/) is a finite sequence of rigorous instructions, typically used to solve a class of specific problems or to perform a computation.[^1]

Introduction to Algorithm[^2]

不正式的说,算法就是任何定义优良的计算过程:接收一些值作为输入,在有限的时间内,产生一些值作为输出。

Informally, an algorithm is any well-defined computational procedure that takes some value, or set of values, as input and produces some value, or set of values, as output in a finite amount of time.

1.2 什么是数据结构?

定义

在计算机科学领域,数据结构是一种数据组织、管理和存储格式,通常被选择用来高效访问数据

In computer science, a data structure is a data organization, management, and storage format that is usually chosen for efficient access to data

Introduction to Algorithm[^2]

数据结构是一种存储和组织数据的方式,旨在便于访问和修改

A data structure is a way to store and organize data in order to facilitate access and modifications

接下来我们通过对一个非常著名的二分查找算法的讲解来认识一下算法

1.3 二分查找 [^3]

二分查找算法也称折半查找,是一种非常高效的工作于有序数组的查找算法。后续的课程中还会学习更多的查找算法,但在此之前,不妨用它作为入门。

二分查找基础版

需求:在有序数组 A 内,查找值 target

  • 如果找到返回索引
  • 如果找不到返回 -1
算法描述
前提给定一个内含 n 个元素的有序数组 A,满足 A_{0}\leq A_{1}\leq A_{2}\leq \cdots \leq A_{n-1},一个待查值 target
1设置 i=0,j=n-1
2如果 i \gt j,结束查找,没找到
3设置 m = floor(\frac {i+j}{2}) ,m 为中间索引,floor 是向下取整(\leq \frac {i+j}{2} 的最小整数)
4如果 target < A_{m} 设置 j = m - 1,跳到第2步
5如果 A_{m} < target 设置 i = m + 1,跳到第2步
6如果 A_{m} = target,结束查找,找到了

java 实现

public static int binarySearch(int[] a, int target) {
    int i = 0, j = a.length - 1;
    while (i <= j) {
        int m = (i + j) >>> 1;
        if (target < a[m]) {			// 在左边
            j = m - 1;
        } else if (a[m] < target) {		// 在右边
            i = m + 1;
        } else {
            return m;
        }
    }
    return -1;
}
  • i,j 对应着搜索区间 [0,a.length-1](注意是闭合的区间),i<=j 意味着搜索区间内还有未比较的元素,i,j 指向的元素也可能是比较的目标
    • 思考:如果不加 i==j 行不行?
    • 回答:不行,因为这意味着 i,j 指向的元素会漏过比较
  • m 对应着中间位置,中间位置左边和右边的元素可能不相等(差一个),不会影响结果
  • 如果某次未找到,那么缩小后的区间内不包含 m

二分查找改变版

另一种写法

public static int binarySearch(int[] a, int target) {
    int i = 0, j = a.length;
    while (i < j) {
        int m = (i + j) >>> 1;
        if (target < a[m]) {			// 在左边
            j = m;
        } else if (a[m] < target) {		// 在右边
            i = m + 1;
        } else {
            return m;
        }
    }
    return -1;
}
  • i,j 对应着搜索区间 [0,a.length)(注意是左闭右开的区间),i<j 意味着搜索区间内还有未比较的元素,j 指向的一定不是查找目标
    • 思考:为啥这次不加 i==j 的条件了?
    • 回答:这回 j 指向的不是查找目标,如果还加 i==j 条件,就意味着 j 指向的还会再次比较,找不到时,会死循环
  • 如果某次要缩小右边界,那么 j=m,因为此时的 m 已经不是查找目标了

衡量算法好坏

时间复杂度

下面的查找算法也能得出与之前二分查找一样的结果,那你能说出它差在哪里吗?

public static int search(int[] a, int k) {
    for (
        int i = 0;
        i < a.length;
        i++
    ) {
        if (a[i] == k) {
            return i;
        }
    }
    return -1;
}

考虑最坏情况下(没找到)例如 [1,2,3,4] 查找 5

  • int i = 0 只执行一次
  • i < a.length 受数组元素个数 n 的影响,比较 n+1 次
  • i++ 受数组元素个数 n 的影响,自增 n 次
  • a[i] == k 受元素个数 n 的影响,比较 n 次
  • return -1,执行一次

粗略认为每行代码执行时间是 t,假设 n=4 那么

  • 总执行时间是 (1+4+1+4+4+1)*t = 15t
  • 可以推导出更一般地公式为,T = (3*n+3)t

如果套用二分查找算法,还是 [1,2,3,4] 查找 5

public static int binarySearch(int[] a, int target) {
    int i = 0, j = a.length - 1;
    while (i <= j) {
        int m = (i + j) >>> 1;
        if (target < a[m]) {			// 在左边
            j = m - 1;
        } else if (a[m] < target) {		// 在右边
            i = m + 1;
        } else {
            return m;
        }
    }
    return -1;
}
  • int i = 0, j = a.length - 1 各执行 1 次

  • i <= j 比较 floor(\log_{2}(n)+1) 再加 1 次

  • (i + j) >>> 1 计算 floor(\log_{2}(n)+1) 次

  • 接下来 if() else if() else 会执行 3* floor(\log_{2}(n)+1) 次,分别为

    • if 比较
    • else if 比较
    • else if 比较成立后的赋值语句
  • return -1,执行一次

结果:

  • 总执行时间为 (2 + (1+3) + 3 + 3 * 3 +1)*t = 19t
  • 更一般地公式为 (4 + 5 * floor(\log_{2}(n)+1))*t

注意:

左侧未找到和右侧未找到结果不一样,这里不做分析

两个算法比较,可以看到 n 在较小的时候,二者花费的次数差不多

image-20221108095747933

但随着 n 越来越大,比如说 n=1000 时,用二分查找算法(红色)也就是 54t,而蓝色算法则需要 3003t

image-20221108100014451

画图采用的是 Desmos | 图形计算器

计算机科学中,时间复杂度是用来衡量:一个算法的执行,随数据规模增大,而增长的时间成本

  • 不依赖于环境因素

如何表示时间复杂度呢?

  • 假设算法要处理的数据规模是 n,代码总的执行行数用函数 f(n) 来表示,例如:

    • 线性查找算法的函数 f(n) = 3*n + 3
    • 二分查找算法的函数 f(n) = (floor(log_2(n)) + 1) * 5 + 4
  • 为了对 f(n) 进行化简,应当抓住主要矛盾,找到一个变化趋势与之相近的表示法

大 O 表示法[^4]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-CY1uDvnO-1685434527696)(.\imgs\image-20221108103846566.png)]

其中

  • c, c_1, c_2 都为一个常数
  • f(n) 是实际执行代码行数与 n 的函数
  • g(n) 是经过化简,变化趋势与 f(n) 一致的 n 的函数

渐进上界

渐进上界(asymptotic upper bound):从某个常数 n_0开始,c*g(n) 总是位于 f(n) 上方,那么记作 O(g(n))

  • 代表算法执行的最差情况

例1

  • f(n) = 3*n+3
  • g(n) = n
  • 取 c=4,在n_0=3 之后,g(n) 可以作为 f(n) 的渐进上界,因此表示法写作 O(n)

例2

  • f(n) = 5*floor(log_2(n)) + 9
  • g(n) = log_2(n)
  • O(log_2(n))

已知 f(n) 来说,求 g(n)

  • 表达式中相乘的常量,可以省略,如
    • f(n) = 100*n^2 中的 100
  • 多项式中数量规模更小(低次项)的表达式,如
    • f(n)=n^2+n 中的 n
    • f(n) = n^3 + n^2 中的 n^2
  • 不同底数的对数,渐进上界可以用一个对数函数 \log n 表示
    • 例如:log_2(n) 可以替换为 log_{10}(n),因为 log_2(n) = \frac{log_{10}(n)}{log_{10}(2)},相乘的常量 \frac{1}{log_{10}(2)} 可以省略
  • 类似的,对数的常数次幂可省略
    • 如:log(n^c) = c * log(n)

常见大 O 表示法

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-3nv3XtAI-1685434527702)(.\imgs\image-20221108114915524.png)]

按时间复杂度从低到高

  • 黑色横线 O(1),常量时间,意味着算法时间并不随数据规模而变化
  • 绿色 O(log(n)),对数时间
  • 蓝色 O(n),线性时间,算法时间与数据规模成正比
  • 橙色 O(n*log(n)),拟线性时间
  • 红色 O(n^2) 平方时间
  • 黑色朝上 O(2^n) 指数时间
  • 没画出来的 O(n!)

渐进下界

渐进下界(asymptotic lower bound):从某个常数 n_0开始,c*g(n) 总是位于 f(n) 下方,那么记作 \Omega(g(n))

渐进紧界

渐进紧界(asymptotic tight bounds):从某个常数 n_0开始,f(n) 总是在 c_1g(n) 和 c_2g(n) 之间,那么记作 \Theta(g(n))

空间复杂度

与时间复杂度类似,一般也使用大 O 表示法来衡量:一个算法执行随数据规模增大,而增长的额外空间成本

public static int binarySearchBasic(int[] a, int target) {
    int i = 0, j = a.length - 1;    // 设置指针和初值
    while (i <= j) {                // i~j 范围内有东西
        int m = (i + j) >>> 1;
        if(target < a[m]) {         // 目标在左边
            j = m - 1;
        } else if (a[m] < target) { // 目标在右边
            i = m + 1;
        } else {                    // 找到了
            return m;
        }
    }
    return -1;
}

二分查找性能

下面分析二分查找算法的性能

时间复杂度

  • 最坏情况:O(\log n)
  • 最好情况:如果待查找元素恰好在数组中央,只需要循环一次 O(1)

空间复杂度

  • 需要常数个指针 i,j,m,因此额外占用的空间是 O(1)

二分查找平衡版

public static int binarySearchBalance(int[] a, int target) {
    int i = 0, j = a.length;
    while (1 < j - i) {
        int m = (i + j) >>> 1;
        if (target < a[m]) {
            j = m;
        } else {
            i = m;
        }
    }
    return (a[i] == target) ? i : -1;
}

思想:

  1. 左闭右开的区间,i 指向的可能是目标,而 j 指向的不是目标
  2. 不奢望循环内通过 m 找出目标, 缩小区间直至剩 1 个, 剩下的这个可能就是要找的(通过 i)
    • j - i > 1 的含义是,在范围内待比较的元素个数 > 1
  3. 改变 i 边界时,它指向的可能是目标,因此不能 m+1
  4. 循环内的平均比较次数减少了
  5. 时间复杂度 \Theta(log(n))

二分查找 Java 版

private static int binarySearch0(long[] a, int fromIndex, int toIndex,
                                     long key) {
    int low = fromIndex;
    int high = toIndex - 1;

    while (low <= high) {
        int mid = (low + high) >>> 1;
        long midVal = a[mid];

        if (midVal < key)
            low = mid + 1;
        else if (midVal > key)
            high = mid - 1;
        else
            return mid; // key found
    }
    return -(low + 1);  // key not found.
}
  • 例如 [1,3,5,6] 要插入 2 那么就是找到一个位置,这个位置左侧元素都比它小
    • 等循环结束,若没找到,low 左侧元素肯定都比 target 小,因此 low 即插入点
  • 插入点取负是为了与找到情况区分
  • -1 是为了把索引 0 位置的插入点与找到的情况进行区分

Leftmost 与 Rightmost

有时我们希望返回的是最左侧的重复元素,如果用 Basic 二分查找

  • 对于数组 [1, 2, 3, 4, 4, 5, 6, 7],查找元素4,结果是索引3

  • 对于数组 [1, 2, 4, 4, 4, 5, 6, 7],查找元素4,结果也是索引3,并不是最左侧的元素

public static int binarySearchLeftmost1(int[] a, int target) {
    int i = 0, j = a.length - 1;
    int candidate = -1;
    while (i <= j) {
        int m = (i + j) >>> 1;
        if (target < a[m]) {
            j = m - 1;
        } else if (a[m] < target) {
            i = m + 1;
        } else {
            candidate = m; // 记录候选位置
            j = m - 1;     // 继续向左
        }
    }
    return candidate;
}

如果希望返回的是最右侧元素

public static int binarySearchRightmost1(int[] a, int target) {
    int i = 0, j = a.length - 1;
    int candidate = -1;
    while (i <= j) {
        int m = (i + j) >>> 1;
        if (target < a[m]) {
            j = m - 1;
        } else if (a[m] < target) {
            i = m + 1;
        } else {
            candidate = m; // 记录候选位置
            i = m + 1;	   // 继续向右
        }
    }
    return candidate;
}

应用

对于 Leftmost 与 Rightmost,可以返回一个比 -1 更有用的值

Leftmost 改为

public static int binarySearchLeftmost(int[] a, int target) {
    int i = 0, j = a.length - 1;
    while (i <= j) {
        int m = (i + j) >>> 1;
        if (target <= a[m]) {
            j = m - 1;
        } else {
            i = m + 1;
        }
    }
    return i; 
}
  • leftmost 返回值的另一层含义:\lt target 的元素个数
  • 小于等于中间值,都要向左找

Rightmost 改为

public static int binarySearchRightmost(int[] a, int target) {
    int i = 0, j = a.length - 1;
    while (i <= j) {
        int m = (i + j) >>> 1;
        if (target < a[m]) {
            j = m - 1;
        } else {
            i = m + 1;
        }
    }
    return i - 1;
}
  • 大于等于中间值,都要向右找

几个名词

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-4q8jaV38-1685434527704)(./imgs/image-20221125174155058.png)]

范围查询

  • 查询 x \lt 4,0 … leftmost(4) - 1
  • 查询 x \leq 4,0 … rightmost(4)
  • 查询 4 \lt x,rightmost(4) + 1 … \infty
  • 查询 4 \leq x, leftmost(4) … \infty
  • 查询 4 \leq x \leq 7,leftmost(4) … rightmost(7)
  • 查询 4 \lt x \lt 7,rightmost(4)+1 … leftmost(7)-1

求排名:leftmost(target) + 1

  • target 可以不存在,如:leftmost(5)+1 = 6
  • target 也可以存在,如:leftmost(4)+1 = 3

求前任(predecessor):leftmost(target) - 1

  • leftmost(3) - 1 = 1,前任 a_1 = 2
  • leftmost(4) - 1 = 1,前任 a_1 = 2

求后任(successor):rightmost(target)+1

  • rightmost(5) + 1 = 5,后任 a_5 = 7
  • rightmost(4) + 1 = 5,后任 a_5 = 7

求最近邻居

  • 前任和后任距离更近者

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

IO多路转接之select

本文分享的是IO多路转接中的select&#xff0c;其中包括select函数如何去使用&#xff0c;以及使用相关代码实现客户端向服务端发送消息的服务&#xff0c;从而更好地理解多路转接的select。 多路转接 多路转接是IO模型的一种&#xff0c;这种IO模型通过select函数进行IO等待&…

AI浪潮再掀低代码开发热,快来了解最新趋势!

在近些年的发展中&#xff0c;人工智能 (AI) 已融入我们社会和生活的方方面面。从聊天机器人和虚拟助手到自动化工业机械和自动驾驶汽车&#xff0c;我们已经越来越离不开AI技术了&#xff0c;哪怕是我们的日常生活中也充满了它的影子&#xff0c;我们很难忽视它的影响。 AI时代…

小程序、网页跳转App的原理

从不同的渠道&#xff0c;如小程序、二维码、网页等&#xff0c;直接跳转到App内对应的页面&#xff0c;并传递相关的参数信息&#xff0c;已经由拥有深度链接技术的SDK实现了&#xff0c;App只需接入这类SDK即可获得多样化跳转的功能。本文将详细介绍多样化跳转的原理。 多样…

Unity | HDRP高清渲染管线学习笔记:基本操作

目录 一、场景整体环境光强度 1.HDRI Sky 2.Shadows 二、屏幕后处理效果(Post Processing) 1.Exposure 2.Post-processing/Tonemapping 三、抗锯齿 四、添加光源 1.Light Explorer窗口 2.光照探针组 3.反射探针 4.烘焙光照贴图 本文主要是了解HDRP基本操作&#xf…

高完整性系统:Hoare Logic

目录 1. 霍尔逻辑&#xff08;Proving Programs Correct&#xff09; 1.1 警告&#xff08;Caveats&#xff09; 1.2 误解&#xff08;Misconception&#xff09; 1.3 编程语言&#xff08;Programming Language&#xff09; 1.4 程序&#xff08;Programs&#xff09; 1…

Html源代码加密?

什么是Html源代码加密&#xff1f; 使用JavaScript加密转化技术将Html变为密文&#xff0c;以此保护html源代码&#xff0c;这便是Html源码加密。 同时&#xff0c;这种加密技术还可实现网页反调试、防复制、链接加密等功能。 应用场景 什么情况下需要Html源代码加密&#x…

clipboard复制粘题问题

clipboard复制粘贴问题 简单的clipboard用法引入clipboard使用方法 通过监听获取剪切板数据自定义获取clipboard剪切板值 记录下项目中使用clipboard复制粘题问题 简单的clipboard用法 引入clipboard npm install clipboard --save官网地址:传送门 使用方法 通过监听获取剪切…

基于neo4图数据库的简易对话系统

文章目录 一、环境二、思路第一步&#xff1a;输入问句第二步&#xff1a;针对问句进行分析&#xff0c;包括意图识别和实体识别第三步&#xff1a;问句转化第四步&#xff1a;问题回答的模板设计 三、代码解读1. 项目结构2. 数据说明3. 主文件kbqa_test.py解读4. entity_extra…

【第三方库】PHP实现创建PDF文件和编辑PDF文件

目录 引入Setasign/fpdf、Setasign/fpdi 解决写入中文时乱码问题 1.下载并放置中文语言包&#xff08;他人封装&#xff09;&#xff1a;https://github.com/DCgithub21/cd_FPDF 2.编写并运行生成字体文件的程序文件&#xff08;addFont.php&#xff09; 中文字体举例&…

【数据结构】第七周

稀疏矩阵快速转置 【问题描述】 稀疏矩阵的存储不宜用二维数组存储每个元素&#xff0c;那样的话会浪费很多的存储空间。所以可以使用一个一维数组存储其中的非零元素。这个一维数组的元素类型是一个三元组&#xff0c;由非零元素在该稀疏矩阵中的位置&#xff08;行号…

xxl-job的部署及springboot集成使用

介绍 XXL-Job是一个分布式任务调度平台&#xff0c;可进行任务调度、管理和监控&#xff0c;并提供任务分片、失败重试、动态分配等功能。它是一个开源项目&#xff0c;基于Spring Boot和Quartz开发&#xff0c;支持常见的任务调度场景。 XXL-Job的使用相对简单&#xff0c;只…

自学网络安全最细规划(建议收藏)

01 什么是网络安全 网络安全可以基于攻击和防御视角来分类&#xff0c;我们经常听到的 “红队”、“渗透测试” 等就是研究攻击技术&#xff0c;而“蓝队”、“安全运营”、“安全运维”则研究防御技术。 无论网络、Web、移动、桌面、云等哪个领域&#xff0c;都有攻与防两面…

微信小程序后台:解决微信扫普通链接地址无法跳转到体验版微信的问题,配置普通链接二维码规则解释和理解

微信小程序后台&#xff1a;解决微信扫普通链接地址无法跳转到体验版微信的问题&#xff0c;配置普通链接二维码规则解释和理解 一、现象与原因 最近突然发现微信管理平台中&#xff0c;设置好的普通二维码连接跳转到体验版小程序的功能&#xff0c;没有区分体验版和生产版&a…

条件变量基本使用

一、条件变量 应用场景&#xff1a;生产者消费者问题&#xff0c;是线程同步的一种手段。 必要性&#xff1a;为了实现等待某个资源&#xff0c;让线程休眠。提高运行效率 int pthread_cond_wait(pthread_cond_t *restrict cond, pthread_mutex_t *restrict mutex); int pthr…

手把手教你做独立t检验

一、案例介绍 为研究国产四类新药阿卡波糖胶囊的降血糖效果&#xff0c;某医院用40名2型糖尿病患者进行同期随机对照试验。研究者将这些患者随机等分到试验组&#xff08;用阿卡波糖胶囊&#xff09;和对照组&#xff08;用拜唐苹胶囊&#xff09;&#xff0c;分别测得试验开始…

如何使用宝塔面板搭建网站(Linux服务器配置篇)

搭建网站我们需要&#xff1a; 必须是Linux服务器&#xff08;最低要求配置1核1G当然再低些也能运行但是不建议&#xff09;自己的域名&#xff08;可以去阿里云或者腾讯云了解&#xff09;PHP项目 此处展示的是华为云服务器&#xff08;各个服务器的购买和使用差别不大&#…

“以API接口快速获得aliexpress速卖通商品详情-返回值说明

为了方便商家获取速卖通上的商品信息&#xff0c;速卖通提供了API接口来获取商品数据。本文将介绍如何通过API接口获取速卖通商品数据。 一、申请API接口权限 在使用API接口前&#xff0c;首先需要在速卖通官网注册账号并通过实名认证。然后&#xff0c;在个人资料页面找到开…

Java 的多线程浅析

前言 Java 的多线程在当今业界已经成为了一个非常重要的技能。无论您是新手还是经验丰富的程序员&#xff0c;精通 Java 的多线程编程都是至关重要的。因为多线程可以帮助您实现更快的应用程序、更高效的性能以及更出色的用户体验。在这篇文章中&#xff0c;我们将介绍有关 Ja…

【资料分享】PLC中输入输出端子

PLC输入输出分为高速和低速&#xff0c;一般来说不会超出&#xff0c;隔离器MOS的设计。其中具体采用光耦隔离还是数字隔离器隔离&#xff0c;其隔离器件会限制其输入输出的速率&#xff1b;PLC的源型和漏型就取决于最后末端所接的MOS管是如何布置的。 MOS管的源极和漏极 MOS…

Java 注解配合Spring AOP 导入Excel文件

Java 注解配合Spring AOP 导入Excel文件 这个就是把上一篇&#xff0c;封装了一层&#xff1b;根据注解中配置的变量名和方法名&#xff0c;通过JoinPoint获取到对应的对象和方法 注解 import static java.lang.annotation.ElementType.METHOD; import static java.lang.ann…