资讯专栏INFORMATION COLUMN

二分查找

jerryloveemily / 2465人阅读

摘要:正文二分查找关于二分查找法二分查找法主要是解决在一堆数中找出指定的数这类问题。用二分查找法找寻上界与精确查找不同之处在于,精确查找分成三类大于,小于,等于目标数。

由一道题目引出的:

题目描述

给定一个有序的数组,查找某个数是否在数组中,请编程实现。

分析与解法

一看到数组本身已经有序,我想你可能反应出了要用二分查找,毕竟二分查找的适用条件就是有序的。那什么是二分查找呢?

二分查找可以解决(预排序数组的查找)问题:只要数组中包含T(即要查找的值),那么通过不断缩小包含T的范围,最终就可以找到它。其算法流程如下:

一开始,范围覆盖整个数组。
将数组的中间项与T进行比较,如果T比数组的中间项要小,则到数组的前半部分继续查找,反之,则到数组的后半部分继续查找。
如此,每次查找可以排除一半元素,范围缩小一半。就这样反复比较,反复缩小范围,最终就会在数组中找到T,或者确定原以为T所在的范围实际为空。
对于包含N个元素的表,整个查找过程大约要经过log(2)N次比较。

此时,可能有不少读者心里嘀咕,不就二分查找么,太简单了。

然《编程珠玑》的作者Jon Bentley曾在贝尔实验室做过一个实验,即给一些专业的程序员几个小时的时间,用任何一种语言编写二分查找程序(写出高级伪代码也可以),结果参与编写的一百多人中:90%的程序员写的程序中有bug(我并不认为没有bug的代码就正确)。

也就是说:在足够的时间内,只有大约10%的专业程序员可以把这个小程序写对。但写不对这个小程序的还不止这些人:而且高德纳在《计算机程序设计的艺术 第3卷 排序和查找》第6.2.1节的“历史与参考文献”部分指出,虽然早在1946年就有人将二分查找的方法公诸于世,但直到1962年才有人写出没有bug的二分查找程序。

你能正确无误的写出二分查找代码么?不妨一试,关闭所有网页,窗口,打开记事本,或者编辑器,或者直接在本文评论下,不参考上面我写的或其他任何人的程序,给自己十分钟到N个小时不等的时间,立即编写一个二分查找程序。

正文:二分查找 关于二分查找法

二分查找法主要是解决在“一堆数中找出指定的数”这类问题。

而想要应用二分查找法,这“一堆数”必须有一下特征:(1)存储在数组中 (2) 有序排列
所以如果是用链表存储的,就无法在其上应用二分查找法了。

至于是顺序递增排列还是递减排列,数组中是否存在相同的元素都不要紧。不过一般情况,我们还是希望并假设数组是递增排列,数组中的元素互不相同。

二分查找法的基本实现

二分查找法在算法家族大类中属于“分治法”,分治法基本都可以用递归来实现的,二分查找法的递归JS实现如下:

function bsearch(array,low,high,target)
{
    if (low > high) return -1;
    var mid = Math.floor((low + high)/2);
    if (array[mid]> target){
        return  bsearch(array, low, mid -1, target);
    } else if (array[mid]< target){
        return  bsearch(array, mid+1, high, target);
    }ese{return mid;}
        
}

不过所有的递归都可以自行定义stack来解递归,所以二分查找法也可以不用递归实现,而且它的非递归实现甚至可以不用栈,因为二分的递归其实是尾递归,它不关心递归前的所有信息。

function bsearchWithoutRecursion(array,low,high,target)
{
    while(low <= high)
    {
        var mid = Math.floor((low + high)/2);
        if (array[mid] > target){
            high = mid - 1;
        }else if (array[mid] < target){
            low = mid + 1;
        }else{
            return mid;
        }
    }
    return -1;
}

用二分查找法找寻边界值

之前的都是在数组中找到一个数要与目标相等,如果不存在则返回-1。我们也可以用二分查找法找寻边界值,也就是说在有序数组中找到“正好大于(小于)目标数”的那个数。
用数学的表述方式就是:
在集合中找到一个大于(小于)目标数t的数x,使得集合中的任意数要么大于(小于)等于x,要么小于(大于)等于t。

举例来说:
给予数组和目标数
var array = {2, 3, 5, 7, 11, 13, 17};
var target = 7;
那么上界值应该是11,因为它“刚刚好”大于7;下届值则是5,因为它“刚刚好”小于7。

用二分查找法找寻上界

function BSearchUpperBound(array,low,high,target)
{
    if(low > high || target >= array[high]) return -1;
    
    var mid = (low + high) / 2;
    while (high > low)
    {
        if (array[mid] > target){
            high = mid;
       } else{
            low = mid + 1; 
       }
        mid = (low + high) / 2;
    }

    return mid;
}

与精确查找不同之处在于,精确查找分成三类:大于,小于,等于(目标数)。而界限查找则分成了两类:大于和不大于。
如果当前找到的数大于目标数时,它可能就是我们要找的数,所以需要保留这个索引,也因此if (array[mid] > target)时 high=mid; 而没有减1。

用二分查找法找寻下界

function BSearchLowerBound(array,low,high,target)
{
    if(high < low  || target <= array[low]) return -1;
    
    var mid = (low + high + 1) / 2; //make mid lean to large side
    while (low < high)
    {
        if (array[mid] < target){
            low = mid;
        }else{
            high = mid - 1;
        }
        mid = (low + high + 1) / 2;
    }

    return mid;
}

下届寻找基本与上届相同,需要注意的是在取中间索引时,使用了向上取整。若同之前一样使用向下取整,那么当low == high-1,而array[low] 又小于 target时就会形成死循环。因为low无法往上爬超过high。
这两个实现都是找严格界限,也就是要大于或者小于。如果要找松散界限,也就是找到大于等于或者小于等于的值(即包含自身),只要对代码稍作修改就好了:

去掉判断数组边界的等号:
target >= array[high]改为 target > array[high]
在与中间值的比较中加上等号:
array[mid] > target改为array[mid] >= target

用二分查找法找寻区域

之前我们使用二分查找法时,都是基于数组中的元素各不相同。假如存在重复数据,而数组依然有序,那么我们还是可以用二分查找法判别目标数是否存在。不过,返回的index就只能是随机的重复数据中的某一个。

此时,我们会希望知道有多少个目标数存在。或者说我们希望数组的区域。
结合前面的界限查找,我们只要找到目标数的严格上届和严格下届,那么界限之间(不包括界限)的数据就是目标数的区域了。

//return type: pair
//the fisrt value indicate the begining of range,
//the second value indicate the end of range.
//If target is not find, (-1,-1) will be returned
pair SearchRange(int A[], int n, int target) 
{
    pair r(-1, -1);
    if (n <= 0) return r;
    
    int lower = BSearchLowerBound(A, 0, n-1, target);
    lower = lower + 1; //move to next element
    
    if(A[lower] == target)
        r.first = lower;
    else //target is not in the array
        return r;
    
    int upper = BSearchUpperBound(A, 0, n-1, target);
    upper = upper < 0? (n-1):(upper - 1); //move to previous element
    
    //since in previous search we had check whether the target is
    //in the array or not, we do not need to check it here again
    r.second = upper;
    
    return r;
}

它的时间复杂度是两次二分查找所用时间的和,也就是O(log n) + O(log n),最后还是O(log n)。

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/81925.html

相关文章

  • 数据结构与算法——二分查找

    摘要:所以,二分查找较适用于一次排序,多次查找的数据。面对大量的数据,二分查找方能体现其优势。 1. 二分查找的思想 二分查找是一种使用十分普遍的查找算法,其基本的思路也非常的简单,在一个有序的数据集合中,我们想要查找某个数据,直接取最中间的那个数据,将它和要找的数据进行比较,如果较大,则在更大的那个数值区间继续取中间值查找,反之亦然。 例如我们要在一个有序的集合里[1,3,5,6,7,8,...

    boredream 评论0 收藏0
  • PHP算法之二分查找

    摘要:二分查找的定义二分查找也称折半查找,它是一种效率较高的查找方法。但是,折半查找要求线性表必须采用顺序存储结构,而且表中元素按关键字有序排列。算法的要求从上面的定义我们可以知道,满足该算法的要求必须如下两点必须采用顺序存储结构。 showImg(https://segmentfault.com/img/remote/1460000016466416?w=800&h=191); 二分查找的...

    Soarkey 评论0 收藏0
  • 数据结构与算法——二分查找练习

    摘要:查找最后一个等于给定值的元素这种变形的二分查找和上面的这种情况很类似,还是利用上面的那个数组,我们要查找最后一个等于的元素。 1. 概述 前面说到了二分查找问题,看起来非常的简单,的确,前面的两种实现都不难,代码也很容易写,因为那只是最基础的二分查找问题了。今天来看看几种稍微复杂的二分查找问题: 查找第一个等于给定值的元素 查找最后一个等于给定值的元素 查找第一个大于等于给定值的元素...

    JasinYip 评论0 收藏0
  • 二分查找】| 模拟 20 万数据快速查询 IP 归属地

    摘要:通过两个二分查找的条件继续进行问题的分析,那么问题又来了,二分查找是快速的查找一个数据是否存在一组数据中,而且效率极高,亿查找一个数据只需次查找。二分查找的三点重点循环退出条件注意是而不是。 showImg(https://segmentfault.com/img/remote/1460000018761246);这篇文章主要深入数据结构与算法在解决实际问题怎么运用和分析的,对于 IP...

    The question 评论0 收藏0
  • 数据结构与算法:二分查找

    摘要:为检查长度为的列表,二分查找需要执行次操作。最后需要指出的一点是高水平的读者可研究一下二叉树关于二叉树,戳这里数据结构与算法二叉树算法常见练习在一个二维数组中,每一行都按照从左到右递增的顺序排序,每一列都按照从上到下递增的顺序排序。 常见数据结构 简单数据结构(必须理解和掌握) 有序数据结构:栈、队列、链表。有序数据结构省空间(储存空间小) 无序数据结构:集合、字典、散列表,无序...

    zsirfs 评论0 收藏0

发表评论

0条评论

最新活动
阅读需要支付1元查看
<