二分查找法

减半再减半,减到剩一个——如何利用有序数列的特性

     记得之前有档综艺节目“幸运52”,主持人让选手规定时间内猜商品的价格,针对选手的报价主持人提示“高了”或者“低了”,直到价格猜对就可以获得此商品。比如猜电视机的价格,选手出价8000,主持人回答“高了“,选手再出价4000,回答”低了“……6000——高了——5000——高了——4500,对了!乍一看这完全是碰运气,但其中却蕴含了“二分查找”的思想。

    我们把这个场景转化为一个编程问题:如何从一组有序的整数(物品的价格区间)里找到某个数(正确的价格)的位置。
例如数组[2 3 6 8 9 10 15],从中找到10的位置。我们当然可以从头找到尾,相等就是找到了,没有相等的就是没找到。毕竟就这几个数,即使挨个查找也很快。但是数量较多时,1万个,100万个……就需要考虑效率问题。“从头找到尾”是遍历的方法,忽略了数组“有序的”这个特性。当我们从数组中随便取一个数a(排除头尾元素),它的左侧必定小于它,右侧必定大于它(这点类似之前介绍的“快速排序”中的基准值的概念)。如果选的是中间的元素a,去除恰好等于要查询的数的情况,如果比a大,则从右侧区间再取一半,再取这个区间的中间值来比较大小,以此类推,每次减半,直到比较完最后一个中间值。这就是“二分查找法”,也叫做“折半查找法”(之前介绍“归并排序”时,对数组的拆分就是不断从中间拆,有效降低了栈的深度)。我们引用一下当时归并算法提到的“对数”概念,它本身就是不断除2。

 

可以看到折半是很“恐怖”的,即使一亿个元素,仅需27次就能分割完毕。

我们看个具体例子,代码如下:

 1 import java.util.Arrays;
 2 
 3 public class BinarySearch {
 4     public static void main(String[] args) {
 5         int[] numbers = {12, 13, 16, 18, 19, 20, 25, 26, 29, 32};
 6         int target = 25;
 7         System.out.println(Arrays.toString(numbers) + " " + target);
 8         System.out.println(target + " position " + search(numbers, target));
 9     }
10 
11     private static int search(int[] numbers, int target) {
12         int start = 0;
13         int end = numbers.length - 1;
14         int mid = 0;
15         int splitTimes = 0;
16         while (true) {
17             splitTimes++;
18             mid = start + (end - start) / 2;
19             if (numbers[mid] == target) {
20                 System.out.println(splitTimes + " " + numbers[mid] + "=" + target + " start " + start + " end " + end + " mid " + mid);
21                 System.out.println("splitTimes " + splitTimes);
22                 return mid;
23             } else if (target < numbers[mid]) {
24                 System.out.println(splitTimes + " " + numbers[mid] + ">" + target + " start " + start + " end " + end + " mid " + mid);
25                 end = mid - 1;
26             } else {
27                 System.out.println(splitTimes + " " + numbers[mid] + "<" + target + " start " + start + " end " + end + " mid " + mid);
28                 start = mid + 1;
29             }
30             if (start > end) {
31                 System.out.println("splitTimes " + splitTimes);
32                 return -1;
33             }
34         }
35     }
36 }

 输出说明

[12, 13, 16, 18, 19, 20, 25, 26, 29, 32] 25
1 19<25 start 0 end 9 mid 4 #第1次取中间的19,小于25,到右侧区间查找
2 26>25 start 5 end 9 mid 7 #第2次取中间的26,大于25,到左侧区间查找
3 20<25 start 5 end 6 mid 5 #第3次取中间的20,小于25,到右侧区间查找
4 25=25 start 6 end 6 mid 6 #第4次取中间的25,找到了
splitTimes 4
25 position 6

 查找示例图

再说下算法中的注意事项

一 循环结束条件是start大于end,等于的时候相当于分割后只剩一个元素,还没参与比较,必然不能退出。大于说明是左侧位置超过了右侧位置,说明已经处理完毕,这时才能退出循环。

二 end的初始值,应该为数组长度减一,如果不减一,相当于结束位置多了一位,当要查询的数大于数组最大元素时,必然会到达这个多出的位置,从而导致数组越界。

三 start end的更新,计算完中间位置mid,比较大小后就可以跳过这个位置了,因此下一个start或end的所属区间必定不包含这个mid。如果查询的数大于mid对应值,则下一个区间必然位于右侧,开始位置start就等于mid+1,如果小于mid对应的值,则下一个区间必然位于左侧,结束位置end就等于mid-1。

只要明白了这三个注意事项,就不会有问题。

 

扩展思考

1 mid的计算方法 start+(end-start)/2、end-(end-start)/2、(start+end)/2 这三种有啥区别?

2 如果有序数组存在重复元素,有没有什么影响?如何找到重复元素的第一个位置或者最后一个位置?

3 查找IP归属地能否用到二分查找?

 

posted @ 2022-06-24 13:39  binary220615  阅读(83)  评论(0编辑  收藏  举报