【数据结构与算法(Java版)】深度剖析二分查找算法

        【二分查找算法】的时间复杂度为O(log n),其中n为数组的长度。因为每次查找都将查找范围缩小一半,所以算法的时间复杂度是对数级别的。

 

目录

前言

二分查找算法是什么?

算法实现

方式一:(左闭右闭) 

文字描述 

流程图展示

案例分析 

代码示例

方式二:(左闭右开)

流程图差异

代码示例:

注意事项:

代码优化

总结


前言

        在生活中,我们经常会接触到查找。不同的查找方式效率也会有所不同,今天就来了解一下【二分查找算法】。首先,进入到如下场景中:

思考: 假设,有一个存在n个元素的升序排序数组(如下图),需要查找某个目标值在数组中的索引值。一般会如何去实现?

        按照我们正常的思路,可能首先想到的是遍历该数组,依次将每一个元素和目标值比较,直到找到目标值,返回索引,否则返回-1。代码示例如下:

package com.zhy.algorithm;public class LineSearch {/*** 返回目标值在升序数组中的索引,找不到返回-1。*  线性实现方式* @return*/public static int lineSearchM(int[] a,int target){for(int i = 0; i < a.length; i++){if (target == a[i]){return i;}}return -1;}
}

上面的实现方法,从代码行数来看,可谓是简洁,也更易于理解。但这样写会出现什么样的弊端呢?

通过几个场景来分析一下:

  • 场景1:查找目标值5在数组中的索引值:使用【线性查找】需要5次才能找到;而如果采用【二分查找法】,只用1次就能找到。
  • 场景2:查找目标值7在数组中的索引值:使用【线性查找】需要7次才能找到;而如果采用【二分查找法】,只用2次就能找到。
  • 场景3:查找目标值15在数组中的索引值:使用【线性查找】需要9次才能结束;而如果采用【二分查找法】,只用3次就能结束。

这么一对比,随着数据量不断增大的情况,使用【二分查找法】在时间效率上能得到很大的提升。那么我们就来看看,二分查找法究竟是怎样的一种算法?


二分查找算法是什么?

        二分查找算法(Binary Search Algorithm)也叫折半查找,是一种在有序数组中查找目标值的常用算法。它的基本思想是每次将待查找的区间缩小一半,直到找到目标值或者确定目标值不存在。是一种效率较高的查找算法。

算法实现

方式一:(左闭右闭) 

        左闭右闭指的是,在查找过程中,左边界和右边界都包含在查找范围内。也就是说,当找到当前中间元素与目标元素相等时,直接返回中间元素的位置。左闭右闭的写法是:while(left <= right)。

文字描述 

首先,了解一下二分查找算法具体的步骤如下:

  1. 初始化区间的起始位置left = 0,终止位置right = 数组的长度减1。
  2. 计算区间的中间位置(middle):middle = (left + right) / 2。
  3. 比较中间位置的值与目标值的大小关系:
    • 如果中间值等于目标值,则找到目标值,返回中间位置
    • 如果中间值大于目标值,则更新right = middle - 1,继续下一轮查找。
    • 如果中间值小于目标值,则更新left = middle + 1,继续下一轮查找。
  4. 重复步骤2和步骤3,直到left > right,表示找不到目标值,返回-1

流程图展示

案例分析 

下面,通过两个具体的案例,逐步了解一下算法的执行步骤:

案例一:从下面列表a中查找目标值8的索引。(找到的场景)

案例二: 从下面列表a中查找目标值20的索引。(找不到的场景)

代码示例

了解了二分查找算法的核心思想,那代码实现起来也不算难,代码示例如下:

package com.zhy.algorithm;public class BinarySearch {/*** 二分查找法实现方式一:(左闭右闭)*/public static int binarySearchOne(int[] a,int target){//1.记录数组的两端索引int left = 0;int right = a.length - 1;//2.循环两端索引在中间的数据,判定条件,当left <= right时,证明还有元素while (left <= right){//求left和right的一个中间索引int middle = (left + right) >>> 1;//用中间索引的值和目标值进行比较if (target == a[middle]){//1.目标值 == 中间值,可以确定,找到了,返回middlereturn middle;}else if(a[middle] < target){//2.目标值大于中间值的情况下,可以确定,目标值在右边,left索引往右移left = middle + 1;}else {//3.目标值小于中间值的情况下,可以确定,目标值在左边,right索引往左移right = middle - 1;}}//3.如果循环结束,没有找到,返回-1return -1;}public static void main(String[] args) {int[] a = {1,2,3,4,5,6,7,8,9};int target = 51;System.out.println("找到的场景:");for (int i = 0; i < a.length; i++){System.out.println("元素 " + a[i] + " 在数组中的位置为:" + binarySearchOne(a,a[i]));}System.out.println("\n没找到的场景:");System.out.println(target + " 在数组中的位置为:" + binarySearchOne(a,target));}
}

方式二:(左闭右开)

        左闭右开指的是,在查找过程中,左边界包含在查找范围内,但右边界不包含在查找范围内。也就是说,当找到当前中间元素与目标元素相等时,不返回中间元素的位置,而是将右边界设为中间元素的位置,继续向左查找。左闭右开的写法是:while(left < right)。

        可以用于对有序数组进行插入、删除等操作,因为当插入或删除一个元素时,不会改变数组的有序性,而左闭右开的写法可以保证查找时不会遗漏目标元素。但在普通的二分查找中,使用左闭右闭的写法就足够了。 

这两种实现方式的差异并不大,它们之间也不存在什么性能,下面我们一起看看实现差异:

流程图差异

代码示例:

package com.zhy.algorithm;public class BinarySearch {/*** 二分查找法实现方式二:(左闭右开)*/public static int binarySearchOne(int[] a,int target){//1.记录数组的两端索引int left = 0;int right = a.length;//2.循环两端索引在中间的数据,判定条件,当left <= right时,证明还有元素while (left < right){//求left和right的一个中间索引int middle = (left + right) >>> 1;//用中间索引的值和目标值进行比较if (target == a[middle]){//1.目标值 == 中间值,可以确定,找到了,返回middlereturn middle;}else if(a[middle] < target){//2.目标值大于中间值的情况下,可以确定,目标值在右边,left索引往右移left = middle + 1;}else {//3.目标值小于中间值的情况下,可以确定,目标值在左边,right索引往左移right = middle;}}//3.如果循环结束,没有找到,返回-1return -1;}public static void main(String[] args) {int[] a = {1,2,3,4,5,6,7,8,9};int target = 51;System.out.println("找到的场景:");for (int i = 0; i < a.length; i++){System.out.println("元素 " + a[i] + " 在数组中的位置为:" + binarySearchOne(a,a[i]));}System.out.println("\n没找到的场景:");System.out.println(target + " 在数组中的位置为:" + binarySearchOne(a,target));}
}

注意事项:

代码优化

前面计算中间值middle时,用的计算公式是:int middle = (left + right) / 2; 

但是这样写会出现怎么样的弊端呢?

        当left+right计算出来的值已经 > int类型的范围时,结果就无法预估了,那算法的结果自然也是不对的。

那我们应该解决这个问题呢?

        其实也很简单,通过位移运算符就可以了,每次往右移一位其实就是除以二,公式可以改为:int middle = (left + right) >>> 1;


总结

        在前言中,我们通过几个示例对比了线性查找和二分查找,来引入二分查找的高效率,但由于数据量太小以及查找的位置来看,其实很片面,下面我们通过一种“事前分析法”的方式来对比一下两个算法。

那怎么判断一个算法的好坏呢?

        一般是计算这个算法的最差的情况,那对于查找算法,最差的情况是不是就是找不到元素。首先,拆分前面两种算法每一行代码的执行次数。

线性查找法

拆分每一条语句的执行次数,假设元素个数是nint i = 0;          1次i < a.length;       n+1次if (target == a[i]) n次i++                 n次return -1           1次
将所有的执行次数相加,就是该算法的执行次数:3*n+3

二分查找法 

那【二分查找法】的循环次数怎么确定呢?其实有一个规律:
元素个数            循环次数        规律
4-7                 3次          log_2(4) + 1 = 2 + 1 = 3
8-15                4次          log_2(8) + 1 = 3 + 1 = 4
16-31               5次          log_2(16) + 1 = 4 + 1 = 5
32-63               6次          log_2(32) + 1 = 5 + 1 = 6
……                ……
从以上结果中,发现,元素个数和循环次数之间的规律为:log_2(n)(以2为底n的对数) + 1,
但由于元素个数是个区间,不能整除的向下取整,最终循环次数为 L = floor(log_2(n)) + 1下面拆分每一条语句的执行次数,假设元素个数是nint left = 0;                       1次int right = a.length - 1;           1次while (left <= right)               L + 1次int middle = (left + right) >>> 1;  L次target == a[middle];                L次a[middle] < target;                 L次left = middle + 1;                  L次return -1;                          1次
将所有的执行次数相加,就是该算法的执行次数:(floor(log_2(n)) + 1) * 5 + 4

算出来了两种算法的执行次数公式,下面取几个具体的数值带入n算一下。

假设n = 4:

        线性查找算法:3*n+3 = 3*4+3=15

        二分查找算法:(floor(log_2(n)) + 1) * 5 + 4 = (floor(log_2(4)) + 1) * 5 + 4 = 19    

这么一看,好像线性查找更快一点,别急,我们在带入一个大一点的数:

        

假设n = 1024:

        线性查找算法:3*n+3 = 3*1024+3=3075

        二分查找算法:(floor(log_2(n)) + 1) * 5 + 4 = (floor(log_2(1024)) + 1) * 5 + 4 = 59

反转来了,当数据量不断增大的时候,二分查找的效率快非常多。

        下面我们通过一个画来更直观的感觉,进入,Desmos | 图形计算器输入公式(n要换成x),可以看到二分查找是缓慢增加,而线性是直线增长。因此,可以得出结论,在一个有序的列表中查找数据,二分查找的执行效率远远高于线性查找。

 

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

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

相关文章

电机控制常见的外围器件

小型断路器&#xff1a; 这些通通都叫小型断路器&#xff0c;二十年的老电工不一定都认识&#xff0c;不信看看_哔哩哔哩_bilibili 1PIN 2PIN 3PIN 4PIN: 正常情况下火线和零线的电流是相等的&#xff0c;但是漏电的情况下&#xff0c;两线的电流差值大于30毫安&#xff0c;漏…

合纵连横 – 以 Flink 和 Amazon MSK 构建 Amazon DocumentDB 之间的实时数据同步

在大数据时代&#xff0c;实时数据同步已经有很多地方应用&#xff0c;包括从在线数据库构建实时数据仓库&#xff0c;跨区域数据复制。行业落地场景众多&#xff0c;例如&#xff0c;电商 GMV 数据实时统计&#xff0c;用户行为分析&#xff0c;广告投放效果实时追踪&#xff…

笔记本hp6930p安装Android-x86避坑日记

一、序言 农历癸卯年前大扫除&#xff0c;翻出老机hp6930p&#xff0c;闲来无事&#xff0c;便安装Android-x86玩玩&#xff0c;期间多次入坑&#xff0c;随手记之以避坑。 笔记本配置&#xff1a;T9600,4G内存&#xff0c;120G固态160G机械硬盘 二、Android-x86系统简介 官…

2023最新盲盒交友脱单系统源码

源码获取方式 搜一搜&#xff1a;万能工具箱合集 点击资源库直接进去获取源码即可 如果没看到就是待更新&#xff0c;会陆续更新上 或 源码软件库 最新盲盒交友脱单系统源码&#xff0c;纸条广场&#xff0c;单独抽取/连抽/同城抽取/高质量盒子 新增功能包括心动推荐&#xff…

js使用new Image()创建img对象不生效

我在做vue项目的时候&#xff0c;想获取到图片的宽高&#xff0c;然后把宽高发给后端&#xff0c;代码类似是这样的 function getFileData(file: File) {return new Promise(function (resolve, reject) {let reader new FileReader();reader.readAsDataURL(file);reader.onlo…

备考2024年高考全国甲卷文科数学:历年选择题真题练一练

距离2024年高考还有三个多月的时间&#xff0c;最后这个时间&#xff0c;同学们基本上是以刷题为主。刷题的时候最重要的是把往年的真题吃透&#xff0c;因为真题是严格按照考纲出的&#xff0c;掌握了真题后面的知识点&#xff0c;并能举一反三地运用&#xff0c;那么高考的高…

用Python Matplotlib画图导致paper中含有Type-3字体,如何解决?

用Python Matplotlib画图导致paper中含有Type-3字体&#xff0c;如何解决&#xff1f; 在提交ACM或者IEEE论文之前&#xff0c;都会有格式的检查&#xff0c;格式的其中一个要求是paper中不能含有Type-3的字体。因为Type-1和True Type字体都是矢量字体&#xff0c;而Type-3并不…

华为OD机试 - 数字排列(Java JS Python C C++)

题目描述 小明负责公司年会,想出一个趣味游戏: 屏幕给出 1 ~ 9 中任意 4 个不重复的数字,大家以最快时间给出这几个数字可拼成的数字从小到大排列位于第 N 位置的数字,其中 N 为给出数字中最大的(如果不到这么多数字则给出最后一个即可)。 注意: 2 可以当作 5 来使用…

老杨说运维 | 运维大数据价值探索

文末附有视频 伴随第六届双态IT乌镇用户大会的圆满完成&#xff0c;擎创科技“一体化数智管理和大模型应用”主题研讨会也正式落下了帷幕。 云原生转型正成为很多行业未来发展战略&#xff0c;伴随国家对信创数字化要求的深入推进&#xff0c;面对敏稳共存这一近年出现的新难…

MySQL死锁产生的原因和解决方法

一.什么是死锁 要想知道MYSQL死锁产生的原因,就要知道什么是死锁?在了解什么是死锁之前,先来看一个概念:线程安全问题 1.线程安全问题 1.1什么是线程安全问题 线程安全问题&#xff0c;指的是在多线程环境当中&#xff0c;线程并发访问某个资源&#xff0c;从而导致的原子性&a…

RocketMQ快速实战以及集群架构原理详解

RocketMQ快速实战以及集群架构原理详解 组成部分 启动Rocket服务之前要先启动NameServer NameServer 提供轻量级Broker路由服务&#xff0c;主要是提供服务注册 Broker 实际处理消息存储、转发等服务的核心组件 Producer 消息生产者集群&#xff0c;通常为业务系统中的一个功…

板块二 JSP和JSTL:第四节 EL表达式 来自【汤米尼克的JAVAEE全套教程专栏】

板块二 JSP和JSTL&#xff1a;第四节 EL表达式 一、什么是表达式语言二、表达式取值&#xff08;1&#xff09;访问JSP四大作用域&#xff08;2&#xff09;访问List和Map&#xff08;3&#xff09;访问JavaBean 三、 EL的各种运算符&#xff08;1&#xff09;.和[ ]运算符&…

《The Art of InnoDB》第二部分|第4章:深入结构-磁盘结构-redo log

4.3 redo log 目录 4.3 redo log 4.3.1 redo log 介绍 4.3.2 redo log 的作用 4.3.3 redo log file 结构 4.3.4 redo log 提交逻辑 4.3.5 redo log 持久化逻辑 4.3.6 redo log 检查点 4.3.7 小结

汇编语言与接口技术实践——秒表

1. 设计要求 基于 51 开发板,利用键盘作为按键输入,将数码管作为显示输出,实现电子秒表。 功能要求: (1)计时精度达到百分之一秒; (2)能按键记录下5次时间并通过按键回看 (3)设置时间,实现倒计时,时间到,数码管闪烁 10 次,并激发蜂鸣器,可通过按键解除。 2. 设计思…

抖音数据抓取工具|短视频下载工具|视频内容提取软件

一、开发背景&#xff1a; 随着抖音平台的流行&#xff0c;越来越多的人希望能够下载抖音视频以进行个人收藏或分享。然而&#xff0c;目前在网上找到的抖音视频下载工具功能单一&#xff0c;操作繁琐&#xff0c;无法满足用户的需求。因此&#xff0c;我们决定开发一款功能强大…

java面试题之mysql篇

1、数据库索引 ​​​​​​​ 索引是对数据库表中一列或多列的值进行排序的一种结构&#xff0c;使用索引可快速访问数据库表中的特定信息。如果想按特定职员的姓来查找他或她&#xff0c;则与在表中搜索所有的行相比&#xff0c;索引有助于更快地获取信息。 索引的一个主要…

编程笔记 Golang基础 030 接口

编程笔记 Golang基础 030 接口 一、接口的定义&#xff1a;二、接口的实现&#xff1a;三、接收者类型四、应用示例五、接口的意义 在Go语言中&#xff0c;接口是一种类型定义&#xff0c;它描述了一组方法签名&#xff0c;任何实现了这些方法的类型都隐式地实现了这个接口。这…

k8s-创建命名空间的方法

使用命令式创建namespace kubectl create namespace test-namespace查看命名空间 kubectl get namespace使用声明式创建命名空间 a. 编写dev-namespace.yaml文件 apiVersion: v1 kind: Namespace metadata:name: dev-namespaceb. 使用dev-namespace.yaml&#xff0c;yaml文件创…

音视频开发之旅(69)-SD图生图

目录 1. 效果展示 2. ControlNet介绍 3. 图生图流程浅析 4. SDWebui图生图代码流程 5. 参考资料 一、效果展示 图生图的应用场景非常多&#xff0c;比较典型的应用场景有风格转化&#xff08;真人与二次元&#xff09;、线稿上色、换装和对图片进行扩图等&#xff0c;下面…

TCP/IP协议栈:模拟器实现基本的L2和L3功能

在C中实现的TCPI/IP网络堆栈模拟器。该模拟器实现基本的第2层&#xff08;MAC地址&#xff0c;Arp&#xff09;和第3层&#xff08;路由&#xff0c;IP&#xff09;功能。 TCP/IP协议栈是一个网络通信的基础架构&#xff0c;包含了多层次的协议和功能。在模拟实现基本的L2和L3…