Algo: Binary search

    二分查找的基本写法:java

#include <vector>
#include <iostream>

int binarySearch(std::vector<int> coll, int key)
{
    int l = 0;
    int r = (int)coll.size() - 1;

    while (l <= r)
    {
        int m = l + (r - l) / 2;
 
        if (key == coll[m])
        {
            return m;
        }

        if (key > coll[m])
        {
            l = m + 1;
        }
        else
        {
            r = m - 1;
        }
    }

    return -1;
}
int main() { int arr[] = { 2, 3, 4, 10, 40 }; std::vector<int> coll(arr, arr + sizeof(arr) / sizeof(arr[0])); int key = 10; int index = binarySearch(coll, key); if (-1 == index) { std::cout << "Element is not present in array" << std::endl; } else { std::cout << "Element is present at index " << index << std::endl; } return 0; }

 

    历史上,Knuth在其<<Sorting and Searching>>一书的第6.2.1节指出:尽管第一个二分搜索算法于1946年就出现,然而第一个彻底正确的二分搜索算法直到1962年才出现。ios

    而不经仔细斟酌而写出的一个二分查找常常遭遇off by one或者无限循环的错误。下面将讨论二分查找的理论基础,实现应用,及如何采用何种技术保证写出一个正确的二分程序,让咱们免于思考麻烦的边界及结束判断问题。算法

    在C++的STL中有以下函数 lower_bound、upper_bound、binary_search、equal_range,这些函数就是咱们要考虑如何实现的那些。经过实现这些函数,能够检查你是否真的掌握了二分查找。数组

    理论基础:
    当咱们碰到一个问题,须要判断它是否能够采用二分查找来解决。对于最通常的数的查找问题,这点很容易判断,然而对于某些好比能够采用二分+贪心组合,二分解方程,即某些具备单调性的函数问题,也是能够利用二分解决的,然而有时它们表现的不那么显然。网络

    考虑一个定义在有序集合S上的断言,搜索空间内包含了问题的候选解。在本文中,一个断言其实是一个返回布尔值的二值函数。这个断言能够用来验证一个候选解是不是所定义的问题合法的候选解。函数

    咱们把下面的一条定理称之为Main Theorem: Binary search can be used if and only if for all x in S, p(x) implies p(y) for all y > x. 实际上经过这个属性,咱们能够将搜索空间减半,也就是说若是咱们的问题的解应用这样的一个验证函数,验证函数的值能够知足上述条件,这样这个问题就能够用二分查找的方法来找到那个合适的解,好比最左侧的那个合法解。以上定理还有一个等价的说法 !p(x) implies !p(y) for all y < x 。这个定理很容易证实,这里省略证实。测试

    实际上若是把这样的一个p函数应用于整个序列,咱们能够获得以下的一个序列
    fasle false false ......true true....
    若是用01表示,实际上就是以下这样的一个序列0 0 0 0......1 1 1 1.......
    而全部的二分查找问题实际均可以转化为这样的一个01序列中第一个1的查找问题,实际上咱们就为二分查找找到了一个统一的模型。就像排序网络中利用的01定理,若是能够对全部的01序列排序,则能够为全部的序列排序。实际上二分查找也能够用来解决true true....fasle false false ......即1 1 1 1...... 0 0 0 0.....序列的查找问题。固然实际若是咱们把p的定义变反,这个序列就变成了上面的那个,也就是能够转化为上面的模型。spa

    这样咱们就把全部问题转化为求0011模式序列中第一个1出现的位置。固然实际中的问题,也多是求1100模式序列最后一个1的位置。同时要注意对应这两种状况下的实现有些许不一样,而这个不一样对于程序的正确性是很关键的。翻译

    下面的例子对这两种状况都有涉及,通常来讲具备最大化要求的某些问题,它们的断言函数每每具备1100模式,好比poj3258 River Hopscotch;而具备最小化要求的某些问题,它们的断言函数每每具备0011模式,好比poj3273 Monthly Expense。指针

    而对于数key的查找,咱们能够利用以下一个断言使它成为上述模式。好比x是否大于等于key,这样对于一个上升序列来讲它的断言函数值成为以下模式:0 0 0 0......1 1 1 1.......,而寻找最左边的key(相似stl中的lower_bound,则就是上述模型中寻找最左边的1.固然问题是寻找最后出现的那个key(相似stl中的upper_bound),只须要把断言修改为:x是否小于等于key,就变成了1 1 1 1...... 0 0 0 0.....序列的查找问题。

    可见这样的查找问题,变成了如何寻找上述序列中的最左或最右的1的问题。

    相似的一个单调函数的解的问题,只要设立一个断言:函数值是否大于等于0?也变成了如上的序列,若是是单调上升的,则变成了0011模式,反之则是1100模式。实际上当函数的自变量取值为实数时,这样的一个序列实际上变成了一种无穷数列的形式,也就是1111.....0000中间是无穷的,01的边界是无限小的。这样寻找最右侧的1,通常是寻找一个近似者,也就是采用对实数域上的二分(下面的源代码4),而用fabs(begin-end)来控制精度,肯定是否中止迭代。好比poj 3122就是在具备1111.....0000模式的无穷序列中查找那个最右侧的1,对应的自变量值。

    基本例题

    poj 3233 3497 2104 2413 3273 3258 1905 3122

    注:

    poj1905 实际上解一个超越方程 L"sinx -Lx=0,能够利用源码4,二分解方程

    poj3258 寻找最大的可行距离,其实是111000序列中寻找最右侧的1,能够参考源码3

    poj3273 寻找最小的可行值,其实是000111序列中寻找最左侧的1,能够参考源码2

 

    总结

    一、首先寻找进行二分查找的依据,即符合main 理论的一个断言:0 0 0 ........111.......

    二、肯定二分的上下界,尽可能的让上下界松弛,防止漏掉合理的范围,肯定上界,也能够倍增法

    三、观察肯定该问题属于0011仍是1100模式的查找

    四、写程序注意两个不变性的保持

    五、注意验证程序能够处理01这种两个序列的用例,不会出错

    六、注意mid = begin+(end-begin)/2,用mid=(begin+end)/2是有溢出危险的。实际上早期的java的jdk里的二分搜索就有这样的bug,后来java大师Joshua Bloch发现,才改正的。

 

    对二分查找进行分类:取整方式:向下取整 向上取整 (共2种)区间开闭:闭区间 左闭右开区间 左开右闭区间 开区间 (共4种)问题类型:对于不降低序列a,求最小的i,使得a[i] = key对于不降低序列a,求最大的i,使得a[i] = key对于不降低序列a,求最小的i,使得a[i] > key对于不降低序列a,求最大的i,使得a[i] < key对于不上升序列a,求最小的i,使得a[i] = key对于不上升序列a,求最大的i,使得a[i] = key对于不上升序列a,求最小的i,使得a[i] < key对于不上升序列a,求最大的i,使得a[i] > key(共8种)综上所述,二分查找共有2*4*8=64种写法。

    重要的是要会写一种对的。

    首先有几个数字要注意

    一、中位数有两个:
    下位中位数:lowerMedian = (length - 2) / 2;
    上位中位数:upperMedian = length / 2;
    经常使用的是下位中位数,通用的写法以下,语言int常常自动向下取整,

    median = (length - 1) / 2;
    指针的区间固然能够开区间,也能够闭区间,也能够半开半闭。但老老实实两头取闭区间老是不会错。上面的中位数,转换成两头闭区间 [low,high] 就变成下面这样:

    median = low + (high - low) / 2;

    二、不要图快用加法,会溢出,
    median = ( low + high ) / 2;     // OVERFLOW

    三、另一个关键点是“终结条件”

    不要以 low == high 作终结条件,会被跳过的。

 

if (low == high)
{
    return (nums[low] >= target)? low : ++low;
}

    不相信在 [1, 5] 里找 0 试试?

   

    正确的终结条件是:

    low > high
    也就是搜索空间为空。

   

    知足终结条件之后,返回值彻底不须要纠结,直接返回低位 low。

    由于回过头去放慢镜头,二分查找的过程就是一个 维护 low 的过程:
    low从0起始。只在中位数遇到肯定小于目标数时才前进,而且永不后退。low一直在朝着第一个目标数的位置在逼近。知道最终到达。
    至于高位 high,就放心大胆地缩小目标数组的空间吧。

   

    因此最后的代码很是简单,

#include <vector>
#include <iostream>

int binarySearch(std::vector<int> coll, int key)
{
    int low = 0;
    int high = (int)coll.size() - 1;

    while (low <= high)
    {
        int mid = low + (high - low) / 2;
        if (key > coll[mid])
        {
            low = mid + 1;
        }
        else if (key < coll[mid])
        {
            high = mid - 1;
        }
        else
        {
            return mid;
        }
    }
    
    return low;
}
 
int main()
{
    int arr[] = { 2, 3, 4, 10, 40 };
    std::vector<int> coll(arr, arr + sizeof(arr) / sizeof(arr[0]));
    
    int key = 10;
    int index = binarySearch(coll, key);

    if (-1 == index)
    {
        std::cout << "Element is not present in array" << std::endl;
    }
    else
    {
        std::cout << "Element is present at index " << index << std::endl;
    }

    return 0;
}

 

    递归版也同样简单,

#include <vector>
#include <iostream>

int binarySearchRecur(std::vector<int> coll, int key, int low, int high)
{
    if (low > high)
    {
        return low;
    }

    int mid = low + (high - low) / 2;
    if (key > coll[mid])
    {
        return binarySearchRecur(coll, key, mid + 1, high);
    }
    else if (key < coll[mid])
    {
        return binarySearchRecur(coll, key, low, mid - 1);
    }
    else
    {
        return mid;
    }
}

int main()
{
    int arr[] = { 2, 3, 4, 10, 40 };
    std::vector<int> coll(arr, arr + sizeof(arr) / sizeof(arr[0]));
    int size = (int)coll.size();

    int key = 10;
    int index = binarySearchRecur(coll, key, 0, size);

    if (-1 == index)
    {
        std::cout << "Element is not present in array" << std::endl;
    }
    else
    {
        std::cout << "Element is present at index " << index << std::endl;
    }

    return 0;
}

 

    但上面的代码能正常工做,有一个前提条件:元素空间没有重复值。

    推广到有重复元素的空间,二分查找问题就变成:
    寻找元素第一次出现的位置。
    也能够变相理解成另外一个问题,对应C++的 lower_bound() 函数,寻找第一个大于等于目标值的元素位置。

    但只要掌握了上面说的二分查找的心法,代码反而更简单:

#include <vector>
#include <iostream>

int binarySearch(std::vector<int> coll, int key)
{
    int low = 0;
    int high = (int)coll.size() - 1;

    while (low <= high)
    {
        int mid = low + (high - low) / 2;
        if (key > coll[mid])
        {
            low = mid + 1;
        }
        else
        {
            high = mid - 1;
        }
    }
    
    return low;
}
 
int main()
{
    int arr[] = { 2, 3, 4, 10, 40 };
    std::vector<int> coll(arr, arr + sizeof(arr) / sizeof(arr[0]));
    
    int key = 10;
    int index = binarySearch(coll, key);

    if (-1 == index)
    {
        std::cout << "Element is not present in array" << std::endl;
    }
    else
    {
        std::cout << "Element is present at index " << index << std::endl;
    }

    return 0;
}

 

    翻译成递归版也是同样:

#include <vector>
#include <iostream>

int binarySearchRecur(std::vector<int> coll, int key, int low, int high)
{
    if (low > high)
    {
        return low;
    }

    int mid = low + (high - low) / 2;
    if (key > coll[mid])
    {
        return binarySearchRecur(coll, key, mid + 1, high);
    }
    else
    {
        return binarySearchRecur(coll, key, low, mid - 1);
    }
}

int main()
{
    int arr[] = { 2, 3, 4, 10, 40 };
    std::vector<int> coll(arr, arr + sizeof(arr) / sizeof(arr[0]));
    int size = (int)coll.size();

    int key = 10;
    int index = binarySearchRecur(coll, key, 0, size);

    if (-1 == index)
    {
        std::cout << "Element is not present in array" << std::endl;
    }
    else
    {
        std::cout << "Element is present at index " << index << std::endl;
    }

    return 0;
}

 

    以上代码均经过leetcode测试。标准银弹。天天早起写一遍,锻炼肌肉。

    最后想说,不要怕二分查找难写,边界状况复杂。实际状况是,你以为烦躁,大牛也曾经由于这些烦躁过。一些臭名昭著的问题下面,常常是各类大牛的评论(恶心,变态,F***,等等)。并且这并不考验什么逻辑能力,只是仔细的推演罢了。拿个笔出来写一写,算一算不丢人。不少问题完全搞清楚之后,常常就是豁然开朗,而后之后妥妥触类旁通。以上。

 

参考:

https://www.zhihu.com/question/36132386
https://en.wikipedia.org/wiki/Binary_search_algorithm
https://www.geeksforgeeks.org/binary-search/
https://www.topcoder.com/community/data-science/data-science-tutorials/binary-search/

相关文章
相关标签/搜索