`
qiemengdao
  • 浏览: 276503 次
  • 性别: Icon_minigender_1
  • 来自: 武汉
社区版块
存档分类
最新评论

二分查找之谜题

 
阅读更多

一、前言

二分查找本身是个简单的算法,但是正是因为其简单,更容易写错。甚至于在二分查找算法刚出现的时候,也是存在bug的(溢出的bug),这个bug直到几十年后才修复(见《编程珠玑》)。本文打算对二分查找算法进行总结,并对由二分查找引申出来的问题进行分析和汇总。若有错误,请不吝赐教。

二、二分查找是这样的

相信大家都知道二分查找的基本算法,如下所示,这就是二分查找算法:
int bisearch(int a[], int n, int t)  //数组a有序,长度为n, 待查找的值为t
{
    int l = 0, u = n - 1;
    while (l <= u) {
        int m = l + (u - l) / 2; //同(l+u)/ 2,这里是为了溢出
        if (t > a[m])
            l = m + 1;
        else if (t < a[m])
            u = m - 1;
        else
            return m;
    }
    return -(l+1);
}
算法的思想就是:从数组中间开始,每次排除一半的数据,时间复杂度为O(lgN)。这依赖于数组有序这个性质。如果t存在数组中,则返回t在数组的位置;否则,不存在则返回-(l+1)。
这里需要解释下为什么t不存在数组中时不是返回-1而要返回-(l+1)。首先我们可以观察l的值,如果查找不成功,则l的值恰好是t应该在数组中插入的位置。
举个例子,假定有序数组a={1, 3, 4, 7, 8}, 那么如果t=0,则显然t不在数组中,则二分查找算法最终会使得l=0 > u=-1 退出循环;如果t=9,则t也不在数组中,则最后l=5 > u=4退出循环。如果t=5,则最后l=3 > u=2退出循环。因此在一些算法中,比如DHT(一致性哈希)中,就需要这个返回值来使得新加入的节点可以插入到合适的位置中,在求最长递增子序列的NlgN算法中,也用到了这一点,参见博文最长递增子序列算法
还有一个小点就是之所以返回-(l+1)而不是直接返回-l是因为l可能为0,如果直接返回-l就无法判断是正常返回位置0还是查找不成功返回的0。

三、二分查找数字第一次出现的位置

现在考虑一个稍微复杂点的问题,如果有序数组中有重复数字,比如数组a={1, 2, 3, 3, 5, 7, 8},需要在其中找出3第一次出现的位置。这里3第一次出现位置为2。这个问题在《编程珠玑》第九章有很好的分析,这里就直接用了。算法的精髓在于循环不变式的巧妙设计,代码如下:
int bsearch_first(int a[], int n, int t)
{
    int l = -1, u = n;
    while (l + 1 != u) {
        /*循环不变式a[l]<t<=a[u] && l<u*/
        int m = l + (u - l) / 2; //同(l+u)/ 2
        if (t > a[m])
            l = m;
        else
            u = m;
    }
    /*assert: l+1=u && a[l]<t<=a[u]*/
    int p = u;
    if (p>=n || a[p]!=t)
        p = -1;
    return p;
}
算法分析:设定两个不存在的元素a[-1]和a[n],使得a[-1] < t <= a[n],但是我们并不会去访问者两个元素,因为(l+u)/2 > l=-1, (l+u)/2 < u=n。循环不变式为l<u && t>a[l] && t<=a[u] 。循环退出时必然有l+1=u, 而且a[l] < t <= a[u]。循环退出后u的值为t可能出现的位置,其范围为[0, n],如果t在数组中,则第一个出现的位置p=u,如果不在,则设置p=-1返回。该算法的效率虽然解决了更为复杂的问题,但是其效率比初始版本的二分查找还要高,因为它在每次循环中只需要比较一次,前一程序则通常需要比较两次。
举个例子:对于数组a={1, 2, 3, 3, 5, 7, 8},我们如果查找t=3,则可以得到p=u=2,如果查找t=4,a[3]<t<=a[4], 所以p=u=4,判断a[4] != t,所以设置p=-1.一种例外情况是u>=n, 比如t=9,则u=7,此时也是设置p=-1.
特别注意的是,l=-1,u=n这两个值不能写成l=0,u=n-1。虽然这两个值不会访问到,但是如果改成后面的那样,就会导致二分查找失败,那样就访问不到第一个数字。如在a={1,2,3,4,5}中查找1,如果初始设置l=0,u=n-1,则会导致查找失败。

扩展
如果要查找数字在数组中最后出现的位置呢?其实这跟上述算法是类似的,稍微改一下上面的算法就可以了,代码如下:
int bsearch_last(int a[], int n, int t)
{
    int l = -1, u = n;
    while (l + 1 != u) {
        /*循环不变式, a[l] <= t < a[u]*/
        int m = l + (u - l) / 2;
        if (t >= a[m])
            l = m;
        else
            u = m;
    }
    /*assert: l+1 = u && a[l] <= t < a[u]*/
    int p = l;
    if (p<=-1 || a[p]!=t)
        p = -1;
    return p;
}

四、旋转数组元素查找问题

题目:把一个有序数组最开始的若干个元素搬到数组的末尾,我们称之为数组的旋转。例如数组{3, 4, 5, 1, 2}为{1, 2, 3, 4, 5}的一个旋转。现在给出旋转后的数组和一个数,旋转了多少位不知道,要求给出一个算法,算出给出的数在该数组中的下标,如果没有找到这个数,则返回-1。要求查找次数不能超过n。

分析:由题目可以知道,旋转后的数组虽然整体无序了,但是其前后两部分是部分有序的。由此还是可以使用二分查找来解决该问题的。

解法一::2次二分查找。首先确定数组分割点,也就是说分割点两边的数组都有序。比如例子中的数组以位置2分割,前面部分{3,4,5}有序,后半部分{1,2}有序。然后对这两部分分别使用二分查找即可。代码如下:

int split(int a[], int n)
{
    for (int i=0; i<n-1; i++) {
        if (a[i+1] < a[i])
            return i;
    }
    return -1;
}

int bsearch_rotate(int a[], int n, int t)
{
    int p = split(a, n); //找到分割位置
    if (p == -1)
        return bsearch_first(a, n, t); //如果原数组有序,则直接二分查找即可
    else {
        int left = bsearch_first(a, p+1, t); //查找左半部分
        if (left == -1) {  //左半部分没有找到,则查找右半部分
            int right = bsearch_first(a+p+1, n-p-1, t); //查找右半部分
            if (right != -1) return right+p+1;  //返回位置,注意要加上p+1
            return -1;
        }
        return left; //左半部分找到,则直接返回
    }
}

解法二:1次二分查找。

二分查找算法有两个关键点:1)数组有序;2)根据当前区间的中间元素与x的大小关系,确定下次二分查找在前半段区间还是后半段区间进行。

仔细分析该问题,可以发现,每次根据low和high求出mid后,mid左边([low, mid])和右边([mid, high])至少一个是有序的。

a[mid]分别与a[left]和a[right]比较,确定哪一段是有序的。

如果左边是有序的,若x<a[mid]且x>a[left], 则right=mid-1;其他情况,left =mid+1;

如果右边是有序的,若x> a[mid] 且x<a[right] 则left=mid+1;其他情况,right =mid-1;

代码如下:

int bsearch_rotate(int a[], int n, int t)
{
    int low = 0, high = n-1;
    while (low <= high) {
        int mid = low + (high-low) / 2;
        if (t == a[mid])
            return mid;
        if (a[mid] >= a[low]) { //数组左半有序
            if (t >= a[low] && t < a[mid])
                high = mid - 1;
            else
                low = mid + 1;
        } else {       //数组右半段有序
            if (t > a[mid] && t <= a[high])
                low = mid + 1;
            else
                high = mid - 1;
        }   
    }   
    return -1; 
}

参考资料

旋转数组的二分查找
编程珠玑第二版第九章

分享到:
评论

相关推荐

    算法谜题_suanfa_算法谜题_

    4. **排序与查找**:快速排序、归并排序、二分查找等经典算法常常在谜题中扮演重要角色。 5. **动态规划**:用于解决具有重叠子问题和最优子结构的问题,如背包问题、最长公共子序列等。 6. **贪心算法**:通过每...

    算法谜题_高清中英文版

    2. **搜索算法**:包括深度优先搜索(DFS)、广度优先搜索(BFS)、二分查找和回溯法等,这些算法在解决路径寻找、查找特定元素或避免死循环等问题时非常有效。 3. **图论**:图论是算法中的一大分支,书中可能涵盖最小...

    算法谜题.pdf

    - **搜索算法**:如二分查找、深度优先搜索等,用于查找特定的数据。 - **图算法**:包括最短路径算法(Dijkstra算法)、最小生成树算法(Prim算法)等,用于处理图形数据。 - **动态规划**:一种用于求解最优化问题...

    算法谜题 英文版本

    #### 二、书籍结构 - **前言**:包含关于本书的简介和指导。 - **致谢**:感谢在编写过程中提供帮助的人士。 - **谜题列表**:列出了所有收录的谜题。 - **教程谜题**:用于引导读者进入算法谜题的世界。 - **主...

    算法谜题(完整版)

    - **二分查找**:只适用于已经排序的数据集,通过不断缩小搜索范围来提高查找效率。 #### 3. 图算法 图算法处理的是图结构数据。常见的图算法包括最短路径算法(如Dijkstra算法)、最小生成树算法(如Prim算法)等...

    算法谜题【pdf】

    2. **搜索算法**:二分查找、深度优先搜索(DFS)和广度优先搜索(BFS)等,这些算法在解决寻找目标值或遍历结构问题时至关重要。 3. **图论**:可能涉及最短路径算法,如Dijkstra算法和Floyd-Warshall算法,以及...

    Scratch作品:二维二分打乱

    在这个“二维二分打乱”作品中,我们看到的是一个结合了算法和游戏的项目,使用了Scratch编程语言来实现。下面将详细解释其中涉及的知识点。 首先,我们要理解“二分打乱”算法。在计算机科学中,二分法通常用于...

    Scratch教案完整版

    52 第五十二课 "二分查找法"则引导孩子们学习高级的算法,理解如何在有序列表中高效地搜索目标值,提高问题解决能力。 50 第五十课 "斐波那契数列"将介绍数学中的一个重要序列,通过编程实现斐波那契数列的生成,...

    编程的乐趣——用Python解算法谜题.zip

    - **搜索算法**:包括线性搜索、二分查找等,对于有序数据,二分查找具有较高的效率。 - **图算法**:如深度优先搜索(DFS)和广度优先搜索(BFS)在解决复杂问题时非常有用。 - **动态规划**:通过构建状态转移...

    算法谜题 中文

    - **二分查找**:适用于有序数组,每次都将查找区间减半,直到找到目标值或者确定目标值不存在于数组中。 #### 图算法 - **迪杰斯特拉算法(Dijkstra's Algorithm)**:用于寻找图中两点之间的最短路径。该算法保证...

    算法谜题[完整版]

    - **二分查找**:适用于有序数组的搜索,每次将搜索区间减半,直至找到目标元素或搜索区间为空。 - **广度优先搜索(BFS)**:从根节点开始,逐层遍历所有节点,适用于寻找最短路径等问题。 - **深度优先搜索(DFS)...

    数据结构以及经典的算法合集

    查找算法有顺序查找、二分查找、哈希查找等,其中二分查找适用于有序数组,效率高。图算法有深度优先搜索(DFS)和广度优先搜索(BFS),以及最短路径算法如Dijkstra算法和Floyd-Warshall算法。 此外,动态规划是...

    puzzles:谜题

    5. **算法**: 谜题通常涉及排序(快速排序、归并排序、冒泡排序等)、搜索(二分查找、深度优先搜索、广度优先搜索等)以及其他算法,如动态规划、贪心算法、回溯法等。 6. **多线程**: Java内置了强大的多线程支持...

    数据结构资料

    查找算法包括顺序查找、二分查找和哈希查找,其中二分查找适用于有序数组,哈希查找则通过键值映射实现快速查找。排序算法有冒泡排序、选择排序、插入排序、快速排序、归并排序、堆排序等,它们各有优劣,适用于不同...

    算法设计与分析基础( Anany Levitin第3版)课后答案

    二分搜索在有序数组中具有高效的查找性能,而哈希表则提供了一种快速查找和插入的方法,但需要处理哈希冲突问题。 3. **图算法**:包括深度优先搜索(DFS)、广度优先搜索(BFS)、最小生成树(如Prim和Kruskal算法)、...

    小学数学数学故事且看诸葛亮如何妙算

    小学数学中的这个故事展示了诸葛亮如何巧妙地利用二分查找法来推理并确定一个人心里默选的数字。二分查找法是一种在有序数组中查找特定元素的搜索算法,它通过不断缩小搜索范围,以最少的比较次数找到目标值。在这个...

    java数据结构和算法解析

    二分查找适用于有序数组,而哈希查找则利用哈希表实现快速查找。 8. **图和树**:图数据结构用于表示对象之间的关系,如邻接矩阵和邻接表。树数据结构模拟了层级关系,如二叉树、平衡树(AVL树、红黑树)和堆树。 ...

    数据结构课件(ppt版)

    查找算法包括顺序查找、二分查找、哈希查找等,其中哈希表能实现近乎常数时间的查找效率。 此外,我们还会学习到动态规划、贪心算法和回溯法等算法设计策略,它们在解决实际问题中具有广泛应用。例如,动态规划常...

    数据结构与算法C++

    二分查找适用于已排序的数组,哈希查找通过散列函数实现快速查找。 8. **图论算法**:Dijkstra算法用于找出图中两点间的最短路径,而Floyd-Warshall算法可以找到所有点对之间的最短路径。 9. **动态规划**:这是一...

Global site tag (gtag.js) - Google Analytics