有一天小明到图书馆借了 N 本书,出图书馆的时候,警报响了,于是保安把小明拦下,要检查一下哪本书没有登记出借。小明正准备把每一本书在报警器下过一下,以找出引发警报的书,但是保安露出不屑的眼神:你连二分查找都不会吗?于是保安把书分成两堆,让第一堆过一下报警器,报警器响;于是再把这堆书分成两堆…… 最终,检测了 logN 次之后,保安成功的找到了那本引起警报的书,露出了得意和嘲讽的笑容。于是小明背着剩下的书走了。 从此,图书馆丢了 N - 1 本书。
保安怎么知道只有一本书📖没有登记出借,万一全部都没有登记呢?
这个故事其实说出了二分查找需要的条件
用于查找的内容逻辑上来说是需要有序的
查找的数量只能是一个,而不是多个
比如在一个有序的数组并且无重复元素的数组中,例如[1, 2, 3, 4, 5, 6],需要查找3的位置就可以使用二分查找。
在二分查找中,目标元素的查找区间的定义十分重要,不同的区间的定义写法不一样
因为查找的区间是不断迭代的,所以确定查找的范围十分重要,主要就是左右区间的开和闭的问题,开闭不一样,对应的迭代方式也不一样,有以下两种方式:
- 左闭右闭[left, right]
- 左闭右开[left, right)
二分法的思想很简单,因为整个数组是有序的,数组默认是递增的。
首先选择数组中间的数字和需要查找的目标值比较
如果相等最好,就可以直接返回答案了
如果不相等
如果中间的数字大于目标值,则中间数字向右的所有数字都大于目标值,全部排除
如果中间的数字小于目标值,则中间数字向左的所有数字都小于目标值,全部排除
所以二分查找算法可以描述为:
- 前提:有已排序数组 A(假设已经做好)
- 定义左边界 L、右边界 R,确定搜索范围,循环执行二分查找(3、4两步)
- 获取中间索引 M = Floor((L+R) /2)
- 中间索引的值 A[M] 与待搜索的值 T 进行比较
① A[M] == T 表示找到,返回中间索引
② A[M] > T,中间值右侧的其它元素都大于 T,无需比较,中间索引左边去找,M - 1 设置为右边界,重新查找
③ A[M] < T,中间值左侧的其它元素都小于 T,无需比较,中间索引右边去找, M + 1 设置为左边界,重新查找 - 当 L > R 时,表示没有找到,应结束循环
算法实现:
public static int binarySearch(int[] a, int t){
//左边界
int left=0;
//有边界
int right=a.length-1;
//中间索引
int m;
while (right<=left){
m=(left+right)/2;
//如果相等,说明找到了
if (a[m]==t){
return m;
//如果大于t,说明t在左半侧,
}else if(a[m]>t){
right=m-1;
//如果小于t,说明t在右半侧
}else{
left=m+1;
}
}
//如果最终没找到,返回-1
return -1;
}
解决整数溢出问题
当 l 和 r 都较大时,l + r 有可能超过整数范围,造成运算错误,解决方法有两种:
int m = l + (r - l) / 2;
还有一种是:
int m = (l + r) >>> 1;
其它考法
- 有一个有序表为 1,5,8,11,19,22,31,35,40,45,48,49,50 当二分查找值为 48 的结点时,查找成功需要比较的次数
- 使用二分法在序列 1,4,6,7,15,33,39,50,64,78,75,81,89,96 中查找元素 81 时,需要经过( )次比较
- 在拥有128个元素的数组中二分查找一个数,需要比较的次数最多不超过多少次
对于前两个题目,记得一个简要判断口诀:奇数二分取中间,偶数二分取中间靠左。对于后一道题目,需要知道公式:
$$n = log_2N = log{10}N/log{10}2$$
其中 n 为查找次数,N 为元素个数