二分查找
第一部分
原理
与有序数组相关的问题,比如确定某个数是否在其中,采用二分查找可以达到log(n)的复杂度。原理虽然简单,但是要真正编码实现,而且无bug往往还不是那么容易。本人就实践过多次,每次写完后都要调试好几次才能去掉很多bug。但是在面试过程中,面试官看的就是你平时编码多不多,肯定不会让你有那么多机会调试,你最多可以心里调试一遍,这样往往就留下很多bug,一跑就可能出错。
以下是我对bug出现最多的原因的总结
(start、mid、end分别表示头、中点、尾端的游标或指针)
- 在while循环中出现死循环,原因是mid更新start,(start < end) true, 再次mid更新start,…,如此往复。
- 最终返回值游标采用mid,导致结果错误。
- 原因和解决办法
- 对1, 是因为最后出现了start = end - 1,即两者相邻的情形,此时mid = (start + end) / 2, 在int中,mid = start 之后,如果在if语句中出现 start = mid这样的更新游标语句,那么自然就会陷入死循环。因此我们要避免前面的游标直接由mid更新,即用start = mid + 1,此时将等于的情况挪给end更新来完成:end = mid, 因为这种情况绝不会导致死循环。
- 对2, 是因为最后一次start = end后,将跳出while循环,而促使这个条件成立的,可能是由于start = mid + 1, 也就是说,最终值应该是更新后的 (start + end) / 2, 由于跳出了循环,将得不到更新后的值,而是前一步的mid值。所以最终的输出游标可以采用end,start应该也可以。
e.g. 下面是在升序数组arr中,查找value是否在其中的一个例子,其中,start = mid + 1 使死循环不再出现。
public boolean ascendingFind(int[] arr, int start, int end, int value){
if (start > end) {
return false;
}
int mid = 0;
while(start < end){
mid = (start + end) >> 1;
if(arr[mid] < value){
start = mid + 1;
}else{
end = mid;
}
}
if (arr[end] == value) {
return true;
}
return false;
}
第二部分
- 典型应用场景程序分析
经过上述总结后,本以为可以得心应手的,却发现在紧张状态下仍然对逻辑分析感觉很混乱,唯一有一点的好处是,我知道了很可能导致bug的点在start上面,还有对相邻的start和end进行分析。
1. 给定数组a[](升序), 数m,求出m在a中最左边的位置/最右边的位置
最左边的位置,应该用a[mid] < m时,start = mid+1,为什么我们这么选而不用 a[mid] > m, end = mid - 1呢?因为根据第一部分的原理,我们为了避免最后start和mid交替赋值的死循环问题,我们想尽量在更新start时不要用start = mid。但有些情况下我们避免不了start = mid这样的情况,我们需要另外考虑。
这样我们就有:
// 输出等于m的最左边的下标,如果m不在a中,则输出大于m的第一个下标
public int leftMostEqual(int[] a, int m){
int start = 0;
int end = a.length - 1;
int mid;
while(start < end){
mid = start + (end - start) / 2;
if (a[mid] < m) {
start = mid + 1;
}else {
end = mid;
}
}
return end;
那最后的返回值取start、end还是mid呢?分析可以发现start = end,mid可能还没有更新,所以应该选择end。另外,当m不在a中时,上面的代码输出大于m的第一个下标。其实这里就对应了下面一个问题:
找出int[] a中大于等于m的最左边的下标
的解, 实现上完全一样,接口为:
public int leftMostGreatEqual(int[] a, int m)
接下来的问题为:找出m在a中的最右边的位置,
和上面问题的区别为,start = mid+1在这里用不了了,因为如果a[start] = m,我们不知道当前start是否是最后一个位置,还是第一个位置,所以不能往右移,只能用start = mid,这样为了避免死循环,我们需要改变while(start < end), 如下:
// 输出等于m的最右边的下标,如果m不在a中,则输出小于m的最后一个下标
public int rightMostEqual(int[] a, int m){
int start = 0;
int end = a.length - 1;
int mid;
while(start < end - 1){
mid = start + (end - start) / 2;
if (a[mid] > m) {
end = mid - 1;
}else {
start = mid;
}
}
if(a[end] == m)
return end;
else {
return start;
}
}
我们将终止条件变成了start < end - 1防止start和end发生相邻,当start和end相邻时,循环结束。
那么我们应该取end还是start呢?通过举例可以发现,a[end]肯定不会小于m,所以我们可以从end开始尝试和目标值m比较,再返回正确的值。
另外,返回小于等于某给定数的最右边的下标 的问题,也和这个问题解法完全一样,他们的本质是一样的。
就是这样,欢迎各位提出问题和意见。