我们在一个集合中查询是否存在某一个元素时,经常都是通过索引一个一个的去遍历,可能集合中数据量小看不出明显的性能耗损。下面摘用了《算法图解》的案例,以便于更好的理解二分算法。
Bob要为NASA编写一个查找算法,这个算法在火箭即将登陆月球前开
始执行,帮助计算着陆地点。
这个示例表明,两种算法的运行时间呈现不同的增速。Bob需要做出决
定,是使用简单查找还是二分查找。使用的算法必须快速而准确。一方
面,二分查找的速度更快。Bob必须在10秒钟内找出着陆地点,否则火
箭将偏离方向。另一方面,简单查找算法编写起来更容易,因此出现
bug的可能性更小。Bob可不希望引导火箭着陆的代码中有bug!为确保
万无一失,Bob决定计算两种算法在列表包含100个元素的情况下需要的
时间。
假设检查一个元素需要1毫秒。使用简单查找时,Bob必须检查100个元
素,因此需要100毫秒才能查找完毕。而使用二分查找时,只需检查7个
元素(log 2 100大约为7),因此需要7毫秒就能查找完毕。然而,实际
要查找的列表可能包含10亿个元素,在这种情况下,简单查找需要多长
时间呢?二分查找又需要多长时间呢?
Bob使用包含10亿个元素的列表运行二分查找,运行时间为30毫秒
(log 2 1 000 000 000大约为30)。他心里想,二分查找的速度大约为简
单查找的15倍,因为列表包含100个元素时,简单查找需要100毫秒,而
二分查找需要7毫秒。因此,列表包含10亿个元素时,简单查找需要30
× 15 = 450毫秒,完全符合在10秒内查找完毕的要求。Bob决定使用简单
查找。这是正确的选择吗?
不是。实际上,Bob错了,而且错得离谱。列表包含10亿个元素时,简
单查找需要10亿毫秒,相当于11天!为什么会这样呢?因为二分查找和
简单查找的运行时间的增速不同。
也就是说,随着元素数量的增加,二分查找需要的额外时间并不多,而
简单查找需要的额外时间却很多。因此,随着列表的增长,二分查找的
速度比简单查找快得多。Bob以为二分查找速度为简单查找的15倍,这
不对:列表包含10亿个元素时,为3300万倍。有鉴于此,仅知道算法需
要多长时间才能运行完毕还不够,还需知道运行时间如何随列表增长而
增加。
把问题转换成我们开发中的场景,那就是现在有一个3亿元素的数组array(集合也行,但必须是有序的!)
用简单查找去查询
public class Test {
public static void main(String[] args) {
//准备3亿数据
long startTime=System.currentTimeMillis(); //获取开始时间
int[] array = preparationData();
long endTime=System.currentTimeMillis(); //获取结束时间
System.out.println("准备3亿数据程序运行时间: "+(endTime-startTime)+"ms");
startTime=System.currentTimeMillis(); //获取开始时间
System.out.println(simpleAlgorithm(array, 0));
endTime=System.currentTimeMillis(); //获取结束时间
System.out.println("简单查找程序运行时间: "+(endTime-startTime)+"ms");
startTime=System.currentTimeMillis(); //获取开始时间
System.out.println(commonBinarySearch(array, 0));
endTime=System.currentTimeMillis(); //获取结束时间
System.out.println("二分查找程序运行时间: "+(endTime-startTime)+"ms");
}
//准备3亿数据
public static int[] preparationData(){
int array[] = new int[300000000];
for(int i = 0 ; i<300000000; i++){
array[i] = i;
}
System.out.println("添加数据次数:"+(array.length));
return array;
}
//简单查找
public static Integer simpleAlgorithm(int a[], int x) {
Integer f = null ;
int length = a.length;
int i;
for (i = 0; i < length ; i++) {
if (x == a[i]) {
f = i;
break;
}
}
if(f == null){
f = -1;
}
System.out.println("简单找数据次数:"+(f+1));
return f;
}
//二分查找
public static int commonBinarySearch(int[] arr,int key){
int low = 0;
int high = arr.length - 1;
int middle = 0; //定义middle
int i = 0;
if(key < arr[low] || key > arr[high] || low > high){
return -1;
}
while(low <= high){
middle = (low + high) / 2;
if(arr[middle] > key){
//比关键字大则关键字在左区域
high = middle - 1;
}else if(arr[middle] < key){
//比关键字小则关键字在右区域
low = middle + 1;
}else{
System.out.println("简单找数据次数:"+(i+1));
return middle;
}
i++;
}
return -1; //最后仍然没有找到,则返回-1
}
}
输出结果:
添加数据次数:300000000
准备3亿数据程序运行时间: 741ms
简单找数据次数:1
0
简单查找程序运行时间: 0ms
简单找数据次数:28
0
二分查找程序运行时间: 0ms
当我们查询0的时候,其实也就索引为0的位置的数据,简单查找只查了1次就返回了结果,二分查找却查了28次,
运行时间几乎都是0ms
很明显简单查找效率要高很多,如果我们现在查询1000000000,也就是索引位置为99999999的数据呢?
输出结果:
添加数据次数:300000000
准备3亿数据程序运行时间: 549ms
简单找数据次数:100000000
99999999
简单查找程序运行时间: 52ms
简单找数据次数:25
99999999
二分查找程序运行时间: 0ms
简单查找只查了100000000次就返回了结果,二分查找却查了25次,
运行时间上简单查找52ms,二分查找还是0ms
是不是感觉二分查找的优势体现出来了?我们继续查询299999999,也就是最后一个索引位置的数字
输出结果
添加数据次数:300000000
准备3亿数据程序运行时间: 627ms
简单找数据次数:300000000
299999999
简单查找程序运行时间: 128ms
简单找数据次数:29
299999999
二分查找程序运行时间: 0ms
总结:
简单查找:随着索引位置不同的数据查询效率也不同(查找顺序从左往右依次查找)
二分查找:每查询一次会排除一半的数据,直到最后只剩下一个数据