• 二分法查找


    二分查找针对的是一个有序的数据集合,查找思想类似分治算法。每次通过跟区间的中间元素对比,将待查找的区间缩小为之前的一半,直到找到要查找的元素,或者区间被缩小为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));
  • 相关阅读:
    继承映射
    一对多,多对一,自关联的配置
    Spring 配置自动扫描spring bean配置
    Dao 处理
    2019暑假集训 括号匹配
    2019暑假集训 BLO
    2019暑假集训 Intervals
    2019暑假集训 平板涂色
    2019暑假集训 走廊泼水节
    0002-五层小山
  • 原文地址:https://www.cnblogs.com/yuyujuan/p/14097023.html
Copyright © 2020-2023  润新知