二分法查找
二分查找针对的是一个有序的数据集合,查找思想类似分治算法。每次通过跟区间的中间元素对比,将待查找的区间缩小为之前的一半,直到找到要查找的元素,或者区间被缩小为0。
查找元素在数组中的位置
非递归写法
function bsearch(arr,n,value){ let low=0,high=n-1; while(low<=high){ let mid = parseInt(low + (high-low) /2); if(arr[mid]==value){ return mid; }else if(arr[mid] < value){ low = mid+1; }else{ high = mid -1; } } return -1; } let arr=[1,2,3,4,5,6,7,8,9,10,11]; console.log(bsearch(arr,arr.length,7));
递归写法
function bsearch(arr,n,value){ return bsearchInternally(arr,0,n-1,value) } function bsearchInternally(arr,low,high,value){ if(low > high){ return -1 } let mid = parseInt(low + (high -low) /2); if(arr[mid] == value){ return mid; }else if(arr[mid] < value){ return bsearchInternally(arr,mid+1,high,value) }else{ return bsearchInternally(arr,low,mid-1,value) } } let arr=[1,2,3,4,5,6,7,8,9,10,11]; console.log(bsearch(arr,arr.length,7));
上面的例子就是一个最简单(有序数组中不存在重复元素)的二分查找例子。在这个例子中,我们需要注意一下几点:
1、循环退出条件
在二分查找中,循环退出条件是low<=high,而不是low<high
2、mid的取值
常规去中间值的方法是mid=(low+high)/2,但是如果 low 和 high 比较大的话,两者之和就有可能会溢出。改进的方法是将 mid 的计算方式写成 low+(high-low)/2
3、low和high的更新
low=mid+1,high=mid-1。注意这里的 +1 和 -1,如果直接写成 low=mid 或者 high=mid,就可能会发生死循环。比如,当 high=3,low=3 时,如果 a[3] 不等于 value,就会导致一直循环不退出。
O(logn) 惊人的查找速度
二分查找是一种非常高效的查找算法。
我们假设数据大小是 n,每次查找后数据都会缩小为原来的一半,也就是会除以 2。最坏情况下,直到查找区间被缩小为空,才停止。
可以看出来,这是一个等比数列。其中 n/2k =1 时,k 的值就是总共缩小的次数。而每一次缩小操作只涉及两个数据的大小比较,所以,经过了 k 次区间缩小操作,时间复杂度就是 O(k)。通过 n/2k =1,我们可以求得 k=log2n,所以时间复杂度就是 O(logn)。
因为 logn 是一个非常“恐怖”的数量级,即便 n 非常非常大,对应的 logn 也很小。比如 n 等于2 的 32 次方,这个数很大了吧?大约是 42 亿。也就是说,如果我们在 42 亿个数据中用二分查找一个数据,最多需要比较 32 次。在用大 O 标记法表示时间复杂度的时候,会省略掉常数、系数和低阶。对于常量级时间复杂度的算法来说,O(1) 有可能表示的是一个非常大的常量值,比如 O(1000)、O(10000)。所以,常量级时间复杂度的算法有时候可能还没有 O(logn) 的算法执行效率高。
二分查找变形问题
在上面的例子中,我们假设了杯查找的对象是有序且无重复数据的数组。但在实际情况中,被查找的数据并不一定能如此简单,例如下面这些变形问题,依然建立在有序数组的前提下,当数组中出现重复数据时,又改如何查找呢?
查找第一个值等于给定值的元素
对于上面这个数组,如果我们用前面的代码实现,
首先拿 8 与区间的中间值 a[4] 比较,8 比 6大,于是在下标 5 到 9 之间继续查找。下标 5 和 9 的中间位置是下标 7,a[7] 正好等于 8,所以代码就返回了。尽管 a[7] 也等于 8,但它并不是我们想要找的第一个等于 8 的元素,因为第一个值等于 8 的元素是数组下标为 5 的元素。
1 function bsearch(arr,n,value){ 2 let low=0,high=n-1; 3 while(low<=high){ 4 let mid = parseInt(low + (high-low) /2); 5 if(arr[mid] > value){ 6 high = mid -1; 7 }else if(arr[mid] < value){ 8 low = mid+1; 9 }else{ 10 if((mid==0) || (arr[mid-1] != value)){ 11 return mid 12 }else{ 13 high = mid-1 14 } 15 } 16 } 17 return -1; 18 } 19 let arr=[1,3,4,5,6,8,8,8,11,18]; 20 console.log(bsearch(arr,arr.length,8));
a[mid] 跟要查找的 value 的大小关系有三种情况:大于、小于、等于。对于 a[mid]>value 的情况,我们需要更新 high= mid-1;对于 a[mid]<value 的情况,我们需要更新 low=mid+1。这两点都很好理解。那当 a[mid]=value 的时候应该如何处理呢?如果我们查找的是任意一个值等于给定值的元素,当 a[mid] 等于要查找的值时,a[mid] 就是我们要找的元素。但是,如果我们求解的是第一个值等于给定值的元素,当 a[mid] 等于要查找的值时,我们就需要确认一下这个 a[mid] 是不是第一个值等于给定值的元素。
我们重点看第 10 行代码。如果 mid 等于 0,那这个元素已经是数组的第一个元素,那它肯定是我们要找的;如果 mid 不等于 0,但 a[mid] 的前一个元素 a[mid-1] 不等于 value,那也说明a[mid] 就是我们要找的第一个值等于给定值的元素。如果经过检查之后发现 a[mid] 前面的一个元素 a[mid-1] 也等于 value,那说明此时的 a[mid]肯定不是我们要查找的第一个值等于给定值的元素。那我们就更新 high=mid-1,因为要找的元素肯定出现在 [low, mid-1] 之间。
查找最后一个值等于给定值的元素
1 function bsearch(arr,n,value){ 2 let low=0,high=n-1; 3 while(low<=high){ 4 let mid = parseInt(low + (high-low) /2); 5 if(arr[mid] > value){ 6 high = mid -1; 7 }else if(arr[mid] < value){ 8 low = mid+1; 9 }else{ 10 if((mid==n-1) || (arr[mid+1] != value)){ 11 return mid 12 }else{ 13 low = mid-1 14 } 15 } 16 } 17 return -1; 18 } 19 let arr=[1,3,4,5,6,8,8,8,11,18]; 20 console.log(bsearch(arr,arr.length,8));
这里还是修改第10行代码就可以,如果 a[mid] 这个元素已经是数组中的最后一个元素了,那它肯定是我们要找的;如果 a[mid] 的后一个元素 a[mid+1] 不等于 value,那也说明 a[mid] 就是我们要找的最后一个值等于给定值的元素。
如果我们经过检查之后,发现 a[mid] 后面的一个元素 a[mid+1] 也等于 value,那说明当前的这个 a[mid] 并不是最后一个值等于给定值的元素。我们就更新 low=mid+1,因为要找的元素肯定出现在 [mid+1, high] 之间。
查找第一个大于等于给定值的元素
1 function bsearch(arr,n,value){ 2 let low=0,high=n-1; 3 while(low<=high){ 4 let mid = parseInt(low + (high-low) /2); 5 if(arr[mid] >= value){ 6 if((mid==0) || (arr[mid-1] < value)){ 7 return mid; 8 }else{ 9 high = mid-1 10 } 11 }else{ 12 low = mid +1 13 } 14 } 15 return -1; 16 } 17 let arr=[1,3,4,5,6,8,8,8,11,18]; 18 console.log(bsearch(arr,arr.length,8));
如果 a[mid] 小于要查找的值 value,那要查找的值肯定在 [mid+1, high] 之间,所以,我们更新low=mid+1。
对于 a[mid] 大于等于给定值 value 的情况,我们要先看下这个 a[mid] 是不是我们要找的第一个值大于等于给定值的元素。如果 a[mid] 前面已经没有元素,或者前面一个元素小于要查找的值value,那 a[mid] 就是我们要找的元素。这段逻辑对应的代码是第 6 行。
如果 a[mid-1] 也大于等于要查找的值 value,那说明要查找的元素在 [low, mid-1] 之间,所以,我们将 high 更新为 mid-1。
查找最后一个小于等于给定值的元素
有了上面的基础,最后一个就很简单了:
1 function bsearch(arr,n,value){ 2 let low=0,high=n-1; 3 while(low<=high){ 4 let mid = parseInt(low + (high-low) /2); 5 if(arr[mid] > value){ 6 high = mid - 1; 7 }else{ 8 if((mid == n-1) || (arr[mid+1] > value)){ 9 return mid 10 }else{ 11 low = mid +1 12 } 13 } 14 } 15 return -1; 16 } 17 let arr=[1,3,4,5,6,8,8,8,11,18]; 18 console.log(bsearch(arr,arr.length,8));
相关题目
704
35
34
69
367