二分查找---总结
总结二分查找相关的知识及解题应用
最简单基础的二分搜索可以如下实现:
public static int binarySearch(int[] a,int key) {
int i = 0, j = a.length - 1;
while (i <= j) {
int m = (i + j) >>> 1;
if (key < a[m])
j= m - 1;
else if (key > a[m])
i= m + 1;
else
return m; // key found
}
return -1; // key not found.
}
我们不妨看一下Java函数中的二分查找:(int数组举例)
分析:
private static int binarySearch0(int[] a, int fromIndex, int toIndex,
int key) {
int low = fromIndex; //区间开始
int high = toIndex - 1; //区间结束
while (low <= high) {
int mid = (low + high) >>> 1;
int midVal = a[mid];
if (midVal < key) // 要找的key在mid的右边
low = mid + 1; //更新左边界
else if (midVal > key) //在左边
high = mid - 1; //更新右边界
else
return mid; // key found
}
return -(low + 1); // key not found.
}
要注意的是,在取mid的时候,我们利用的是算数右移操作而不是简单是除以二
原因是Java的二进制符号问题,在Java中的数值的二进制的首位表示符号位,
假设int有八位(实际Java的int是8字节32位) 01111000 + 01000000 = 10111000
乍一看没问题,但是转为十进制就能看出有什么问题了:
120 + 64 = -56 -56/2 = -28 这里把运算结果的1当成了符号位,所以没有得到正确结果
而我们利用右移运算即可避免这类错误
10111000 >>>01011100 得到了正确结果92
这里的返回值为什么是-(low + 1)
呢?
我们通过查看代码中的注释就能得到解答:
我们可以知道low
指的是,如果找不到要找的元素key,那么将该key插入到数组中的位置.
那么为什么要将其+1并取负呢? hh 因为我们如果直接返回low那不就是默认找到了吗…
我们这个函数的目的是为了返回是否找到,而不是为了获取找不到而插入的下标!
所以返回一个负数是为了代表未找到.
那有人又要问了,为什么不直接返回-low
呢,你傻啊,万一low是0
呢…
至此,我们知道了Java的二分查找函数的原理
当然,还有另一种改动版的二分查找
public static int binarySearch1(int[] a,int key) {
int i = 0, j = a.length; //改动1:j(右边界)初始化为不存在的a.length(不参与比较)
while (i < j) { //改动2:这里把=去掉
int m = (i + j) >>> 1;
if (key < a[m])
j= m; //改动3:更新右边界为m而不是m-1(因为j所指的元素是虚拟的边界不用再比较了)
else if (key > a[m])
i = m + 1;
else
return m;
}
return -1;
}
这一种改动版只是改变了j
的含义,原来是参与比较的右边界,现在变成了不参与比较的右边界
即二分搜索区间由[i,j]
变为了[i,j)
这里我们的左闭右开区间造成了区间左侧和右侧元素的比较次数不平均的问题
二分查找的平衡版,用来解决该问题
public static int binarySearch1(int[] a,int key) {
int i = 0, j = a.length;
while (i + 1 < j) { //改动
int m = (i + j) >>> 1;
if (key < a[m]){
j = m
}
else{
i = m; //改动
}
}
//将比较这一步骤提取到循环外侧,while循环只用于缩小范围
//(i和j之间只有一个元素a[m]的时候退出循环)
//可以减少while循环中的一层比较
if (a[i] == key) {
return i;
} else {
return -1
}
}
这种平衡版的一个缺点是,如果第一次循环的m处就是要找的key,那么还会继续循环
也就是将基础二分法的最优情况O(1)也变成了O(log(n))
如果数组中存在重复的元素,也可以利用二分查找到key的第一个位置或者最后一个位置
public static int left(int[] a,int key) {
int i = 0, j = a.length - 1, candidate = -1;
while (i <= j) {
int m = (i + j) >>> 1;
if (key < a[m])
j= m - 1;
else if (key > a[m])
i= m + 1;
else{
candidate = m; //m暂时作为候选而不是马上确定
j = m - 1; //更新右边界继续往左边寻找
}
}
return candidate;
}
public static int right(int[] a,int key) {
int i = 0, j = a.length - 1, candidate = -1;
while (i <= j) {
int m = (i + j) >>> 1;
if (key < a[m])
j= m - 1;
else if (key > a[m])
i= m + 1;
else{
candidate = m; //m暂时作为候选而不是马上确定
i = m + 1; //更新左边界继续往右边寻找
}
}
return candidate;
}
优化后:
public static int left(int[] a,int key) {
int i = 0, j = a.length - 1, candidate = -1;
while (i <= j) {
int m = (i + j) >>> 1;
if (key <= a[m])
j= m - 1;
else
i= m + 1;
}
return i;
}
public static int right(int[] a,int key) {
int i = 0, j = a.length - 1, candidate = -1;
while (i <= j) {
int m = (i + j) >>> 1;
if (key <= a[m])
j= m - 1;
else
i= m + 1;
}
return i - 1; //j
}
接下来是几道leetcode的二分查找题目,可以运用上面总结的内容解决