二分查找
我们老师说过二分查找算法的一个例子,如有一天小明同学去图书馆借书。在看了一圈之后发现了好多喜欢的书,一口气借了20本书
在图书馆的门口,小明对图书进行消磁登记,但是在这个过程中,其中有一本书小明忘记了消磁,所以在过安检门的时候触发了警报
小明这个时候很慌,不得不将这20本书再重新一本一本的过一遍案件,用来判断到底是哪一本书没有消磁
这个时候图书馆看门的大爷看不下去了,直接接过小明手里的20本书,并进行了如下操作:
大爷首先将20本书分为两部分,每一部分10本书,并将两部分图书分别过一遍安检,
然后将触发警报的那一部分图书再次进行划分,同样划分成为两部分,每部分5本书,重复上面的操作
然后将触发警报的5本书再次划分成两部分,一部分2本书,另一部分3本书,然后再次将上述的两部分分别过安检
最终确定,两本书的那一部分中,有一本是没有消磁的,然后大爷直接将这两本书分别过安检,最终成功找到了那一本没有消磁的书.……
小明被这一套神操作彻底惊呆了,因为他不知道,大爷使用的正是二分搜索的思想
二分算法的定义
二分搜索算法的核心思想,就是对待搜索序列每一次查找后,如果没有找到查找目标,那么就对待搜索序列进行折半,在其中的一般序列中继续查找,并根据查询结果继续搜索,知道找到目标元素在待搜索序列中的下标进行返回,或者确定待搜索序列中不存在目标元素为止
二分查找的前提条件
计算机编程当中实现的二分搜索算法是具有一定前提条件的,二分搜索的前提条件就是:待搜搜序列的所有元素必须是有序的
二分查找的实现原理
二分搜索的实现原理和实现步骤可以描述如下(以升序有序序列为例):
步骤1:首先根据搜索序列的起点下标和重点下标,计算出搜索序列的中间下标
步骤2:使用中间下标对应的序列元素与目标元素进行比较
步骤3∶如果目标元素小于中间下标元素,则说明目标元素有可能存在于中间下标元素的左侧,将搜索序列结束下标重新标定义为中间下标-1
如果目标元素大于中间下标元素,则说明目标元素有可能存在于中间下标元素的右侧,将搜索序列开始下标重新定义为中间下标+1
步骤4∶重复上述步骤1-3,直到中间下标元素等于目标元素时,说明目标元素在搜索序列中已找到,则返回此时的中间下标,就是目标元素在搜索序列中的下标
步骤5∶如果在重复上述步骤的时候,出现搜索序列起点下标大于搜索序列终点下标的情况,则可以确定目标元素在搜索序列中不存在,此时返回-1
根据上面的步骤,就能够实现一个二分搜索算法。
从上面的步骤描述中可以得知:我们在重新确定搜索序列的起点和终点的时候,是根据目标元素和中间下标元素的之间的大小关系确定的
并且我们根据这个大小关系能够推测出目标元素有可能存在于当前中间下标元素的左边还是右边。
这一推测的前提条件,就是整个搜索序列是有序的
下面我们通过一组图片来观察和分析二分搜索算法的整体实现流程,并且从中找到代码层面的实现规律
二分查找条件
通过上面的两张图示,我们可以发现在二分搜索中具有如下规律:
规律1:在搜索过程中,我们始终都是使用序列中间下标对应的元素和目标元素比较(array[m]和target比较),而在查找的的情况下,我们返回的是此时的中间下标(返回m的值)
规律2:中间下标的计算公式如下:中间下标m = (搜索序列起点下标s + 搜索序列终点下标e) / 2,并向下取整
规律3:如果目标元素取值小于中间下标元素取值,那么在下一次查询的时候,将搜索序列终点下标改变为中间下标-1
规律4:如果目标元素取值大于中间下标元素取值,那么在下一次查询的时候,将搜索序列起点下标改变为中间下标+1
规律5:关于二分搜索的结束条件,一句话总结就是:活要见人 || 死要见尸
活要见人:指的是如果在搜索过程中出现中间下标元素等于目标元素的情况(array[m]== target),则说明目标元素已找到,此时返回中间下标即可
死要见尸:指的是如果在搜索过程中出现搜索序列的起点下标或者终点下标在经过变化之后,出现起点下标在终点下标右侧的情况(s > e),则表示确定序列中不存在目标元素,返回-1结束
所以综上所述,二分搜索运行下去的条件就是:活不见人 && 死不见尸(array[m] !=target && s <= e)
规律6:值得注意的是,在某些极端条件下,搜索序列的起点下标、终点下标和中间下标是会重合的,那么此时依然是有可能找到目标元素的,所以这种情况下,还是要继续搜索一次的
算法代码(非递归)
public int binarySearch(int[] array,int target){
//定义搜索序列的起点下标、终点下标和中间下标
int s = 0; //起点下标
int e = array.length - 1; //终点下标
int m ; //中间下标
//使用do-while循环实现一个二分搜索算法,为什么使用do-while循环呢?
do {
//只要还能够走到这个位置,说明又进行了一次二分搜索
//此时搜索序列的起点下标和终点下标发生了变化,中间下标需要重新计算
m = (s + e) / 2;
//通过比较array[m]的取值和target的大小关系
if(array[m] < target){
s = m + 1;//起点等于中间值+1
}
if(array[m] > target){
e = m - 1;//终点等于中间值-1
}
if(array[m] == target){
return m;
}
}while(target != array[m] && s <= e);
return -1;//如果返回-1就是没找到啊
}
算法代码(递归)
可见不加递归的代码是多么的繁琐
public int binarySearchRecursion(int[] array,int target,int start,int end){
int m = (start + end) / 2;
if(array[m] < target){
return binarySearchRecursion(array,target,m+1,end);
}
if(array[m] > target){
return binarySearchRecursion(array,target,start,m-1);
}
if(array[m] == target){
return m;
}
if(start > end){
return -1;
}
return -1;
}
我知道这个递归理解起来可能有些困难,但是各位同学只要在纸上画一下就可以大致有轮廓了,不能光靠脑子想哦
二分算法题
来源于力扣704二分查找,找个简单题给大家助个兴
给定一个 n 个元素有序的(升序)整型数组 nums 和一个目标值 target ,写一个函数搜索 nums 中的 target,如果目标值存在返回下标,否则返回 -1。
来源:力扣(LeetCode)
链接:https://leetcode-cn.com/problems/binary-search
也是成功的
之后上第二题
你是产品经理,目前正在带领一个团队开发新的产品。不幸的是,你的产品的最新版本没有通过质量检测。由于每个版本都是基于之前的版本开发的,所以错误的版本之后的所有版本都是错的。假设你有 n 个版本 [1, 2, …, n],你想找出导致之后所有版本出错的第一个错误的版本。
你可以通过调用 bool isBadVersion(version) 接口来判断版本号 version 是否在单元测试中出错。实现一个函数来查找第一个错误的版本。你应该尽量减少对调用 API 的次数。
输入:n = 5, bad = 4
输出:4
解释:
调用 isBadVersion(3) -> false
调用 isBadVersion(5) -> true
调用 isBadVersion(4) -> true
所以,4 是第一个错误的版本
来源:力扣(LeetCode)
链接:https://leetcode-cn.com/problems/first-bad-version
这段代码是看的官方的,我是辣鸡,我自己写的总有些问题,下面说说对于这个官方代码我的理解
public class Solution extends VersionControl {
public int firstBadVersion(int n) {
int start = 1;
int end = n;
while(start < end){
int mid =start + (end - start) / 2;
if(isBadVersion(mid)){
end = mid;
}else{
start = mid + 1;
}
}
return start;
}
}
首先,我们要知道这个二分是肯定会搜索到值的,所以也不用返回-1什么的,我们可以分析一下,如果找到的这个值是错误的,我们就把右边界缩到mid,去【start,mid】中寻找第一个发生错误的版本号在哪里,如果这个值是正确的话,【start,mid】这个区域是没有错误版本号的。所以start= mid + 1;我们去【mid+1,end】看它在哪里,最终start会与end重合,所以返回start或者end都是可以的。
第三题
给定一个排序数组和一个目标值,在数组中找到目标值,并返回其索引。如果目标值不存在于数组中,返回它将会被按顺序插入的位置。
来源:力扣(LeetCode)
链接: https://leetcode-cn.com/problems/search-insert-position
public int searchInsert(int[] nums, int target) {
int i = 0;
while(i < nums.length && target >= nums[i] ){
if(target == nums[i]){
return i;
}else{
i++;
}
}
return i;
}
解释:如果没有遍历完数组且目标值大于等于遍历的数组值时,遍历继续,否则的话直接返回下标值,简单的来说,我们只需要找到大于目标值或者与目标值相等的下标进行返回即可。