二分查找

1.算法介绍

二分查找法(Binary Search)算法,也叫折半查找算法。二分查找针对的是一个有序的数据集合,查找思想有点类似于分治思想。每次都通过跟区间的中间元素对比,将带查找的区间缩小为之前的一半,知道找到要查找的元素,或者区间被缩小为0。二分查找是一种非常非常高效的查询算法,时间复杂度未O(logn)。

基本算法思想:先确定待查找元素所在的区间范围,在逐步缩小范围,直到找到元素或找不到该元素为止。

2.算法过程

二分查找算法的过程如下所示:

  • 每次查找时从数组的中间元素开始,如果中间元素正好是要查找的元素,则搜索过程结束;
  • 如果某一特定元素大于或者小于中间元素,则在数组大于或小于中间元素的那一半中查找,而且跟开始一样从中间元素开始比较。
  • 如果在某一步骤数组为空,则代表找不到。

举个例子来说,给定一个有序数组 [0, 1, 2, 3, 4, 5, 6, 7, 8]。如果我们希望查找 5 是否在这个数组中。

  • 第一次区间为整个数组 [0, 1, 2, 3, 4, 5, 6, 7, 8],中位数是 4,因为 4 小于 5,所以如果 5 存在在这个数组中,那么 5 一定在 4 右边的这一半区间中。于是我们的查找范围变成了 [4, 5, 6, 7, 8]
  • 第二次区间为 [4, 5, 6, 7, 8],中位数是 6,因为 5 小于 6,所以如果 5 存在在这个数组中,那么 5 一定在 6 左边的这一半区间中。于是我们的查找范围变成了 [4, 5, 6]
  • 第三次区间为 [4, 5, 6],中位数是 5,正好是我们需要查找的数字。

于是我们发现,对于一个长度为 9 的有序数组,我们只进行了 3 次查找就找到了我们需要查找的数字。而如果是依次遍历数组,则最坏情况下,我们需要查找 9 次。

3.简单例题

【LeetCode】704.二分查找

给定一个 n 个元素有序的(升序)整型数组 nums 和一个目标值 target ,写一个函数搜索 nums 中的 target,如果目标值存在返回下标,否则返回 -1

示例:

输入: nums = [-1,0,3,5,9,12], target = 9
输出: 4
解释: 9 出现在 nums 中并且下标为 4

二分查找解题:

class Solution:def search(self, nums: List[int], target: int) -> int:l,r = 0,len(nums)-1while l <= r:mid = (l+r)//2if nums[mid] > target:r = mid - 1elif nums[mid] < target:l = mid + 1else:return midreturn -1

4.二分查找细节

从上面的例子中我们了解了二分查找的思路和具体代码。但是真正在解决二分查找题目的时候还是需要考虑很多细节的。比如说以下几个问题:

  1. 区间的开闭问题:区间应该是左闭右闭,还是左闭右开?
  2. mid 的取值问题:mid = (left + right) // 2,还是 mid = (left + right + 1) // 2
  3. 出界条件的判断:left <= right,还是 left < right
  4. 搜索区间范围的选择:left = mid + 1right = mid - 1left = midright = mid 应该怎么写?

下面一一讲解。

区间的开闭问题

区间的左闭右闭、左闭右开指的是初始待查找区间的范围。

  • 左闭右闭:初始化赋值时,left = 0right = len(nums) - 1left 为数组第一个元素位置,right 为数组最后一个元素位置,从而区间 [left, right] 左右边界上的点都能取到。
  • 左闭右开:初始化赋值时,left = 0right = len(nums)left 为数组第一个元素位置,right 为数组最后一个元素的下一个位置,从而区间 [left, right) 左边界点能取到,而右边界上的点不能取到。

关于左闭右闭、左闭右开,其实在网上都有对应的代码和解法。但是相对来说,左闭右开这种写法在解决问题的过程中,需要考虑的情况更加复杂,所以建议 全部使用「左闭右闭」区间

mid 的取值问题

在二分查找的实际问题中,最常见的 mid 取值就是 mid = (left + right) // 2 或者 mid = left + (right - left) // 2。前者是最常见写法,后者是为了防止整型溢出。式子中 // 2 就代表的含义是中间数「向下取整」。当待查找区间中有偶数个元素个数时,则位于最中间的数为 2 个,这时候使用上面式子只能取到中间靠左边那个数,而取不到中间靠右边的那个数。那么,右边的那个数到底能取吗?

其实,右边的数也是可以取的,令 mid = (left + right + 1) // 2,或者 mid = left + (right - left + 1) // 2。这样如果待查找区间的元素为偶数个,就能取到中间靠右边的那个数了。

这是因为二分查找的思路是根据每次选择中间位置上的数值来决定下一次在哪个区间查找元素。每一次选择的元素位置可以是中间位置,但并不是一定非得是区间中间位置元素,靠左一些、靠右一些、甚至区间三分之一、五分之一处等等,都是可以的。比如说 mid = left + (right - left + 1) * 1 // 5 也是可以的。

但一般来说,取中间位置元素在平均意义下所达到的效果最好。同时这样写最简单。而对于 mid 值是向下取整还是向上取整,大多数时候是选择不加 1。但有些写法中,是需要考虑加 1 的,这个后面会说这种写法。

出界条件的判断

我们经常看到二分查找算法的写法中,while 语句出界判断的语句有left <= rightleft < right 两种写法。那我们究竟应该在什么情况用什么写法呢?

  • 如果判断语句为 left <= right,且查找的元素不存在,则 while 判断语句出界条件是 left == right + 1,写成区间形式就是 [right + 1, right],此时待查找区间为空,待查找区间中没有元素存在,所以此时终止循环可以直接返回 -1 是正确的。

    • 比如说区间 [3, 2],不可能存在一个元素既大于等于 3 又小于等于 2,此时直接终止循环,返回 -1 即可。
  • 如果判断语句为left < right,且查找的元素不存在,则 while 判断语句出界条件是 left == right,写成区间形式就是 [right, right]。此时区间不为空,待查找区间还有一个元素存在,并不能确定查找的元素不在这个区间中,此时终止循环返回 -1 是错误的。
    • 比如说区间 [2, 2],元素 2 就属于这个区间,此时终止循环,返回 -1 就漏掉了这个元素。

但是如果我们还是想要使用 left < right 的话,怎么办?

可以在返回的时候需要增加一层判断,判断 left 所指向位置是否等于目标元素,如果是的话就返回 left,如果不是的话返回 -1。即:

# ...while left < right:# ...return left if nums[left] == target else -1

此外,用 left < right 的话还有一个好处,就是退出循环的时候 left == right 成立,就不用判断应该返回 left 还是 right 了。

搜索区间范围的选择

在进行区间范围选择的时候,有时候是 left = mid + 1right = mid - 1,还有的时候是 left = mid + 1right = mid,还有的时候是 left = midright = mid - 1。那么我们到底应该如何确定搜索区间范围呢?

这是二分查找的一个难点,写错了很容易造成死循环,或者得不到正确结果。

这其实跟二分查找算法的两种不同思路有关。

  • 思路 1:「直接找」—— 在循环体中找到元素后直接返回结果。
  • 思路 2:「排除法」—— 在循环体中排除目标元素一定不存在区间。

二分查找两种思路

思路 1:「直接找」

第 1 种思路比较简单,一旦我们在循环体中找到元素就直接返回结果。其实我们在上边 「3. 简单二分查找 - [704. 二分查找」 中就已经用过了。这里再看一下思路和代码:

思路:

  • 取两个节点中心位置 mid,先看中心位置值 nums[mid]

    • 如果中心位置值 nums[mid] 与目标值 target 相等,则 直接返回 这个中心位置元素的下标。
    • 如果中心位置值 nums[mid] 小于目标值 target,则将左节点设置为 mid + 1,然后继续在右区间 [mid + 1, right] 搜索。
    • 如果中心位置值 nums[mid] 大于目标值 target,则将右节点设置为 mid - 1,然后继续在左区间 [left, mid - 1] 搜索。
class Solution:def search(self, nums: List[int], target: int) -> int:left = 0right = len(nums) - 1# 在区间 [left, right] 内查找 targetwhile left <= right:# 取区间中间节点mid = left + (right - left) // 2# 如果找到目标值,则直接范围中心位置if nums[mid] == target:return mid# 如果 nums[mid] 小于目标值,则在 [mid + 1, right] 中继续搜索elif nums[mid] < target:left = mid + 1# 如果 nums[mid] 大于目标值,则在 [left, mid - 1] 中继续搜索else:right = mid - 1# 未搜索到元素,返回 -1return -1

细节:

  • 这种思路是在一旦循环体中找到元素就直接返回。
  • 循环可以继续的条件是 left <= right
  • 如果一旦退出循环,则说明这个区间内一定不存在目标元素。

5.2 思路 2:「排除法」

第 2 种思路在循环体中排除目标元素一定不存在区间。

思路:

  • 取两个节点中心位置 mid,根据判断条件先将目标元素一定不存在的区间排除。
  • 然后在剩余区间继续查找元素,继续根据条件排除不存在的区间。
  • 直到区间中只剩下最后一个元素,然后再判断这个元素是否是目标元素。

根据第二种排除法的思路,我们可以写出来两种代码。

第一种代码:

class Solution:def search(self, nums: List[int], target: int) -> int:left = 0right = len(nums) - 1# 在区间 [left, right] 内查找 targetwhile left < right:# 取区间中间节点mid = left + (right - left) // 2# nums[mid] 小于目标值,排除掉不可能区间 [left, mid],在 [mid + 1, right] 中继续搜索if nums[mid] < target:left = mid + 1 # nums[mid] 大于等于目标值,目标元素可能在 [left, mid] 中,在 [left, mid] 中继续搜索else:right = mid# 判断区间剩余元素是否为目标元素,不是则返回 -1return left if nums[left] == target else -1

第二种代码:

class Solution:def search(self, nums: List[int], target: int) -> int:left = 0right = len(nums) - 1# 在区间 [left, right] 内查找 targetwhile left < right:# 取区间中间节点mid = left + (right - left + 1) // 2# nums[mid] 大于目标值,排除掉不可能区间 [mid, right],在 [left, mid - 1] 中继续搜索if nums[mid] > target:right = mid - 1 # nums[mid] 小于等于目标值,目标元素可能在 [mid, right] 中,在 [mid, right] 中继续搜索else:left = mid# 判断区间剩余元素是否为目标元素,不是则返回 -1return left if nums[left] == target else -1

细节:

  • 判断语句是 left < right。这样在退出循环时,一定有left == right 成立,就不用判断应该返回 left 还是 right 了。同时方便定位查找元素的下标。但是一定要注意最后要对区间剩余的元素进行一次判断。
  • 在循环体中,优先考虑 nums[mid] 在什么情况下一定不是目标元素,排除掉不可能区间,然后再从剩余区间中确定下一次查找区间的范围。
  • 在考虑 nums[mid] 在什么情况下一定不是目标元素之后,它的对立面(即 else 部分)一般就不需要再考虑区间范围了,直接取上一个区间的反面区间。如果上一个区间是 [mid + 1, right],那么相反面就是 [left, mid]。如果上一个区间是 [left, mid - 1],那么相反面就是 [mid, right]
  • 区分被分为两部分: [left, mid - 1][mid, right] 时,mid 取值要向上取整。即 mid = left + (right - left + 1) // 2。因为如果当区间中只剩下两个元素时(此时 right = left + 1),一旦进入 left = mid 分支,区间就不会再缩小了,下一次循环的查找区间还是 [left, right],就陷入了死循环。
  • 关于边界设置可以记忆为:只要看到 left = mid 就向上取整。或者记为:
    • left = mid + 1right = midmid = left + (right - left) // 2 一定是配对出现的。
    • right = mid - 1left = midmid = left + (right - left + 1) // 2 一定是配对出现的。

5.3 两种思路适用范围

  • 二分查找的思路 1:因为判断语句是 left <= right,有时候要考虑返回是 left 还是 right。循环体内有 3 个分支,并且一定有一个分支用于退出循环或者直接返回。这种思路适合解决简单题目。即要查找的元素性质简单,数组中都是非重复元素,且 ==>< 的情况非常好写的时候。
  • 二分查找的思路 2:更加符合二分查找算法的减治思想。每次排除目标元素一定不存在的区间,达到减少问题规模的效果。然后在可能存在的区间内继续查找目标元素。这种思路适合解决复杂题目。比如查找一个数组里可能不存在的元素,找边界问题,可以使用这种思路。

6.案例实现:

二分下标题目

【LeetCode】374. 猜数字大小

猜数字游戏的规则如下:

  • 每轮游戏,我都会从 1*n* 随机选择一个数字。 请你猜选出的是哪个数字。
  • 如果你猜错了,我会告诉你,你猜测的数字比我选出的数字是大了还是小了。

你可以通过调用一个预先定义好的接口 int guess(int num) 来获取猜测结果,返回值一共有 3 种可能的情况(-110):

  • -1:我选出的数字比你猜的数字小 pick < num
  • 1:我选出的数字比你猜的数字大 pick > num
  • 0:我选出的数字和你猜的数字一样。恭喜!你猜对了!pick == num

返回我选出的数字。

示例:

输入:n = 10, pick = 6
输出:6

题解:

class Solution:def guessNumber(self, n: int) -> int:left = 1right = nwhile left <= right:mid = (left + right) // 2ret = guess(mid)if ret == 0:return midelif ret == -1:right = mid - 1else:left = mid + 1

二分答案题目

【LeetCode】69. x 的平方根

给你一个非负整数 x ,计算并返回 x算术平方根

由于返回类型是整数,结果只保留 **整数部分 **,小数部分将被 舍去 。

**注意:**不允许使用任何内置指数函数和算符,例如 pow(x, 0.5) 或者 x ** 0.5

示例:

输入:x = 4
输出:2

题解:

class Solution:def mySqrt(self, x: int) -> int:l,r = 1,x/2 + 1while l <= r:mid = (l+r)//2if mid > x/mid:r = mid - 1elif mid < x/mid:l = mid + 1else:return int(mid)return int(r)

思考:对于二分查找的使用主要还是在范围的定界和判断的条件处存在问题

算法--二分查找(python实现)相关推荐

  1. 二分查找python实现代码_二分查找——Python实现

    一.排序思想 二.python实现 def binarySearchDemo(arr, key): """ python二分查找非递归方式 :param arr:待排序列 ...

  2. java 二分查找_计算机入门必备算法——二分查找法

    1.引言 笔者对于计算机的研究一直停滞不前,近期想对一些算法进行复习和进一步的研究,每天都会更新一个新的算法,算法有难有易,层层递进.不希望能学的有多么高深,只希望在一些最基本的算法上有编码的思路,或 ...

  3. Python算法二分查找之重复元素列表

    描述: 请实现有重复数字的升序数组的二分查找 给定一个 元素有序的(升序)长度为n的整型数组 nums 和一个目标值 target  ,写一个函数搜索 nums 中的第一个出现的target,如果目标 ...

  4. 90%的程序员都写错的算法-二分查找万能模版

    新的角度看二分 二分就是将数组分为两段 因此,问题的最终目标是找出蓝红边界 朴素算法 红色指针一开始指向最右超出范围处,随后不断向左移动,直到找到蓝红边界:或者蓝色指针- 时间复杂度O(n)O(n)O ...

  5. 数据结构与算法 | 二分查找

    假如我们在图书馆中整理书记,书按照序号有序排序,序号不连续,当我们需要从书堆中找到特定序号的书时,如果一本一本查找的话,我们可能需要查找N次.但如果我们先找到中间的书,将书分成两堆,与待查书对比,待查 ...

  6. [算法][二分查找][排除法]

    最基本的二分查找算法 「搜索区间」是 [left, right] nums[mid] == target 时可以立即返回 int binary_search(int[] nums, int targe ...

  7. 数据结构与算法——二分查找与二叉排序树

    文章目录 1.预备知识 1.1 题目目录 1.2 二分查找 1.3 递归二分查找 1.4 循环二分查找 1.5 二叉查找(排序)树 1.6 二叉搜索树的代码实现 2.搜索插入位置 2.1 题目描述 2 ...

  8. 二分法python上机实验报告_二分查找-Python刷题笔记

    二分搜索是一种在有序数组中查找某一特定元素的搜索算法. 二分查找示意图 搜索过程从数组的中间元素开始,如果中间元素正好是要查找的元素,则搜索过程结束:如果某一特定元素大于或者小于中间元素,则在数组大于 ...

  9. 【算法】算法 二分查找 二分查找 查找多个相同的值

    文章目录 1.概述 2.算法 3.查找多个相同的值 4. 非递归 本文为博主九师兄(QQ:541711153 欢迎来探讨技术)原创文章,未经允许博主不允许转载. 1.概述 参考视频:https://w ...

  10. php折半查找算法,二分查找 [折半查找] 算法 PHP 版

    查找表:就是同一类型的数据元素构成的数据集合 有静态表和动态表 本文实现PHP版的二分查找算法[本算法仅用于顺序存储的查找表] /** * Created by PhpStorm. * User: 1 ...

最新文章

  1. shell脚本编程基础(1)及RAID阵列
  2. initMNN: init numpy failed mnn笔记
  3. Python Matplotlib 画图显示中文
  4. 听红宝书译者谈Web视角下的前端开发
  5. linux修改文件内容_详解5种实用方法---Linux系统清空或删除大文件内容
  6. 军事武器知识图谱构建与自动问答项目QAonMilitaryKG
  7. #华为云·寻找黑马程序员#【代码重构之路】如何“消除”if/else
  8. linux系统内核参数命令,Linux内核启动参数解析及添加
  9. comet学习(三)cometd心跳机制
  10. 13.vim 全局替换路径
  11. mysql安装包下载与检核
  12. 监控服务器的日志文件,服务器上监控日志文件
  13. BLE-CC2541项目的一点经验记录
  14. 简单实现select多选功能
  15. 酷睿是图拉丁的孙子[经典理论]
  16. LaTeX调整公式中部分字号及行距
  17. bp神经网络缺点及克服,bp神经网络存在的问题
  18. 微信小程序获取rich-text(富文本)渲染内容高度,rich-text(富文本)里img 样式设置
  19. Error:.系统错误,错误码:80058,desc of scope.userLocation is empty[20210513 20:05:21][wxd0659441b63ab707]
  20. C# 可为 null 的类型

热门文章

  1. 简单图(simple graph)原理及实现(Java)
  2. oracle ndb,NDB语法 - ivaneeo's blog - BlogJava
  3. sci的figure怎么做_论文攻略丨SCI论文插图怎么做?有这一篇文章就够了
  4. java简易计算器报告_简易计算器java实训报告.docx
  5. Chrome浏览器谷歌翻译 失效/用不了的解决方法idea-翻译插件失败(TKK: 更新 TKK 失败,请检查网络连接)
  6. 京东E卡购买api接口
  7. 基于STM32设计的实时心率检测仪
  8. 《FreeKick》战术_游戏前线
  9. LOJ#6198. 谢特(SAM+01Trie树合并)
  10. python读取数据集前十行,python数据分析万字干货!一个数据集全方位解读pandas