二分搜索算法

news2024/10/6 8:22:40

目录

  • 1.概述
  • 2.代码实现
    • 2.1.最基本的二分搜索
    • 2.2.搜索最左侧边界
    • 2.3.搜索最右侧边界
  • 3.应用

本文参考:
LABULADONG 的算法网站
《大话数据结构》

1.概述

(1)二分搜索 (Binary Search),又称为折半搜索 (Half-interval Search)。它的前提是线性表中的记录必须是关键码有序(通常从小到大有序),线性表必须采用顺序存储。

(2)二分搜索的基本思想是:在有序表中,取中间记录作为比较对象,若给定值与中间记录的关键字相等,则查找成功;若给定值小于中间记录的关键字,则在中间记录的左半区继续查找;若给定值大于中间记录的关键字,则在中间记录的右半区继续查找。不断重复上述过程,直到查找成功,或所有查找区域无记录,查找失败为止。

(3)二分搜索的时间复杂度为 O(log2n)。

2.代码实现

2.1.最基本的二分搜索

(1)最基本的二分搜索:给定一个 n 个元素有序的(升序)整型数组 nums 和一个目标值 target,搜索 nums 中的 target,如果目标值存在返回下标,否则返回 -1。实现代码如下:

class Solution {
    public int binarySearch(int[] nums, int target) {
        int left = 0;
        int right = nums.length - 1;
        while (left <= right) {
        	//防止整数溢出
            int mid = left + (right - left) / 2;
            if (nums[mid] == target) {
                return mid;
            } else if (nums[mid] < target) {
                left = mid + 1;
            } else if (nums[mid] > target) {
                right = mid - 1;
            }
        }
        return -1;
    }
}

该代码可以解决 LeetCode 中的704. 二分查找这题。

(2)大家肯定对上面的代码非常熟悉,不过这里需要其中的一些细节进行讨论:

① 在计算 mid 时,有以下两种方式:

int mid = (left + right) / 2;
int mid = left + (right - left) / 2;

这两种方法的计算结果相同,但需要注意的是:如果 left 和 right 的值非常大,那么 left + right 可能会出现整数溢出的情况,虽然该情况出现的概率比较低,但是从代码健壮性的角度来考虑,本文还是推荐使用第二种方式

② while 循环条件写成 left <= right 和 left < right 的区别如下:

  • 上述代码中的搜索区间为 [left, right],注意区间的两边均为闭区间,而 while (left <= right) 的终止条件是 left == right + 1,此时的区间表示为 [right + 1, right],这样的区间不存在,即此时的区间为空。所以此时 while 循环终止是正确的,直接返回 -1 即可。
  • while (left < right) 的终止条件是 left == right,此时的区间表示为 [right, right](例如 right = 2,那么区间为 [2, 2])此时区间非空,还有一个数 2,但此时 while 循环终止了,也就是说区间 [2, 2] 被漏掉了,索引为 2 的元素没有被搜索到,如果这时候直接返回 -1 就是错误的。
  • 如果非要写成 while (left < right),那么直接在返回语句处稍作修改即可:
//...
while(left < right) {
    // ...
}
return nums[left] == target ? left : -1;

③ 上述二分搜索代码的存在一定的局限性。例如,当 nums = {1, 4, 4, 4, 7},target = 4 时,此时返回的索引为 2,这没有问题。但是如果想得到 target 的最左/右侧边界的索引时(即分别对应索引 1 和索引 3),则该算法无法直接满足该要求,只能继续向左/右进行线性搜索,但是这样就无法保证二分搜索的对数级的时间复杂度。因此下面将讨论搜索 target 的最左/右侧边界的索引的情况。

2.2.搜索最左侧边界

在这里插入图片描述
(1)实现代码如下:

public int leftBoundSearch(int[] nums, int target) {
    int left = 0;
    int right = nums.length - 1;
    //当 left > right 即 left = right + 1 时结束循环
    while (left <= right) {
        int mid = left + (right - left) / 2;
        if (nums[mid] < target) {
            left = mid + 1;
        } else if (nums[mid] > target) {
            right = mid - 1;         
        } else if (nums[mid] == target) {
            right = mid - 1;
        }
    }
    /*
    	检查 left 越界的情况:
		当 target 比所有元素都大时,循环结束后 left = nums.length
	*/
    if (left >= nums.length || nums[left] != target) {
        return -1;
    }
    return left;
}

(2)下面对该代码中的一些细节进行讨论:

① 在 while 循环中,当 nums[mid] == target 时:

  • mid 未必就是 target 的最左侧边界索引(参考上面提到的例子),故不能直接返回 mid;
  • 而由于我们想得到 target 的最左侧边界索引,且此时 nums[left…mid…right] 中的 nums[mid] 已经等于 target,那么接下来应该在 nums[left…mid - 1] 区间内进行搜索,所以要锁定左侧边界,并令 right = mid - 1;
  • 如果此时的 mid 恰巧就是最左侧边界索引(记为 leftBound ),那么此时 nums[left…mid - 1] 区间内的元素均小于 target,所以在后面的搜索中只有 left 会通过 left = mid + 1 被不断更新,而 right = leftBound - 1保持不变。又由于当 left > right,即 left = right + 1 时结束循环,所以最终 left = leftBound - 1 + 1 = leftBound ;

② 当数组 nums 中不存在 target:

  • target 比所有元素都大时,循环结束后 left = nums.length;
  • 其它情况下,直接判断 nums[left] 是否等于 target 即可;

③ 代码中的两个 else if 语句其实用一个 else 语句代替,这里写出来主要是为了方便理解,合并后的代码如下:

while (left <= right) {
	int mid = left + (right - left) / 2;
	if (nums[mid] < target) {
	    left = mid + 1;
	} else {
	    right = mid - 1;         
	}
}

2.3.搜索最右侧边界

在这里插入图片描述
(1)实现代码如下:

public int rightBoundSearch(int[] nums, int target) {
    int left = 0;
    int right = nums.length - 1;
    //当 left > right 时结束循环
    while (left <= right) {
        int mid = left + (right - left) / 2;
        if (nums[mid] > target) {
        	right = mid - 1;
        } else if (nums[mid] < target) {
            left = mid + 1;
        } else if (nums[mid] == target) {
            left = mid + 1;
        }
    }
    /*
    	检查 right 越界的情况:
		当 target 比所有元素都小时,循环结束后 right 会被减到 -1
	*/
    if (right < 0 || nums[right] != target) {
        return -1;
    }
    return right;
}

(2)下面对该代码中的一些细节进行讨论:

① 在 while 循环中,当 nums[mid] == target 时:

  • mid 未必就是 target 的最右侧边界索引(参考上面提到的例子),故不能直接返回 mid;
  • 而由于我们想得到 target 的最右侧边界索引,且此时 nums[left…mid…right] 中的 nums[mid] 已经等于 target,那么接下来应该在 nums[mid + 1…right] 区间内进行搜索,所以要锁定右侧边界,并令 left = mid + 1;
  • 如果此时的 mid 恰巧就是最右侧边界索引(记为 rightBound),那么此时 nums[mid + 1…right] 区间内的元素均大于 target,所以在后面的搜索中只有 right 会通过 right = mid - 1 被不断更新,而 left = rightBound + 1保持不变。又由于当 left > right,即 right = left - 1 时结束循环,所以最终 right = rightBound+ 1 - 1 = rightBound;

② 当数组 nums 中不存在 target:

  • target 比所有元素都小时,循环结束后 right = -1;
  • 其它情况下,直接判断 nums[right] 是否等于 target 即可;

③ 代码中的两个 else if 语句其实用一个 else 语句代替,这里写出来主要是为了方便理解,合并后的代码如下:

while (left <= right) {
    int mid = left + (right - left) / 2;
    if (nums[mid] > target) {
    	right = mid - 1; 
    } else {
        left = mid + 1;
    }
}

注意:上面的最左/右侧边界讨论的均是数组 nums 中的元素为升序的情况,如果为降序,那么只需相应改变 if 条件中的比较符号即可。

3.应用

(1)可以使用二分搜索的关键在于:能够从问题中抽象出一个自变量 x,一个关于 x 的函数 f(x),以及一个目标值 target,并且 f(x) 必须是在 x 定义域上的单调函数(也可看做有序的),并且题目是要求出 f(x) == target 时的 x 的值(或者 x 的最左/右侧边界值)。

(2)这里以最基本的二分搜索(即 LeetCode 中的704. 二分查找这题)为例,分别找出对应的 x、f(x) 和 target:

x数组 nums 的下标,其范围(定义域)为 [0, nums.length - 1]
f(x)f(x) = nums[x],f(x) 是关于自变量 x 的函数,由于数组 nums 是有序的,所以 f(x) 在定义域上是单调的
target题目中给的 target

(3)从最基本的二分搜索中抽象出 x、f(x) 和 target 较为简单,下面再来看 LeetCode 中的875. 爱吃香蕉的珂珂这题:

在这里插入图片描述

分析如下:

x吃香蕉的速度,即题中的 k
f(x)以速度 x 吃完香蕉所用的时间,并且它是单调递减的,即有序的(可以单独写一个函数来求解)
target警卫离开后回来的时间,即题中的 H

此外,需要注意的是,本题是要求 x/k 的最小值,那么即对应搜索 f(x) = target 时 x 的最左侧边界,对应的如下图所示:

在这里插入图片描述

具体细节可以查看LeetCode_二分搜索_中等_875.爱吃香蕉的珂珂这篇文章,最终的实现代码如下:

//思路1————二分搜索
class Solution {
    public int minEatingSpeed(int[] piles, int h) {
        //maxK: 吃香蕉的最大速度
        int maxK = piles[0];
        for (int i = 1; i < piles.length; i++) {
            if (piles[i] > maxK) {
                maxK = piles[i];
            }
        }
        //吃香蕉的最小速度为 1 根/小时,最大速度为 maxK 根/小时
        int left = 1;
        int right = maxK;
        //查找左边界的二分搜索,计算吃完香蕉的最小速度
        while (left <= right) {
            int mid = left + (right - left) / 2;
            if (getTime(piles, mid) > h) {
                left = mid + 1;
            } else {
                right = mid - 1;
            }
        }
        //由于本题一定有解,故不需要做边界判断
        return left;
    }
    
    /*
    	实现 f(x)
        k:吃香蕉的速度(单位:根/小时)
        返回值:吃完所有香蕉所需的时间(单位:小时)
    */
    public long getTime(int[] piles, int k) {
        //防止整数溢出
        long hour = 0;
        for (int i = 0; i < piles.length; i++) {
            hour += piles[i] / k;
            if (piles[i] % k > 0) {
                hour++;
            }
        }
        return hour;
    }
}

(4)读到这里,想必大家对二分搜索有了一定的了解,大家可以去 LeetCode 上找相关的二分搜索的题目来练习,或者也可以直接查看LeetCode算法刷题目录(Java)这篇文章中的二分搜索章节。如果大家发现文章中的错误之处,可在评论区中指出。

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

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

相关文章

云收藏系统|基于Springboot实现云收藏系统

作者主页&#xff1a;编程指南针 作者简介&#xff1a;Java领域优质创作者、CSDN博客专家 、掘金特邀作者、多年架构师设计经验、腾讯课堂常驻讲师 主要内容&#xff1a;Java项目、毕业设计、简历模板、学习资料、面试题库、技术互助 收藏点赞不迷路 关注作者有好处 文末获取源…

Java实现队列

目录 一、队列概述 二、队列的模拟实现 1、入队 2、出队 3、取队头元素 4、获取队列长度 三、循环队列 1、入队 2、出队 3、取队头元素 4、取队尾元素 四、面试题 1、用队列实现栈 2、用栈实现队列 一、队列概述 队列也是常见的数据结构&#xff0c;是一…

Mybatis源码解析二:DataSource数据源负责创建连接以及Transaction的事物管理

简介 对于一个成熟的ORM框架来说&#xff0c;数据源的管理以及事务的管理一定是不可或缺的组成&#xff0c;对于Mybatis来说&#xff0c;为了使用方便以及扩展简单也是做了一系列的封装&#xff0c;这一篇主要介绍mybatis是如何管理数据源以及事务的。 数据源DataSource Dat…

【深度学习】李宏毅2021/2022春深度学习课程笔记 - Adversarial Attack(恶意攻击)

文章目录一、基本概念1.1 动机1.2 恶意攻击的例子1.3 如何攻击&#xff1f;二、White Box vs Black Box三、One Pixel Attack四、Universal Adversarial Attack五、Beyond Image六、Attack in the Physical World七、Adversarial Reprogramming八、Backdoor in Model九、防御9.…

TLS回调函数实现反调试

title: TLS回调函数实现反调试.md date: 2022-06-16 23:40:49.231 updated: 2022-06-16 23:41:11.924 url: /archives/tls回调函数实现反调试 categories: tags: 逆向 TLS回调函数实现反调试 TLS-线程局部存储 先于我们OEP执行 #include<stdlib.h> #include<time.…

使用红黑树封装map、set

map、set如何用红黑树封装 map、set应用&#xff1a;map是一个使用参数K、参数V的类模板&#xff0c;set是只使用参数K的类模板。因为map应用时&#xff0c;需要使用到KV&#xff0c;而set只是存单个值&#xff0c;K。红黑树类的存储 &#xff1a;map和set类中使用红黑树数据成…

Logback配置详解

简介&#xff1a; logback是java的日志开源组件&#xff0c;是log4j创始人写的&#xff0c;性能比log4j要好&#xff0c;目前主要分为3个模块&#xff1a; logback-core:核心代码模块logback-classic:log4j的一个改良版本&#xff0c;同时实现了slf4j的接口&#xff0c;这样你…

树莓派mjpg-streamer实现监控功能

树莓派实现监控功能&#xff0c;调用mjpg-streamer库来实现。mjpg-streamer是一个开源的摄像头媒体流&#xff0c;通过本地获取摄像头的数据&#xff0c;通过http通讯发送&#xff0c;可以通过浏览器访问树莓派的IP地址和端口号就能看到视频流。 实现步骤 1.git clone https:…

关于内核的概念理解

狭义的操作系统可以认为就是内核&#xff0c;比如Linux内核。广义的操作系统则包括内核和一系列应用软件&#xff0c;比如Linux内核编辑器vim编译器gcc命令行解释器&#xff08;shell&#xff09;等&#xff0c;通常称为GNU/Linux。 源代码https://github.com/torvalds/Linux …

Jenkins自动化部署SpringBoot项目(windows环境)

文章目录1、Jenkins介绍1.1、概念1.2、优势1.3、Jenkins目的2、环境准备3、Jenkins下载3.1、下载3.2、运行3.3、问题解决4、Jenkins配置4.1、用户配置4.2、系统配置4.3、全局工具配置-最重要5、新建项目7、测试8、错误解决1、Jenkins介绍 1.1、概念 Jenkins是一个开源软件项目…

自动化测试Seleniums~1

一.什么是自动化测试 1.自动化测试介绍 自动化测试指软件测试的自动化&#xff0c;在预设状态下运行应用程序或者系统&#xff0c;预设条件包括正常和异常&#xff0c;最后评估运行结果。将人为驱动的测试行为转化为机器执行的过程。 将测试人员双手解放&#xff0c;将部分测…

黑马javaWeb Brand综合案例

01-综合案例-环境搭建 02-查询所有-后台&前台

leetcode83周赛

前言&#xff1a; 周赛两题选手,有点意思 830.较大分组的位置 思路&#xff1a;wa了三发&#xff0c;对边界了解的不够清楚 可以有一个小小的优化,时间复杂度O(n) // arr.add(start); //arr.add(i-1); //res.add(arr); res.add(Arrays.asList(start,i - 1));class Solution {pu…

MATLAB-mesh/ezmesh函数三维图形绘制

l ) mesh 函数生成由X、Y和Z指定的网线面&#xff0c;由C指定颜色的三维网格图。具体调用方法如下。mesh(Z):分别以矩阵Z的行、列下标作为x轴和y轴的自变量绘图。mesh(X , Y,Z):最常用的一般调用格式。mesh(X,Y ,Z,C):完整的调用格式&#xff0c;C用于指定图形的颜色&#xff0…

Ubuntu 20.4 美化桌面、美化引导界面、Mac 既视感

文章目录相关资源安装 gnome-tweaks安装浏览器插件方法一方法二&#xff08;推荐&#xff09;主题美化进行美化配置效果图美化前美化后美化 Dock扩展推荐引导美化安装主题修改配置相关资源 https://pan.baidu.com/s/1D7ZfzVKMmeZPAzuDDAVUbg提取码&#xff1a;ws3f 安装 gnom…

Java基础学习笔记(十)—— 包装类与泛型

包装类与泛型1 包装类1.1 基本类型包装类1.2 Integer类1.3 自动装箱 / 拆箱2 泛型2.1 泛型概述2.2 泛型的用法2.3 类型通配符1 包装类 1.1 基本类型包装类 基本类型包装类的作用 将基本数据类型封装成对象 的好处在于可以在对象中定义更多的功能方法操作该数据 public stat…

C库函数:stdlib.h

stdlib.h C 标准库 – <stdlib.h> | 菜鸟教程 (runoob.com) 该库主要涉及“字符串和其他类型数据的转换”、“内存空间的申请和释放”、“查找和排序”、随机数等功能函数。 7void *calloc(size_t nitems, size_t size) 分配所需的内存空间&#xff0c;并返回一个指向它…

大幅度减少零样本学习所需的人工标注

零样本旨在模仿人类的推理过程&#xff0c;利用可见类别的知识&#xff0c;对没有训练的样本不可见类别进行识别&#xff0c; 类别嵌入&#xff1a;Class embedding&#xff1a; 描述类别语义和视觉特征的向量&#xff0c;能够实现知识在类别间的转移&#xff0c;因而在零样本…

Web进阶:Day2 空间转换、动画

Web进阶&#xff1a;Day2 Date: January 4, 2023 Summary: 空间转换、动画 空间转换 **空间&#xff1a;**是从坐标轴角度定义的。 x 、y 和z三条坐标轴构成了一个立体空间&#xff0c;z轴位置与视线方向相同 空间转换也叫3D转换 属性&#xff1a;transform 语法&#xff1…

SolidWorks二次开发 API-获取当前语言与重命名文件

新的一年了&#xff0c;开始新的分享。 做SolidWorks二次开发的时候&#xff0c;难免会遇到多语言的问题。 针对不同语言的客户生成不同语言的菜单&#xff0c;所以我们要知道Solidworks的当前界面语言是什么。 这个就简单的说一下方法: GetCurrentLanguage 看结果&#xff1a;…