二分查找也叫折半查找,是一种常见的查找方法,它将原本是线性时间提升到了对数时间范围,大大缩短了搜索时间。
二分查找必须具备两个条件,一是数列必须使用顺序存储结构(例如数组),二是数列必须有序。
以升序数列为例,比较一个元素与数列中的中间位置的元素的大小,如果比中间位置的元素大,则继续在后半部分的数列中进行二分查找;如果比中间位置的元素小,则在数列的前半部分进行比较;如果相等,则找到了元素的位置。
每次比较的数列长度都会是之前数列的一半,直到找到相等元素的位置或者最终没有找到要找的元素。
举一个实例来看一下二分查找的详细过程。
假设待查找数列为 1、3、5、7、9、11、19,我们要找的元素为 18。首先待查数列下图所示,我们找到中间的元素 7( (1+7)/2=4,第 4 个位置上的元素)。
中间元素为 7,我们要找的元素比 7 大,继续在后半部分查找。如下图所示,现在后半部分数列为 9、11、19,继续寻找后半部分的中间元素。
中间元素为 11,与 11 比较,比 11 大,则继续在后半部分查找,后半部分只有一个元素 19 了,这时直接与 19 比较,若不相等,则说明在数列中没有找到元素,结束查找。
递归方法实现二分查找的过程如下所示
public class BinarySearch {
private int[] array;
/**
* 递归实现二分查找
* @param target
* @return
*/
public int searchRecursion(int target) {
if (array != null) {
return searchRecursion(target, 0, array.length - 1);
}
return -1;
}
private int searchRecursion(int target, int start, int end) {
if (start > end) {
return -1;
}
int mid = start + (end - start) / 2;
if (array[mid] == target) {
return mid;
} else if (target < array[mid]) {
return searchRecursion(target, start, mid - 1);
} else if (target > array[mid]) {
return searchRecursion(target, mid + 1, end);
}
}
}
进行二分查找时注意的技巧:
- 计算 mid 时建议写成: mid = left + (right - left) / 2,防止 left+right 造成数据溢出。
- 写二分查找时,每一种可能尽量用else if全部写清楚,尽量少用else,可以更好地展示所有细节。
非递归方式实现二分查找的过程如下所示
public class BinarySearch {
private int[] array;
/**
* 初始化数组
* @param array
*/
public BinarySearch(int[] array) {
this.array = array;
}
/**
* 二分查找
* @param target
* @return
*/
public int search(int target) {
if (array == null) {
return -1;
}
int start = 0;
int end = array.length - 1;
while (start <= end) {
//防止只有1个元素的搜索空间[left,right]--left==right时,如果是<,则搜索空间为空
int mid = start + (end - start) / 2;
if (array[mid] == target) {
return mid;
} else if (target < array[mid]) {
end = mid - 1; //变成mid-1或mid+1的原因是,mid在上一轮搜索中已经搜索过了,需要从搜索空间中移除
} else {
start = mid + 1;
}
}
return -1;
}
}
这里要注意的点:
循环的判定条件是:start <= end
,防止只有一个元素,即start == end时,如果用<
,会导致此时的搜索空间为空。
提出一个思路:为什么用二分查找,而不是三分之一、四分之一查找?
举一个实际的例子:我们在查字典的时候,如果要查以a开头的单词,则你会怎么翻字典?肯定是从最前面开始翻;如果要查以 z 开头的单词,则应该会从最后开始翻。显而易见,你不会采用二分查找的方式去查这个单词在哪,因为这样你会很累。
同样,假设数据的范围是 1~10000,让你找 10,你会怎么做?简单来说,我觉得直接用顺序查找都比二分查找更快,因为数列是升序的,用顺序查找比二分查找的比较次数少。
综上考虑,我们可以优化一下二分查找,并不一定要从正中间开始分,而是尽量找到一个更接近我们要找的那个数字的地方
,这样能够减少很多查找次数。
之前我们都是根据长度去找到这个中间位置,现在是根据 key 所在的序列范围区间去找到这个位置。要查找的位置 P = low + (key-a[low]) / (a[high]-a[low]) × (high-low)
,这是有点复杂,但是仔细看一下,这种计算方式其实就是为了找 key 所在的相对位置,让 key 的值更接近划分的位置,从而减少比较次数。
这种对二分查找的优化叫作插值查找,插值查找对于数列比较大并且比较均匀的数列来说,性能会好很多;但是如果数列极不均匀,则插值查找未必会比二分查找的性能好。
二分查找的平均查找长度为 ((n+1)log2(n+1))/n-1,有的书上写的是 log2(n+1)-1,或者是 log2n,具体计算比较麻烦,这里就不讨论了。
二分查找有个很重要的特点,就是不会查找数列的全部元素,而查找的数据量其实正好符合元素的对数,正常情况下每次查找的元素都在一半一半地减少。所以二分查找的时间复杂度为 O(log2n)
。当然,最好的情况是只查找一次就能找到,但最坏和一般情况下的确比顺序查找好了很多。
二分查找要求数列本身有序,所以在选择的时候需要确认数列是否本身有序,如果无序,则还需要进行排序,确认这样的代价是否符合实际需求。其实我们在获取一个列表的很多时候,可以直接使用数据库针对某个字段进行排序,在程序中需要找出某个值的元素时,就很适合使用二分查找了。
二分查找适合元素稍微多一些的数列,如果元素只有十几或者几十个,则其实可以直接使用顺序查找(当然,也有人在顺序查找外面用了一个或几个大循环,执行这几层大循环需要计算机执行百万、千万遍,没有考虑到机器的性能)。
一般对于一个有序列表,如果只需要对其进行一次排序,之后不再变化或者很少变化,则每次进行二分查找的效率就会很高;但是如果在一个有序列表中频繁地插入、删除数据,那么维护这个有序列表的代价就比较高昂。
比如有序数组 nums = [1,2,2,2,3],target = 2,此算法返回的索引是 2。但如果想得到 target 的左侧边界,即索引 1,或者想得到 target 的右侧边界,即索引 3,此时普通的二分查找算法是无法处理的。
这样的需求很常见。你也许会说,找到一个 target 索引,然后向左或向右线性搜索不行吗?可以,但是不好,因为这样难以保证二分查找对数级的时间复杂度了。为满足上述需求,需要对二分查找进行扩展,即寻找左侧和右侧边界的二分查找。
寻找左侧边界的二分查找
int left_bound(int[] nums, int target) {
if (nums.length == 0) return -1;
int left = 0;
int right = nums.length; // 注意
while (left < right) {
// 注意
int mid = left + (right - left) / 2;
if (nums[mid] == target) {
right = mid;
} else if (nums[mid] < target) {
left = mid + 1;
} else if (nums[mid] > target) {
right = mid; // 注意
}
}
return left;
}
为什么这个算法能够查找到左侧边界
关键在于对 nums[mid] == target 这种情况的处理:
if(nums[mid] == target) right = mid;
可见,找到 target 时不会立即返回,而是缩小「搜索区间」的上界 right,在区间 [left, mid) 中继续搜索,即不断向左收缩,达到锁定左侧边界的目的。
寻找右侧边界的二分查找
寻找右侧边界和寻找左侧边界的代码差不多,只有两处不同,已标注:
int right_bound(int[] nums, int target) {
if (nums.length == 0) return -1;
int left = 0, right = nums.length;
while (left < right) {
int mid = (left + right) / 2;
if (nums[mid] == target) {
left = mid + 1; // 1 注意
} else if (nums[mid] < target) {
left = mid + 1;
} else if (nums[mid] > target) {
right = mid;
}
}
return left - 1; // 2 注意
}
二分查找的思想很简单,但细节处需要十分留意:循环结束的判断条件、搜索空间是左闭右开还是其他方式等等,需要仔细确认自己设置的边界条件。
文章浏览阅读582次,点赞22次,收藏10次。重入锁ReentrantLock,顾名思义,就是支持重进入的锁,它表示该锁能够支持一个线程对资源的重复加锁。除此之外,该锁的还支持获取锁时的公平和非公平性选择。关键字隐式的支持重进入,比如一个synchronized修饰的递归方 法,在方法执行时,执行线程在获取了锁之后仍能连续多次地获得该锁虽然没能像synchronized关键字一样支持隐式的重进入,但是在调用lock()方 法时,已经获取到锁的线程,能够再次调用lock()方法获取锁而不被阻塞。
文章浏览阅读764次,点赞18次,收藏16次。作为一门面向对象的编程语言,使用 C++ 编写程序有一个缺点,即随着代码面向对象程度的提高,其执行效率反而会降低。例如,经实验证明几乎在所有情况下,直接操作一个 double 类型变量的执行效率,要比操作一个含 double 类型成员属性的类对象更高。对于大多数读者来说,以上所说是很容易想通的,因为它符合我们对高级编程语言的认知。但本节要介绍的内容,一定程序上会打破这个认知。
文章浏览阅读189次。EL1809 数字量输入端子模块采集现场设备中的二进制 24 V 控制信号,并以电气隔离的形式将这些信号传输到上层自动化单元。该 EtherCAT 端子模块有 16 个通道,每个通道都有一个 LED 用来指示其信号状态。端子模块的各个电源触点互相连接。对于 EL1809,所有输入端的参考地都是 0 V 电源触点。端子模块外壳宽度仅为 12 毫米,有 16 个接点,具有高封装密度。在最小的空间内通过单线制连接技术直接连接多通道传感器。3 ms 输入滤波,消除机械开关弹跳现象。输入规格 type 1/3。
文章浏览阅读672次,点赞25次,收藏18次。Hi,各位同学好呀,这里是M学姐!今天向大家分享一个今年最新完成的毕业设计项目作品,【基于SpringBoot的养老院管理系统】学姐根据实现的难度和等级对项目进行评分(最低0分,满分5分)难度系数:3分工作量:5分创新点:3分界面美化:5分界面美化的补充说明:使用vue的基本都能达到5分项目包含内容如下项目分享:见文末!⽬前,中国已成为世界上⽼年⼈⼝最多的国家,⼈⼝⽼龄化问题较为严重;但是,传统的养⽼院存在管理模式过于⽼套落后,⽼⼈信息管理不够便捷、护⼯⼈员管理不够⾼效等问题。
文章浏览阅读8w次,点赞492次,收藏3.1k次。前言可能受到新冠病毒的影响,台大也开始了网课教学。李宏毅上传了2020版本的机器学习视频,可以说是非常好的学习资料(尽管其中多数都是2017、2019的视频,但有部分更新)。和吴恩达的CS229机器学习相比,中文版本的机器学习显得亲民了许多,李宏毅的机器学习是英文的ppt+中文讲解,非常有利于大家入门。吴恩达的CS229中偏向于传统机器学习……_李宏毅2020机器学习
文章浏览阅读1.2w次,点赞2次,收藏3次。java注解(annotation)的执行顺序_多个annotation先执行哪个
文章浏览阅读2.6w次,点赞4次,收藏11次。win10一大特点就是触摸板多功能,单击双指三指四指,双指滑动,三指滑动等等,其中我觉得双指单击触摸板模拟鼠标右键是非常实用的功能,如果你的电脑双指单击没效果,那你的注册表需要修改了,步骤是:1.win+R:输入regedit 回车2.这里需要注意一点,两个文件夹下面的都需要改,一个文件夹下面两个需改改的,需要特别注意!3.完了一定要重启!!!!!!!!!..._win10系统重装运行检查发现注册表没有synaptics,无法使用双指触控?
文章浏览阅读646次。今日干货刚刚发表查看:66666回复:666公众号后台回复 ssm,免费获取松哥纯手敲的 SSM 框架学习干货。最近一段时间一直在发安全相关的 Spring Security 和 OAuth2,当然这两个系列还在继续,对 Spring Security 和 OAuth2 感兴趣的小伙伴,不要错过前面的文章哦,本文主要将一些理论上的东西,所以要是前面的 OAuth2 不懂,可能阅读起来有些..._openfeign调用需要走网关吗
文章浏览阅读5.5k次。API 版本:3.24 为方便理解,把图层按空间分成两类: graphicsLayers+featureLayers和basemapLayers+其他layers。graphicsLayers+featureLayers在上层,basemapLayers+其他layers在下层,basemapLayers默认在下层的底部。graphicsLayers+featureLayers的图层I..._arcgis js api 图层顺序
文章浏览阅读1.1k次。qq邮箱的设置方法确定手机网络没有问题的詻网络正常就是 邮箱设置问题了在 设置>通用里,里面有个描述文件,点击进去,移走就好,清空已安装的描述文件就好了。 当时这问题也困扰了我很久。。。 希望能帮到你在电脑上打开自己邮箱,在设置里把POP3/SMTP/IMAP都打开,然后再设置手机邮箱、qq邮箱以下是IPHONE设置方法:配置iPhone1、在QQ邮箱中启用IMAP服务;2、点击iPhone..._为什么苹果4无法取得邮件
文章浏览阅读4.3k次。From:http://hi.baidu.com/mweb/blog/item/c75d6a89281708be0f244487.html 准备工作:VS2008原版光盘VS2008SP1补丁VS90SP1-KB957507-v2-CHS-x86.exe (中文智能提示补丁)VS90SP1-KB958502-x86.exe (jquery智能提示补丁)Orca MSI修改工具额外需要的文件_vs2008安装光盘
文章浏览阅读2.1w次。解答:这款软件支持自定义符号和模板。你可以使用软件的符号和模板编辑器来创建和编辑自定义符号和模板。在这款软件中,选择“符号”或“模板”选项,然后点击编辑按钮,即可打开符号或模板编辑器。在编辑器中,你可以绘制自定义符号或设计自定义模板,然后保存并应用到你的公式中。_mathtype7.6产品密钥