44. 盘点那些必问的数据结构算法题之二分查找算法

简介: 44. 盘点那些必问的数据结构算法题之二分查找算法

44. 盘点那些必问的数据结构算法题之二分查找算法


0 概述

二分查找本身是个简单的算法,但是正是因为其简单,更容易写错。甚至于在二分查找算法刚出现的时候,也是存在bug的(溢出的bug),这个bug直到几十年后才修复(见《编程珠玑》)。

本文打算对二分查找算法进行总结,并对由二分查找引申出来的问题进行分析和汇总。若有错误,请指正。

1 二分查找基础

相信大家都知道二分查找的基本算法,如下所示,这就是二分查找算法代码:

/**
 * 基本二分查找算法
 */
int binarySearch(int a[], int n, int 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算法中,也用到了这一点,参见:

http://blog.csdn.net/ssjhust123/article/details/7798737

还有一个小点就是之所以返回-(l+1)而不是直接返回 -l 是因为 l 可能为0,如果直接返回 -l 就无法判断是正常返回位置0还是查找不成功返回的0。

2 查找有序数组中数字第一次出现位置

现在考虑一个稍微复杂点的问题,如果有序数组中有重复数字,比如数组a={1, 2, 3, 3, 5, 7, 8},需要在其中找出3第一次出现的位置。这里3第一次出现位置为2。这个问题在《编程珠玑》第九章有很好的分析,这里就直接用了。

算法的精髓在于循环不变式的巧妙设计,代码如下:

/**
 * 二分查找第一次出现位置
 */
int binarySearchFirst(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=“-1。”" u=“” style=“font-size: inherit;color: inherit;line-height: inherit;”>=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,则会导致查找失败。</t<=a[4],>

扩展

如果要查找数字在数组中最后出现的位置呢?其实这跟上述算法是类似的,稍微改一下上面的算法就可以了,代码如下:

/**
 * 二分查找最后一次出现位置
 */
int binarySearchLast(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;
}

当然还有一种方法可以将查询数字第一次出现和最后一次出现的代码写在一个程序中,只需要对原始的二分查找稍微修改即可,代码如下:

/**
 * 二分查找第一次和最后一次出现位置
 */
int binarySearchFirstAndLast(int a[], int n, int t, int firstFlag)
{
    int l = 0;
    int u = n - 1;
    while(l <= u) {
        int m = l + (u - l) / 2;
        if(a[m] == t) { //找到了,判断是第一次出现还是最后一次出现
            if(firstFlag) { //查询第一次出现的位置
                if(m != 0 && a[m-1] != t)
                    return m;
                else if(m == 0)
                    return 0;
                else
                    u = m - 1;
            } else {   //查询最后一次出现的位置
                if(m != n-1 && a[m+1] != t)
                    return m;
                else if(m == n-1)
                    return n-1;
                else
                    l = m + 1;
            }
        }
        else if(a[m] < t)
            l = m + 1;
        else
            u = m - 1;
    }
    return -1;
}

3 旋转数组元素查找问题

题目

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

分析

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

解1:两次二分查找

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

/**
 * 旋转数组查找-两次二分查找
 */
int binarySearchRotateTwice(int a[], int n, int t)
{
    int p = findRotatePosition(a, n); //找到旋转位置
    if (p == -1)
        return binarySearchFirst(a, n, t); //如果原数组有序,则直接二分查找即可
    int left = binarySearchFirst(a, p+1, t); //查找左半部分
    if (left != -1)
        return left; //左半部分找到,则直接返回
    int right = binarySearchFirst(a+p+1, n-p-1, t); //左半部分没有找到,则查找右半部分
    if (right == -1)
        return -1;
    return right+p+1;  //返回位置,注意要加上p+1
}
/**
 * 查找旋转位置
 */
int findRotatePosition(int a[], int n)
{
    int i;
    for (i = 0; i < n-1; i++) {
        if (a[i+1] < a[i])
            return i;
    }
    return -1;
}

解2:一次二分查找

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

仔细分析该问题,可以发现,每次根据 l 和 u 求出 m 后,m 左边([l, m])和右边([m, u])至少一个是有序的。a[m]分别与a[l]和a[u]比较,确定哪一段是有序的。

如果左边是有序的,若 t<a[m] && t>a[l], 则 u=m-1;其他情况,l =m+1;

如果右边是有序的,若 t> a[m] && t<a[u] 则 l=m+1;其他情况,u =m-1;

代码如下:

/**
 * 旋转数组二分查找-一次二分查找
 */
int binarySearchRotateOnce(int a[], int n, int t)
{
    int l = 0, u = n-1;
    while (l <= u) {
        int m = l + (u-l) / 2;
        if (t == a[m])
            return m;
        if (a[m] >= a[l]) { //数组左半有序
            if (t >= a[l] && t < a[m])
                u = m - 1;
            else
                l = m + 1;
        } else {       //数组右半段有序
            if (t > a[m] && t <= a[u])
                l = m + 1;
            else
                u = m - 1;
        }   
    }   
    return -1; 
}

参考资料

编程珠玑第二版第九章

http://www.jobcoding.com/datastructure-and-algorithm/array/one-sorted-array/rotate_array/

目录
相关文章
|
2月前
|
算法 数据处理 C语言
C语言中的位运算技巧,涵盖基本概念、应用场景、实用技巧及示例代码,并讨论了位运算的性能优势及其与其他数据结构和算法的结合
本文深入解析了C语言中的位运算技巧,涵盖基本概念、应用场景、实用技巧及示例代码,并讨论了位运算的性能优势及其与其他数据结构和算法的结合,旨在帮助读者掌握这一高效的数据处理方法。
68 1
|
2月前
|
机器学习/深度学习 算法 数据挖掘
K-means聚类算法是机器学习中常用的一种聚类方法,通过将数据集划分为K个簇来简化数据结构
K-means聚类算法是机器学习中常用的一种聚类方法,通过将数据集划分为K个簇来简化数据结构。本文介绍了K-means算法的基本原理,包括初始化、数据点分配与簇中心更新等步骤,以及如何在Python中实现该算法,最后讨论了其优缺点及应用场景。
162 4
|
12天前
|
存储 算法 C++
【C++数据结构——查找】二分查找(头歌实践教学平台习题)【合集】
二分查找的基本思想是:每次比较中间元素与目标元素的大小,如果中间元素等于目标元素,则查找成功;顺序表是线性表的一种存储方式,它用一组地址连续的存储单元依次存储线性表中的数据元素,使得逻辑上相邻的元素在物理存储位置上也相邻。第1次比较:查找范围R[0...10],比较元素R[5]:25。第1次比较:查找范围R[0...10],比较元素R[5]:25。第2次比较:查找范围R[0..4],比较元素R[2]:10。第3次比较:查找范围R[3...4],比较元素R[3]:15。,其中是顺序表中元素的个数。
117 66
【C++数据结构——查找】二分查找(头歌实践教学平台习题)【合集】
|
3月前
|
存储 人工智能 算法
数据结构与算法细节篇之最短路径问题:Dijkstra和Floyd算法详细描述,java语言实现。
这篇文章详细介绍了Dijkstra和Floyd算法,这两种算法分别用于解决单源和多源最短路径问题,并且提供了Java语言的实现代码。
112 3
数据结构与算法细节篇之最短路径问题:Dijkstra和Floyd算法详细描述,java语言实现。
|
12天前
|
存储 算法 测试技术
【C++数据结构——树】二叉树的遍历算法(头歌教学实验平台习题) 【合集】
本任务旨在实现二叉树的遍历,包括先序、中序、后序和层次遍历。首先介绍了二叉树的基本概念与结构定义,并通过C++代码示例展示了如何定义二叉树节点及构建二叉树。接着详细讲解了四种遍历方法的递归实现逻辑,以及层次遍历中队列的应用。最后提供了测试用例和预期输出,确保代码正确性。通过这些内容,帮助读者理解并掌握二叉树遍历的核心思想与实现技巧。
37 2
|
28天前
|
存储 运维 监控
探索局域网电脑监控软件:Python算法与数据结构的巧妙结合
在数字化时代,局域网电脑监控软件成为企业管理和IT运维的重要工具,确保数据安全和网络稳定。本文探讨其背后的关键技术——Python中的算法与数据结构,如字典用于高效存储设备信息,以及数据收集、异常检测和聚合算法提升监控效率。通过Python代码示例,展示了如何实现基本监控功能,帮助读者理解其工作原理并激发技术兴趣。
57 20
|
26天前
|
算法 索引
【算法】——二分查找合集
二分查找基础模版和进阶模版,查找元素位置,搜索插入位置,x的平方根,山脉数组的峰顶索引,寻找峰值,点名
|
2月前
|
存储 算法 搜索推荐
Python 中数据结构和算法的关系
数据结构是算法的载体,算法是对数据结构的操作和运用。它们共同构成了计算机程序的核心,对于提高程序的质量和性能具有至关重要的作用
|
2月前
|
数据采集 存储 算法
Python 中的数据结构和算法优化策略
Python中的数据结构和算法如何进行优化?
|
2月前
|
算法
数据结构之路由表查找算法(深度优先搜索和宽度优先搜索)
在网络通信中,路由表用于指导数据包的传输路径。本文介绍了两种常用的路由表查找算法——深度优先算法(DFS)和宽度优先算法(BFS)。DFS使用栈实现,适合路径问题;BFS使用队列,保证找到最短路径。两者均能有效查找路由信息,但适用场景不同,需根据具体需求选择。文中还提供了这两种算法的核心代码及测试结果,验证了算法的有效性。
131 23

热门文章

最新文章