二分法查找

二分查找针对的是一个有序的数据集合,查找思想类似分治算法。每次通过跟区间的中间元素对比,将待查找的区间缩小为之前的一半,直到找到要查找的元素,或者区间被缩小为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

posted on 2020-12-07 15:35  紅葉  阅读(219)  评论(0编辑  收藏  举报