基本介绍

二分搜索(折半搜索)是一种在有序数组中查找某一特定元素的搜索算法。从定义可知,运用二分搜索的前提是数组必须是排好序的。另外,输入并不一定是数组,也有可能是给定一个区间的起始和终止的位置。

他的时间复杂度是 O(lgn),非常高效。

特点

他的缺点要求待查找的数组或者区间是排好序的。

对数组进行动态的删除和插入操作并完成查找,平均复杂度会变为 O(n)。

因此,当输入的数组或者区间是排好序的,同时又不会经常变动,而要求从里面找出一个满足条件的元素的时候,二分搜索就是最好的选择。

解题思路

二分搜索一般化的解题思路如下。

  • 从已经排好序的数组或区间中取出中间位置的元素,判断该元素是否满足要搜索的条件,如果满足,停止搜索,程序结束。

  • 如果正中间的元素不满足条件,则从它两边的区域进行搜索。由于数组是排好序的,可以利用排除法,确定接下来应该从这两个区间中的哪一个去搜索。

  • 通过判断,如果发现真正要找的元素在左半区间的话,就继续在左半区间里进行二分搜索。反之,就在右半区间里进行二分搜索。

二分查找的解题框架

int binarySearch(int[] nums, int target) {int left = 0, right = ...;while(...) {//计算 mid 时需要技巧防止溢出,建议写成: mid = left + (right - left) / 2int mid = (right + left) / 2;if (nums[mid] == target) {...} else if (nums[mid] < target) {left = ...} else if (nums[mid] > target) {right = ...}}return ...;
}

常见解法

递归解法

例题:假设我们要从一个排好序的数组里 {1, 3, 4, 6, 7, 8, 10, 13, 14} 查看一下数字 7 是否在里面,如果在,返回它的下标,否则返回 -1。

递归写法的代码模板如下。

// 二分搜索函数的定义里,除了要指定数组 nums 和目标查找数 target 之外,还要指定查找区间的起点和终点位置,分别用 low 和 high 去表示。
int binarySearch(int[] nums, int target, int low, int high) {// 为了避免无限循环,先判断,如果起点位置大于终点位置,表明这是一个非法的区间,已经尝试了所有的搜索区间还是没能找到结果,返回 -1。 if (low > high) {return -1;}// 取正中间那个数的下标 middle。int middle = low + (high - low) / 2;// 判断一下正中间的那个数是不是要找的目标数 target,是,就返回下标 middle。    if (nums[middle] == target) {return middle;}// 如果发现目标数在左边,就递归地从左半边进行二分搜索。if (target < nums[middle]) {return binarySearch(nums, target, low, middle - 1);} else {return binarySearch(nums, target, middle + 1, high);}//否则从右半边递归地进行二分搜索。
}

注意:

  • 在计算 middle 下标的时候,不能简单地用 (low + hight) / 2,可能会导致溢出。

  • 在取左半边以及右半边的区间时,左半边是 [low, middle - 1],右半边是 [middle + 1, high],这是两个闭区间。因为已经确定了 middle 那个点不是我们要找的,就没有必要再把它加入到左、右半边了。

  • 对于一个长度为奇数的数组,例如:{1, 2, 3, 4, 5},按照 low + (high - low) / 2 来计算,middle 就是正中间的那个位置,对于一个长度为偶数的数组,例如 {1, 2, 3, 4},middle 就是正中间靠左边的一个位置。

时间复杂度是 O(logn)

非递归解法

非递归写法的代码模板如下。

int binarySearch(int[] nums, int target, int low, int high) {// 在 while 循环里,判断搜索的区间范围是否有效while (low <= high) {// 计算正中间的数的下标int middle = low + (high - low) / 2;// 判断正中间的那个数是不是要找的目标数 target。如果是,就返回下标 middleif (nums[middle] == target) {return middle;}// 如果发现目标数在左边,调整搜索区间的终点为 middle - 1;否则,调整搜索区间的起点为 middle + 1if (target < nums[middle]) {high = middle - 1;} else {low = middle + 1;}}// 如果超出了搜索区间,表明无法找到目标数,返回 -1  return -1;
}

为什么 while 循环的条件中是 <=,而不是 < ?

答:因为初始化 high 的赋值是 nums.length - 1,即最后一个元素的索引,而不是 nums.length。

这二者可能出现在不同功能的二分查找中,区别是:前者相当于两端都闭区间 [left, right],后者相当于左闭右开区间 [left, right),因为索引大小为 nums.length 是越界的。

我们这个算法中使用的是 [left, right] 两端都闭的区间。这个区间就是每次进行搜索的区间。

例题分析

找确定的边界

边界分上边界和下边界,有时候也被成为右边界和左边界。确定的边界指边界的数值等于要找的目标数。

例题:LeetCode 第 34 题,在一个排好序的数组中找出某个数第一次出现和最后一次出现的下标位置。

示例:输入的数组是:{5, 7, 7, 8, 8, 10},目标数是 8,那么返回 {3, 4},其中 3 是 8 第一次出现的下标位置,4 是 8 最后一次出现的下标位置。

解题思路

在二分搜索里,把第一次出现的地方叫下边界(lower bound),把最后一次出现的地方叫上边界(upper bound)。

那么成为 8 的下边界的条件应该有两个。

该数必须是 8; 该数的左边一个数必须不是 8: 8 的左边有数,那么该数必须小于 8; 8 的左边没有数,即 8 是数组的第一个数。

而成为 8 的上边界的条件也应该有两个。

该数必须是 8; 该数的右边一个数必须不是 8: 8 的右边有数,那么该数必须大于8; 8 的右边没有数,即 8 是数组的最后一个数。

代码实现

用递归的方法来寻找下边界,代码如下。

int searchLowerBound(int[] nums, int target, int low, int high) {if (low > high) {return -1;}int middle = low + (high - low) / 2;//判断是否是下边界时,先看看 middle 的数是否为 target,并判断该数是否已为数组的第一个数,或者,它左边的一个数是不是已经比它小,如果都满足,即为下边界。if (nums[middle] == target && (middle == 0 || nums[middle - 1] < target)) {return middle;}if (target <= nums[middle]) {return searchLowerBound(nums, target, low, middle - 1);} else {return searchLowerBound(nums, target, middle + 1, high);} //不满足,如果这个数等于 target,那么就得往左边继续查找。
}

查找上边界的代码如下。

int searchUpperBound(int[] nums, int target, int low, int high) {if (low > high) {return -1;}int middle = low + (high - low) / 2;//判断是否是上边界时,先看看 middle 的数是否为 target,并判断该数是否已为数组的最后一个数,或者,它右边的数是不是比它大,如果都满足,即为上边界。    if (nums[middle] == target && (middle == nums.length - 1 || nums[middle + 1] > target)) {return middle;}if (target < nums[middle]) {return searchUpperBound(nums, target, low, middle - 1);} else {return searchUpperBound(nums, target, middle + 1, high);} //不满足时,需判断搜索方向。
}

找模糊的边界

二分搜索可以用来查找一些模糊的边界。模糊的边界指,边界的值并不等于目标的值,而是大于或者小于目标的值。

例题:从数组 {-2, 0, 1, 4, 7, 9, 10} 中找到第一个大于 6 的数。

解题思路

在一个排好序的数组里,判断一个数是不是第一个大于 6 的数,只要它满足如下的条件:

该数要大于 6; 该数有可能是数组里的第一个数,或者它之前的一个数比 6 小。 只要满足了上面的条件就是第一个大于 6 的数。

代码实现

Integer firstGreaterThan(int[] nums, int target, int low, int high) {if (low > high) {return null;}int middle = low + (high - low) / 2;//判断 middle 指向的数是否为第一个比 target 大的数时,须同时满足两个条件:middle 这个数必须大于 target;middle 要么是第一个数,要么它之前的数小于或者等于 target。 if (nums[middle] > target && (middle == 0 || nums[middle - 1] <= target)) {return middle;}if (target < nums[middle]) {return firstGreaterThan(nums, target, low, middle - 1);} else {return firstGreaterThan(nums, target, middle + 1, high);}
}

对于这道题,当不满足条件,而 middle 的数等于 target 的时候怎么办?举例说明,如果要求的是第一个大于 6 的数,而数组中有多个 6 挨在一起,而此时的 middle 指向其中的一个 6,程序必须得在右半边搜索。

旋转过的排序数组

二分搜索也能在经过旋转了的排序数组中进行。

例题:LeetCode 第 33 题,给定一个经过旋转了的排序数组,判断一下某个数是否在里面。

示例:给定数组为 {4, 5, 6, 7, 0, 1, 2},target 等于 0,答案是 4,即 0 所在的位置下标是 4。

解题思路

对于这道题,输入数组不是完整排好序,还能运用二分搜索吗?

由于题目说数字了无重复,举个例子:
1 2 3 4 5 6 7 可以大致分为两类,
第一类 2 3 4 5 6 7 1 这种,也就是 nums[start] <= nums[mid]。此例子中就是 2 <= 5。
这种情况下,前半部分有序。因此如果 nums[start] <=target<nums[mid],则在前半部分找,否则去后半部分找。

第二类 6 7 1 2 3 4 5 这种,也就是 nums[start] > nums[mid]。此例子中就是 6 > 2。
这种情况下,后半部分有序。因此如果 nums[mid] <target<=nums[end],则在后半部分找,否则去前半部分找。

代码实现

int binarySearch(int[] nums, int target, int low, int high) {if (low > high) {return -1;} //判断是否已超出了搜索范围,是则返回-1。int middle = low + (high - low) / 2; //取中位数。if (nums[middle] == target) {return middle;} //判断中位数是否为要找的数if (nums[low] <= nums[middle]) { //判断左半边是不是排好序的。if (nums[low] <= target && target < nums[middle]) { //是,则判断目标值是否在左半边。return binarySearch(nums, target, low, middle - 1); //是,则在左半边继续进行二分搜索。}return binarySearch(nums, target, middle + 1, high); //否,在右半边进行二分搜索。} else {if (nums[middle] < target && target <= nums[high]) { //若右半边是排好序的那一半,判断目标值是否在右边。return binarySearch(nums, target, middle + 1, high); //是,则在右半边继续进行二分搜索。}return binarySearch(nums, target, low, middle - 1); //否,在左半边进行二分搜索。}
}

不定长的边界

前面介绍的二分搜索的例题都给定了一个具体范围或者区间,那么对于没有给定明确区间的问题能不能运用二分搜索呢?

例题:有一段不知道具体长度的日志文件,里面记录了每次登录的时间戳,已知日志是按顺序从头到尾记录的,没有记录日志的地方为空,要求当前日志的长度。

解题思路

可以把这个问题看成是不知道长度的数组,数组从头开始记录都是时间戳,到了某个位置就成为了空:{2019-01-14, 2019-01-17, … , 2019-08-04, …. , null, null, null …}。

思路 1:顺序遍历该数组,一直遍历下去,当发现第一个 null 的时候,就知道了日志的总数量。很显然,这是很低效的办法。

思路 2:借用二分搜索的思想,反着进行搜索。

  • 一开始设置 low = 0,high = 1
  • 只要 logs[high] 不为 null,high *= 2
  • 当 logs[high] 为 null 的时候,可以在区间 [0, high] 进行普通的二分搜索

代码实现

// 先通过getUpperBound函数不断地去试探在什么位置会出现空的日志。
int getUpperBound(String[] logs, int high) {if (logs[high] == null) {return high;}return getUpperBound(logs, high * 2);
}// 再运用二分搜索的方法去寻找日志的长度。
int binarySearch(String[] logs, int low, int high) {if (low > high) {return -1;}int middle = low + (high - low) / 2;if (logs[middle] == null && logs[middle - 1] != null) {return middle;}if (logs[middle] == null) {return binarySearch(logs, low, middle - 1);} else {return binarySearch(logs, middle + 1, high);}
}`

最后

微信搜索:月伴飞鱼

1.日常分享一篇实用的技术文章,对面试,工作都有帮助

2.后台回复666,获得海量免费电子书籍,会持续更新

二分搜索算法解题步骤,吐血整理相关推荐

  1. 秒杀系统优化方案(下)吐血整理

    接上篇秒杀系统优化方案(上)吐血整理 3. 深入优化设计 3.1   初始方案问题分析 在前面针对数据库的优化中,由于数据库行级锁存在竞争造成大量的串行阻塞,我们使用了存储过程(或者触发器)等技术绑定 ...

  2. 2018你那计算机考试新题型,2018考研新题型考察点和解题步骤

    2018考研交流群 521520558 中公考研英语研究院家,禁止转载! 后2个月,要实现考研英语的,2018考研新题型考察点和解题步骤,一起看看吧. 新题型是考研英语中的重点题型,因为技巧性比较强, ...

  3. 【计算机视觉算法岗面经】“吐血”整理:2019秋招资料

    相关链接:[计算机视觉算法岗面经]"吐血"整理:2019秋招面经 //2018/09/27 兵荒马乱.浩浩荡荡的秋招终于差不多要结束了. 秋招这段时间真是感慨很多,一时得意一时失意 ...

  4. Yolov3 mAp计算,吐血整理,含Bug处理

    Yolov3 mAp计算,吐血整理,含Bug处理 用了两天时间把计算mAp值该踩的坑基本上踩了一遍,整理一下,以免以后忘记 一.所需材料 训练好的权重weights 验证集图片 验证集的xml格式标记 ...

  5. 黑客常用SQL注入绕过技术总结!(冰河吐血整理,建议收藏)

    大家好,我是冰河~~ 今天给大家再次分享一篇硬核内容,那就是黑客常用的SQL注入绕过技术,还是那句话:我们学渗透技术不是为了攻击别人的系统,而是了解黑客常用的渗透技能,以此来修复我们自己系统中的漏洞, ...

  6. 想要专升本你不得不看的全干货_吐血整理_专升本_计算机文化基础( 十 四 )

    大家好,我是阿Ken.很快就要整理完第三章了~ 对于专升本_计算机文化基础我已经在博客里整理了已经一半多了,希望能够在我整理后能够帮助其他的小伙伴,会一直整理完所有的专升本_计算机文化基础的笔记,感兴 ...

  7. 想要专升本你不得不看的全干货_吐血整理_专升本_计算机文化基础( 十 三 )

    大家好,我是阿Ken.很快就要整理完第三章了~ 对于专升本_计算机文化基础我已经在博客里整理了已经一半多了,希望能够在我整理后能够帮助其他的小伙伴,这月底整理完所有的专升本_计算机文化基础的笔记,感兴 ...

  8. 回溯法的解题步骤与例子解析

    回溯法有"通用解题法"之称.用它可以系统地搜索问题的所有解.回溯法是一个既带有系统性又带有跳跃性的搜索算法. 在包含问题的所有解的解空间树中,按照深度优先搜索的策略,从根结点出发深 ...

  9. 《吐血整理》顶级大佬学习方法

    你知道的越多,你不知道的越多 点赞再看,养成习惯 正文 实践是检验真理的唯一标准,这句话大家一定要记住,多实践,我最开始学C语言,发现书本看了之后自己觉得自己无敌了,一去写代码,异常满天飞,指针都不知 ...

最新文章

  1. 【Groovy】集合遍历 ( 调用集合的 any 函数判定集合中是否有指定匹配规则的元素 | 代码示例 )
  2. 小型服务器的操作系统,小型机服务器的操作系统
  3. Python实现GitBook工具
  4. 基于DotNet构件技术的企业级敏捷软件开发平台 - AgileEAS.NET - ActiveXForm运行容器...
  5. LeetCode 504. Base 7 (C++)
  6. Linux操作系统中修改hostname
  7. vs2008 sp1 regex
  8. i5 9600k和i5 9400的差距?哪个好?对比才知道
  9. 【Funpack】蓝牙技术 QA
  10. 人工智能——机器学习是什么
  11. Java中的注解是什么意思,有什么用
  12. 大疆推出Avata无人机以及飞行眼镜Goggles 2
  13. laravel 验证手机号
  14. 蓝颜知己的伤感空间日志发布:想你,是一种,刻骨铭心的痛
  15. Unity 用什么IDE比较好?
  16. ue4开关门点击时onclicked事件没有触发的解决方案
  17. android studio 3.4教程,android studio3.4安装指南图文教程
  18. 使用html5写见缝插针源码,HTML5见缝插针手机游戏代码-闯三关送口红.zip
  19. 双网卡设置一个外网一个内网_双网卡同时上网,内网外网同时启用的解放办法...
  20. VC++ 操作 word

热门文章

  1. linux fdisk -l 找不到新加的硬盘
  2. 2023年最新,Win11+Tensorflow+CUDA部署,用显卡加速训练模型
  3. AppImage 桌面图标创建方法
  4. matlab整除取余_MATLAB中有没有求余运算符?
  5. Axure知识点:如何制作轮播图效果(以泉州师范学院官网为例)
  6. 南京邮电大学高级语言程序设计实验三(函数实验)
  7. 基于opencv的手势识别
  8. POJ 2253 Frogger(floyd dijkstra spfa)
  9. C++列车时刻查询开发进度记录
  10. linux 动态定时器,Linux 3.10完整支持的DynTicks(动态定时器)特性简介