【数据结构与算法(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…

备考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并不…

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

文末附有视频 伴随第六届双态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;.和[ ]运算符&…

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

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

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

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

java面试题之mysql篇

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

音视频开发之旅(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…

神经网络2-卷积神经网络一文深度读懂

卷积神经网络&#xff08;Convolutional Neural Network, CNN&#xff09;是一类包含卷积计算且具有深度结构的前馈神经网络&#xff08;Feedforward Neural Networks&#xff09;&#xff0c;主要用于图像识别、语音识别和自然语言处理等任务&#xff0c;是深度学习&#xff0…

使用决策树算法预测隐形眼镜类型

目录 谷歌笔记本&#xff08;可选&#xff09; 编写算法&#xff1a;决策树 准备数据&#xff1a;拆分数据集 测试算法&#xff1a;构造注解树 使用算法&#xff1a;预测隐形眼镜类型 谷歌笔记本&#xff08;可选&#xff09; from google.colab import drive drive.mount…

Springboot之压缩逻辑源码跟踪流程

背景 在项目开发过程中&#xff0c;前后端参数比较多&#xff0c;导致网络传输耗时比较多&#xff0c;因此想将数据压缩传输&#xff0c;以减少网络传输的耗时&#xff0c;从而减少接口的响应时间&#xff0c;可以自己实现&#xff0c;但是spring相关的框架已经内置了该功能&am…

堆排序、快速排序和归并排序

堆排序、快速排序和归并排序是所有排序中最重要的三个排序&#xff0c;也是难度最大的三个排序&#xff1b;所以本文单独拿这三个排序来讲解 目录 一、堆排序 1.建堆 2.堆排序 二、快速排序 1.思想解析 2.Hoare版找基准 3.挖坑法找基准 4.快速排序的优化 5.快速排序非…

Servlet使用Cookie和Session

一、会话技术 当用户访问web应用时&#xff0c;在许多情况下&#xff0c;web服务器必须能够跟踪用户的状态。比如许多用户在购物网站上购物&#xff0c;Web服务器为每个用户配置了虚拟的购物车。当某个用户请求将一件商品放入购物车时&#xff0c;web服务器必须根据发出请求的…