二分查找算法总结-程序员宅基地

技术标签: 算法  数据结构  

1 二分查找简介

  二分查找也叫折半查找,是一种常见的查找方法,它将原本是线性时间提升到了对数时间范围,大大缩短了搜索时间。
  二分查找必须具备两个条件,一是数列必须使用顺序存储结构(例如数组),二是数列必须有序。

2 二分查找的原理及实现

  以升序数列为例,比较一个元素与数列中的中间位置的元素的大小,如果比中间位置的元素大,则继续在后半部分的数列中进行二分查找;如果比中间位置的元素小,则在数列的前半部分进行比较;如果相等,则找到了元素的位置。
  每次比较的数列长度都会是之前数列的一半,直到找到相等元素的位置或者最终没有找到要找的元素。

  举一个实例来看一下二分查找的详细过程。
  假设待查找数列为 1、3、5、7、9、11、19,我们要找的元素为 18。首先待查数列下图所示,我们找到中间的元素 7( (1+7)/2=4,第 4 个位置上的元素)。
在这里插入图片描述
  中间元素为 7,我们要找的元素比 7 大,继续在后半部分查找。如下图所示,现在后半部分数列为 9、11、19,继续寻找后半部分的中间元素。
在这里插入图片描述
  中间元素为 11,与 11 比较,比 11 大,则继续在后半部分查找,后半部分只有一个元素 19 了,这时直接与 19 比较,若不相等,则说明在数列中没有找到元素,结束查找。

  递归方法实现二分查找的过程如下所示

public class BinarySearch {
    
    private int[] array;
    /**
     * 递归实现二分查找
     * @param target
     * @return
     */
    public int searchRecursion(int target) {
    
        if (array != null) {
    
            return searchRecursion(target, 0, array.length - 1);
        }
        return -1;
    }
    private int searchRecursion(int target, int start, int end) {
    
        if (start > end) {
    
            return -1;
        }
        int mid = start + (end - start) / 2;
        if (array[mid] == target) {
    
            return mid;
        } else if (target < array[mid]) {
    
            return searchRecursion(target, start, mid - 1);
        } else if (target > array[mid]) {
    
            return searchRecursion(target, mid + 1, end);
        }
    }
}

进行二分查找时注意的技巧:

  1. 计算 mid 时建议写成: mid = left + (right - left) / 2,防止 left+right 造成数据溢出。
  2. 写二分查找时,每一种可能尽量用else if全部写清楚,尽量少用else,可以更好地展示所有细节。

  非递归方式实现二分查找的过程如下所示

public class BinarySearch {
    
    private int[] array;
    /**
     * 初始化数组
     * @param array
     */
    public BinarySearch(int[] array) {
    
        this.array = array;
    }
    /**
     * 二分查找
     * @param target
     * @return
     */
    public int search(int target) {
    
        if (array == null) {
    
            return -1;
        }
        int start = 0;
        int end = array.length - 1;
        while (start <= end) {
      //防止只有1个元素的搜索空间[left,right]--left==right时,如果是<,则搜索空间为空
            int mid = start + (end - start) / 2;
            if (array[mid] == target) {
    
                return mid;
            } else if (target < array[mid]) {
    
                end = mid - 1;  //变成mid-1或mid+1的原因是,mid在上一轮搜索中已经搜索过了,需要从搜索空间中移除
            } else {
    
                start = mid + 1;
            }
        }
        return -1;
    }
}

这里要注意的点:
循环的判定条件是:start <= end,防止只有一个元素,即start == end时,如果用<,会导致此时的搜索空间为空。

3 二分查找的优化

  提出一个思路:为什么用二分查找,而不是三分之一、四分之一查找?
  举一个实际的例子:我们在查字典的时候,如果要查以a开头的单词,则你会怎么翻字典?肯定是从最前面开始翻;如果要查以 z 开头的单词,则应该会从最后开始翻。显而易见,你不会采用二分查找的方式去查这个单词在哪,因为这样你会很累。
  同样,假设数据的范围是 1~10000,让你找 10,你会怎么做?简单来说,我觉得直接用顺序查找都比二分查找更快,因为数列是升序的,用顺序查找比二分查找的比较次数少。

  综上考虑,我们可以优化一下二分查找,并不一定要从正中间开始分,而是尽量找到一个更接近我们要找的那个数字的地方,这样能够减少很多查找次数。
  之前我们都是根据长度去找到这个中间位置,现在是根据 key 所在的序列范围区间去找到这个位置。要查找的位置 P = low + (key-a[low]) / (a[high]-a[low]) × (high-low),这是有点复杂,但是仔细看一下,这种计算方式其实就是为了找 key 所在的相对位置,让 key 的值更接近划分的位置,从而减少比较次数。
  这种对二分查找的优化叫作插值查找,插值查找对于数列比较大并且比较均匀的数列来说,性能会好很多;但是如果数列极不均匀,则插值查找未必会比二分查找的性能好。

4 二分查找的特点和性能分析

  二分查找的平均查找长度为 ((n+1)log2(n+1))/n-1,有的书上写的是 log2(n+1)-1,或者是 log2n,具体计算比较麻烦,这里就不讨论了。
  二分查找有个很重要的特点,就是不会查找数列的全部元素,而查找的数据量其实正好符合元素的对数,正常情况下每次查找的元素都在一半一半地减少。所以二分查找的时间复杂度为 O(log2n) 。当然,最好的情况是只查找一次就能找到,但最坏和一般情况下的确比顺序查找好了很多。

5 二分查找的适用场景

  二分查找要求数列本身有序,所以在选择的时候需要确认数列是否本身有序,如果无序,则还需要进行排序,确认这样的代价是否符合实际需求。其实我们在获取一个列表的很多时候,可以直接使用数据库针对某个字段进行排序,在程序中需要找出某个值的元素时,就很适合使用二分查找了。
  二分查找适合元素稍微多一些的数列,如果元素只有十几或者几十个,则其实可以直接使用顺序查找(当然,也有人在顺序查找外面用了一个或几个大循环,执行这几层大循环需要计算机执行百万、千万遍,没有考虑到机器的性能)。
  一般对于一个有序列表,如果只需要对其进行一次排序,之后不再变化或者很少变化,则每次进行二分查找的效率就会很高;但是如果在一个有序列表中频繁地插入、删除数据,那么维护这个有序列表的代价就比较高昂。

6 二分查找的缺陷

  比如有序数组 nums = [1,2,2,2,3],target = 2,此算法返回的索引是 2。但如果想得到 target 的左侧边界,即索引 1,或者想得到 target 的右侧边界,即索引 3,此时普通的二分查找算法是无法处理的。
  这样的需求很常见。你也许会说,找到一个 target 索引,然后向左或向右线性搜索不行吗?可以,但是不好,因为这样难以保证二分查找对数级的时间复杂度了。为满足上述需求,需要对二分查找进行扩展,即寻找左侧和右侧边界的二分查找。

7 二分查找的扩展

  寻找左侧边界的二分查找

int left_bound(int[] nums, int target) {
    
    if (nums.length == 0) return -1;
    int left = 0;
    int right = nums.length; // 注意

    while (left < right) {
     // 注意
        int mid = left + (right - left) / 2;
        if (nums[mid] == target) {
    
            right = mid;
        } else if (nums[mid] < target) {
    
            left = mid + 1;
        } else if (nums[mid] > target) {
    
            right = mid; // 注意
        }
    }
    return left;
}

为什么这个算法能够查找到左侧边界

关键在于对 nums[mid] == target 这种情况的处理:
if(nums[mid] == target) right = mid;
可见,找到 target 时不会立即返回,而是缩小「搜索区间」的上界 right,在区间 [left, mid) 中继续搜索,即不断向左收缩,达到锁定左侧边界的目的。

  寻找右侧边界的二分查找
  寻找右侧边界和寻找左侧边界的代码差不多,只有两处不同,已标注:

int right_bound(int[] nums, int target) {
    
    if (nums.length == 0) return -1;
    int left = 0, right = nums.length;

    while (left < right) {
    
        int mid = (left + right) / 2;
        if (nums[mid] == target) {
    
            left = mid + 1; // 1 注意
        } else if (nums[mid] < target) {
    
            left = mid + 1;
        } else if (nums[mid] > target) {
    
            right = mid;
        }
    }
    return left - 1; // 2 注意
}

8 总结

  二分查找的思想很简单,但细节处需要十分留意:循环结束的判断条件、搜索空间是左闭右开还是其他方式等等,需要仔细确认自己设置的边界条件。

版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接:https://blog.csdn.net/Summer_night_star/article/details/123012386

智能推荐

c# 调用c++ lib静态库_c#调用lib-程序员宅基地

文章浏览阅读2w次,点赞7次,收藏51次。四个步骤1.创建C++ Win32项目动态库dll 2.在Win32项目动态库中添加 外部依赖项 lib头文件和lib库3.导出C接口4.c#调用c++动态库开始你的表演...①创建一个空白的解决方案,在解决方案中添加 Visual C++ , Win32 项目空白解决方案的创建:添加Visual C++ , Win32 项目这......_c#调用lib

deepin/ubuntu安装苹方字体-程序员宅基地

文章浏览阅读4.6k次。苹方字体是苹果系统上的黑体,挺好看的。注重颜值的网站都会使用,例如知乎:font-family: -apple-system, BlinkMacSystemFont, Helvetica Neue, PingFang SC, Microsoft YaHei, Source Han Sans SC, Noto Sans CJK SC, W..._ubuntu pingfang

html表单常见操作汇总_html表单的处理程序有那些-程序员宅基地

文章浏览阅读159次。表单表单概述表单标签表单域按钮控件demo表单标签表单标签基本语法结构<form action="处理数据程序的url地址“ method=”get|post“ name="表单名称”></form><!--action,当提交表单时,向何处发送表单中的数据,地址可以是相对地址也可以是绝对地址--><!--method将表单中的数据传送给服务器处理,get方式直接显示在url地址中,数据可以被缓存,且长度有限制;而post方式数据隐藏传输,_html表单的处理程序有那些

PHP设置谷歌验证器(Google Authenticator)实现操作二步验证_php otp 验证器-程序员宅基地

文章浏览阅读1.2k次。使用说明:开启Google的登陆二步验证(即Google Authenticator服务)后用户登陆时需要输入额外由手机客户端生成的一次性密码。实现Google Authenticator功能需要服务器端和客户端的支持。服务器端负责密钥的生成、验证一次性密码是否正确。客户端记录密钥后生成一次性密码。下载谷歌验证类库文件放到项目合适位置(我这边放在项目Vender下面)https://github.com/PHPGangsta/GoogleAuthenticatorPHP代码示例://引入谷_php otp 验证器

【Python】matplotlib.plot画图横坐标混乱及间隔处理_matplotlib更改横轴间距-程序员宅基地

文章浏览阅读4.3k次,点赞5次,收藏11次。matplotlib.plot画图横坐标混乱及间隔处理_matplotlib更改横轴间距

docker — 容器存储_docker 保存容器-程序员宅基地

文章浏览阅读2.2k次。①Storage driver 处理各镜像层及容器层的处理细节,实现了多层数据的堆叠,为用户 提供了多层数据合并后的统一视图②所有 Storage driver 都使用可堆叠图像层和写时复制(CoW)策略③docker info 命令可查看当系统上的 storage driver主要用于测试目的,不建议用于生成环境。_docker 保存容器

随便推点

网络拓扑结构_网络拓扑csdn-程序员宅基地

文章浏览阅读834次,点赞27次,收藏13次。网络拓扑结构是指计算机网络中各组件(如计算机、服务器、打印机、路由器、交换机等设备)及其连接线路在物理布局或逻辑构型上的排列形式。这种布局不仅描述了设备间的实际物理连接方式,也决定了数据在网络中流动的路径和方式。不同的网络拓扑结构影响着网络的性能、可靠性、可扩展性及管理维护的难易程度。_网络拓扑csdn

JS重写Date函数,兼容IOS系统_date.prototype 将所有 ios-程序员宅基地

文章浏览阅读1.8k次,点赞5次,收藏8次。IOS系统Date的坑要创建一个指定时间的new Date对象时,通常的做法是:new Date("2020-09-21 11:11:00")这行代码在 PC 端和安卓端都是正常的,而在 iOS 端则会提示 Invalid Date 无效日期。在IOS年月日中间的横岗许换成斜杠,也就是new Date("2020/09/21 11:11:00")通常为了兼容IOS的这个坑,需要做一些额外的特殊处理,笔者在开发的时候经常会忘了兼容IOS系统。所以就想试着重写Date函数,一劳永逸,避免每次ne_date.prototype 将所有 ios

如何将EXCEL表导入plsql数据库中-程序员宅基地

文章浏览阅读5.3k次。方法一:用PLSQL Developer工具。 1 在PLSQL Developer的sql window里输入select * from test for update; 2 按F8执行 3 打开锁, 再按一下加号. 鼠标点到第一列的列头,使全列成选中状态,然后粘贴,最后commit提交即可。(前提..._excel导入pl/sql

Git常用命令速查手册-程序员宅基地

文章浏览阅读83次。Git常用命令速查手册1、初始化仓库git init2、将文件添加到仓库git add 文件名 # 将工作区的某个文件添加到暂存区 git add -u # 添加所有被tracked文件中被修改或删除的文件信息到暂存区,不处理untracked的文件git add -A # 添加所有被tracked文件中被修改或删除的文件信息到暂存区,包括untracked的文件...

分享119个ASP.NET源码总有一个是你想要的_千博二手车源码v2023 build 1120-程序员宅基地

文章浏览阅读202次。分享119个ASP.NET源码总有一个是你想要的_千博二手车源码v2023 build 1120

【C++缺省函数】 空类默认产生的6个类成员函数_空类默认产生哪些类成员函数-程序员宅基地

文章浏览阅读1.8k次。版权声明:转载请注明出处 http://blog.csdn.net/irean_lau。目录(?)[+]1、缺省构造函数。2、缺省拷贝构造函数。3、 缺省析构函数。4、缺省赋值运算符。5、缺省取址运算符。6、 缺省取址运算符 const。[cpp] view plain copy_空类默认产生哪些类成员函数

推荐文章

热门文章

相关标签