二分查找的定义如下(引自Wiki):

在计算机科学中,二分查找算法(英语:binary search algorithm),也称折半搜索算法(英语:half-interval search algorithm)、对数搜索算法(英语:logarithmic search algorithm),是一种在有序数组中查找某一特定元素的搜索算法。搜索过程从数组的中间元素开始,如果中间元素正好是要查找的元素,则搜索过程结束;如果某一特定元素大于或者小于中间元素,则在数组大于或小于中间元素的那一半中查找,而且跟开始一样从中间元素开始比较。如果在某一步骤数组为空,则代表找不到。这种搜索算法每一次比较都使搜索范围缩小一半。

二分查找算法在最坏情况下是对数时间复杂度的,需要进行 O(logn) 次比较操作(n在此处是数组的元素数量,O是大O记号,log 是对数)。二分查找算法使用常数空间,对于任何大小的输入数据,算法使用的空间都是一样的。除非输入数据数量很少,否则二分查找算法比线性搜索更快,但数组必须事先被排序。尽管一些特定的、为了快速搜索而设计的数据结构更有效(比如哈希表),二分查找算法应用面更广。

总结一句,由于二分必须在有序数组中进行,看到题目条件有有序数组的话就应该想到二分查找。

Leetbook上有关于二分查找的内容,但还是局限在多个模板套用上,且题目与知识点对应不上。更推荐的是这篇文章,真正做到了理解核心而不是套用模板。

二分查找中使用的术语:

目标 Target —— 你要查找的值
索引 Index —— 你要查找的当前位置
左、右指示符 Left,Right —— 我们用来维持查找空间的指标
中间指示符 Mid —— 我们用来应用条件来确定我们应该向左查找还是向右查找的索引

下面我们结合题目来解析二分查找的思路:

704. 二分查找

class Solution:def search(self, nums: List[int], target: int) -> int:left = 0right = len(nums) - 1while left < right:mid = (left + right) // 2if nums[mid] < target:left = mid + 1else:right = midif nums[left] == target:return leftreturn -1

首先是左右指示符的设置,left = 0right = len(nums) - 1 基本上每题开头都是把整个区间作为我们想进行二分查找的区间,当然也有例外,后面会看到。

然后就是求中间指示符 mid 的环节,mid = (left + right) // 2 ,这里更推荐 mid = left + (right - left) // 2 的写法因为要防止 left + right 整形溢出。同时我们要注意到,// 2 相当于是向下取整的,如果是希望向上取整则写成mid = (left + right + 1) // 2 或者 mid = left + (right - left + 1) // 2 。向下取整时 mid 就会被分到左边,向上取整时 mid 就会被分到右边。

紧接着就是二分的核心部分,区间的划分了。很多模板会把区间划分为等于 target、大于 target 和小于 target 三个区间,实际上是有点绕了。此处我们统一每次只划分两个区间,可能存在目标元素的区间和一定不存在目标元素的区间,那么可能存在目标元素的区间要么在左边要么在右边,两种可能

又根据 mid 是被划分在左边的区间还是右边的区间,得到两种分法。因此共有4种情况,如下图所示:


分法一(默认) mid = (left + right) // 2
第一种情况:mid 在左边区间,目标元素在右边区间,则 left = mid + 1
第二种情况:mid 在左边区间,目标元素在左边区间,则 right = mid

分法二 mid = (left + right + 1) // 2
第三种情况:mid 在右边区间,目标元素在右边区间,则 left = mid
第四种情况:mid 在右边区间,目标元素在左边区间,则 right= mid - 1

其中第一种和第二种情况一定同时出现(分法一),第三种和第四种情况一定同时出现(分法二)。然后,我们来考虑下如果只剩下两个元素的情形,如下图所示:

如果是分法一,即left = mid + 1right = mid,此时 mid 必须等于 left,即向下取整,下一步才会有 left = mid + 1 = right 或者 right = mid = left,得到 left == right 从而退出循环。

如果是分法二,即left = midright = mid - 1,此时 mid 必须等于 right,即向上取整,下一步才会有 right = mid - 1 = left 或者 left = mid = right,得到 left == right 从而退出循环。

他们的共同点是,最后退出循环时 left 一定等于右边的那个元素(mid + 1)

最后,可知退出循环后一定有 left == right,如果 left (或者 right,一样的)满足条件(例如 nums[left] == target),则返回 left(或者right)。

35. 搜索插入位置

class Solution:def searchInsert(self, nums: List[int], target: int) -> int:# 特殊情况if nums[-1] < target:return len(nums)left = 0right = len(nums)- 1while left < right:mid = left + (right - left) // 2if nums[mid] < target:left = mid + 1else:right = midreturn left

本题与704题基本一样,区别只是不要求找到一样的元素,而是要找到第一个大于等于 target 的元素索引。此处使用的还是分法一,向下取整 mid = left + (right - left) // 2,mid 一定在左边的区间,if nums[mid] < target即左边的区间小于 target,那么第一个大于等于 target 的元素一定在右边的区间,因此到右边的区间去寻找元素, left = mid + 1。循环结束之后,一定有 left == right,由于它们的区间 [left, right] 一定有第一个大于等于 target 的元素,所以最后区间只有一个元素,它的索引 left 即为所求。

162. 寻找峰值

class Solution:def findPeakElement(self, nums: List[int]) -> int:length = len(nums)left, right = 0, length - 1while left < right:mid = left + (right - left) // 2if nums[mid] < nums[mid + 1]:left = mid + 1else:right = midreturn left

找到大于左右相邻元素的值,若 nums[mid] < nums[mid + 1],则目标区间在右边,剩下两个元素时,mid向下取整等于left,可以取到更大值 left = mid + 1 = right

34. 在排序数组中查找元素的第一个和最后一个位置

class Solution:def searchRange(self, nums: List[int], target: int) -> List[int]:length = len(nums)# 特殊情况if (not nums) or nums[-1] < target or nums[0] > target or length == 0:return [-1, -1]# 找左边界left1, right1 = 0, length - 1while left1 < right1:mid1 = left1 + (right1 - left1) // 2 # 向下取整if nums[mid1] < target:left1 = mid1 + 1else:right1 = mid1# 数组中不存在targetif nums[left1] != target:return [-1, -1]# 找右边界left2, right2 = left1, length - 1  # 此处优化了,找右边界的过程从left1到length - 1的区间中找while left2 < right2:mid2 = left2 + (right2 - left2 + 1) // 2 # 向上取整if nums[mid2] > target:right2 = mid2 - 1else:left2 = mid2return [left1, right2]

本题可以看作是704题的高阶版,数组中的元素是可能重复的,然后要找 target 在数组出现的第一个位置和最后一个位置。

在找第一个位置的时候,可以借助35题的思路,什么样的位置是第一次出现的位置呢?那就是第一个大于等于 target 的元素位置。还是用的分法一,判断条件是 if nums[mid1] < target,如果 mid1 小于 target 即左边的区间小于 target,所以右边的区间大于等于 target,到右边区间继续找left1 = mid1 + 1。循环结束后由于题目是要求 target 出现,所以判断 nums[left1] 与 target 是否相等,相等才继续。

然后找最后一个位置,显然,这相当于找第一个小于等于 target 的元素位置,用分法一,判断条件为 if nums[mid2] > target,如果 mid2 大于 target 即左边的区间大于 target,所以右边的区间小于等于 target,等等,顺序不对???左边的区间大于 target,左边的区间又小于右边的区间,怎么可能右边的区间小于等于 target 呢?因此,我们要改用分法二,向上取整,把 mid2 归到右边的区间,判断条件还是 if nums[mid2] > target,如果 mid2 大于 target 即右边的区间大于 target,所以左边的区间小于等于 target,到左边区间继续找right2 = mid2 - 1。能进行到这里说明 target 肯定会出现了,所以不用判断 nums[right2 ] 与 target 是否相等,直接返回答案。

33. 搜索旋转排序数组

class Solution:def search(self, nums: List[int], target: int) -> int:length = len(nums)if not nums:return -1left, right = 0, length - 1while left < right:mid = left + (right- left) // 2 # 分法一,mid在左边区间,向下取整if nums[mid] < nums[right]: # mid所在位置元素小于最右边元素,说明右边区间有序if nums[mid] < target <= nums[right]: # 如果target在右边区间left = mid + 1else: # 否则在左边区间right = midelse: # mid所在位置元素大于(不会等于)最右边元素,说明左边区间有序if nums[left] <= target <= nums[mid]: # 如果target在左边区间(mid也在左边区间,可能等于target)right = midelse: # 否则在右边区间left = mid + 1if nums[left] == target: # 等于目标值return leftelse: # 不存在目标值return -1

这题的数组是循环有序,对于 mid 来说,要么是 mid 所在的左边区间(分法一)有序,要么是右边区间有序,所以首先要判断哪个区间有序,再到有序区间进行 target 的寻找(因为 mid 与 target 的比较一定是在有序区间进行的)。

右边区间有序,判断条件是 if nums[mid] < target <= nums[right] ,第一个取小于号是因为 mid 在左边区间,一定小于在右边区间的 target,而第二个取小于等于号是因为 target 可能是最右边的元素。

左边区间有序,判断条件是 if nums[left] <= target <= nums[mid],同理,target 和 mid 都在左边区间,都可能等于最左边的元素。

81. 搜索旋转排序数组 II

分法一:

class Solution:def search(self, nums: List[int], target: int) -> bool:length = len(nums)left, right = 0, length - 1while left < right:mid = left + (right - left) // 2if nums[mid] < nums[right]: # 右边区间一定有序if nums[mid] < target <= nums[right]:left = mid + 1else:right = midelif nums[mid] > nums[right]: # 左边区间一定有序(旋转点在右边区间)if nums[left] <= target <= nums[mid]:right = midelse:left = mid + 1else: # 无法判断是否有序,例如[3, 1, 2, 3, 3, 3, 3]if nums[right] == target:return Trueelse:right -= 1return nums[left] == target

分法二:

class Solution:def search(self, nums: List[int], target: int) -> bool:length = len(nums)left, right = 0, length - 1while left < right:mid = left + (right - left + 1) // 2if nums[mid] < nums[right]: # 右边区间一定有序if nums[mid] <= target <= nums[right]:left = midelse:right = mid - 1elif nums[mid] > nums[right]: # 左边区间一定有序(旋转点在右边区间)if nums[left] <= target < nums[mid]:right = mid - 1else:left = midelse: # 无法判断是否有序,例如[3, 1, 2, 3, 3, 3, 3]if nums[right] == target:return Trueelse:right -= 1return nums[left] == target

作为33题的进阶版,这道题难在数组中的元素是可能相同的,如果出现 nums[mid] == nums[right] 的情况,无法判断左边区间还是右边区间是有序的。解决方法就是对于这种情况,每次缩减 right - 1 即右边界左移一位,直到可以判断左右区间哪个有序为止。

题解既有分法一也有分法二,他们的核心区别是分法一把 mid 归到左边区间,分法二把 mid 归到右边区间。由此导致了向下与向上取整的不同、寻找目标区间的左右边界更新位置不同、以及 mid 与左右区间元素的大小关系不同。

153. 寻找旋转排序数组中的最小值

class Solution:def findMin(self, nums: List[int]) -> int:length = len(nums)left, right = 0, length - 1while left < right:mid = left + (right - left) // 2if nums[mid] < nums[right]: # 右边区间有序,拐点一定在左边区间right = midelse: # 右边区间无序,拐点一定在右边区间left = mid + 1return nums[left]

本题由于没有 target,甚至比33题还要简单,只需要不断地找无序的区间(同时也是拐点所在的区间)即可,由剩余两个元素时的情况可以知道,退出循环时必然 left 等于右边的元素,即拐点的右边(最小值)。

154. 寻找旋转排序数组中的最小值 II

class Solution:def findMin(self, nums: List[int]) -> int:length = len(nums)left, right = 0, length - 1while left < right:mid = left + (right - left) // 2if nums[mid] < nums[right]: # 右边区间有序,拐点一定在左边区间right = midelif nums[mid] > nums[right]: # 右边区间无序,拐点一定在右边区间left = mid + 1else: # mid与右边界相等,无法判断,只能缩小范围right -= 1return nums[left]

本题是153题的进阶版,与81题类似,就是多了元素可能重复这个条件。由于存在无法判断是否有序的情况,所以要单独讨论,出现这种情况时就缩小范围 right -= 1,其余情况还是正常找拐点所在区间。

658. 找到 K 个最接近的元素

class Solution:def findClosestElements(self, arr: List[int], k: int, x: int) -> List[int]:n = len(arr)# 最小的起点为0,最大的起点为n-k,这样才能保证选取长度为k的连续子数组low, high = 0, n - k # 框长度为k,所以起点范围[0, n-k]while low < high:mid = (low + high) // 2if x - arr[mid] <= arr[mid + k] - x:   # x更靠近左边的元素,我们的框应该往左边找high = midelse: # x更靠近右边的元素,我们的框应该往右边找low = mid + 1return arr[low: low + k]

这题虽然代码很基本,但是思路不容易。找到 k 个与 x 最接近的数,可以把这 k 个数看作是一个长度为 k 的框,则框的左起点的范围是 [0, n-k]。然后二分查找这个左起点,若 x 与目前左起点 arr[mid] 的距离小于等于 x 与右起点 arr[mid + k] 的距离,if x - arr[mid] <= arr[mid + k] - x:,则框应该向左移,即左起点的取值范围从右边缩小, high = mid,反之从左边缩小,最后得到最接近 x 的 k 个数(框)。

二分查找基础概念与经典题目(Leetcode题解-Python语言)二分索引型相关推荐

  1. 二分查找基础概念与经典题目(Leetcode题解-Python语言)二分数值型

    二分查找的讲解请见上一篇文章.本文主要记录对数值进行二分的题目解法与思路. 374. 猜数字大小 class Solution:def guessNumber(self, n: int) -> ...

  2. 在数组中找重复数、只出现一次的数或丢失数的题目(Leetcode题解-Python语言)

    在一维数组中的考察中,最常见的就是找出数组中的重复数.只出现一次的数或者丢失(消失)数等等. 一般来说,首先想到的就是用哈希表(集合)来记录出现过的数,基本所有的题都可以用集合来做,而技巧性在于有时可 ...

  3. 链表基础概念与经典题目(Leetcode题解-Python语言)

    所谓链表,就是由链节点元素组成的表,那什么是链节点呢?直接上定义: class ListNode:def __init__(self, val=0, next=None):self.val = val ...

  4. 哈希表(散列表)基础概念与经典题目(Leetcode题解-Python语言)之上——原理与设计

    哈希表(Hash table,也叫散列表),是根据键(Key)而直接访问数据在内存中的储存位置(又叫做存储桶,Buckets)的数据结构.也就是说,它通过计算一个关于键值的函数(哈希函数,Hash f ...

  5. 二叉树层序遍历(广度优先搜索)基础概念与经典题目(Leetcode题解-Python语言)

    二叉树的广度优先搜索即从上到下.从左到右地进行搜索,对于层序遍历(Level Order)问题,即依次遍历第一层节点.第二层节点-等,基本可以秒杀. 广度优先搜索是通过队列来实现的,python中优先 ...

  6. 队列的基础概念与经典题目(Leetcode题解-Python语言)

    队列是先入先出(后入后出)的数据结构,常用操作就 push 和 popleft,Python中用列表中的 pop(0) 或者 collection.deque的 popleft() 都可以. 普通队列 ...

  7. 栈的基础概念与经典题目(Leetcode题解-Python语言)

    栈是先入后出(后入先出)的数据结构,常用操作就 push 和 pop,Python中用列表实现即可,基本概念可以看Leetbook相关章节. 普通栈 232. 用栈实现队列 class MyQueue ...

  8. 哈希表(散列表)基础概念与经典题目(Leetcode题解-Python语言)之下——设计键

    在很多应用中,我们会发现某种映射关系(模式),但它并不是简单一 一对应的.这时,我们就要从键 key 入手,通过设计合适的键,建立映射关系.leetbook的这个章节总结了一些常见的键,以供参考. 4 ...

  9. 哈希表(散列表)基础概念与经典题目(Leetcode题解-Python语言)之中——实际应用

    上一节介绍了哈希表的原理与设计方法,这一节则直接python中现有的哈希表类型:哈希集合 set(集合)和哈希映射 dict(字典)来解决实际应用(刷题). 零.概念 在介绍实际应用之前,有一个概念我 ...

最新文章

  1. 某短视频程序员吐槽:公司要求每天刷短视频的时长和绩效挂钩,这合理吗?网友:那某陌员工要按照奔现次数算绩效吗?...
  2. CVPR2021(Oral) 商汤、港中文实现单目人脸重建新突破: 基于生成网络的渲染器!几何形状更精准!渲染效果更真实!...
  3. XML PUBLISHER输出excel禁止自动将数字格式化处理
  4. 买卖股票的最佳时机|||
  5. 若能回到五年前,我会告诉自己这些创业道理
  6. VTK:图片之PickPixel2
  7. debian常用工具所在的包
  8. 设置圆角、定向设置圆角-按钮等控件
  9. hive中not in优化
  10. Markdown图片并排展示、图注对齐
  11. mysql 修改前缀_批量修改mysql的表前缀
  12. 【软件开发规范七】《Android UI设计规范》
  13. ​PHP现在不好找工作是真的吗?
  14. 2,Jenkins实战应用_Jenkins初始部署与简单配置
  15. 水下机器人 结构设计
  16. My summery
  17. Striped64 api详解
  18. Spring学习笔记(一):眼见为实,先上一个简单例子
  19. linux系统的监控工具名称,Linux下几款系统监控工具介绍
  20. B01 - 004、配置联网

热门文章

  1. 【ArcGIS Pro微课1000例】0011:ArcGIS Pro范围内汇总工具的巧妙使用——以甘肃省各地区内河流总长度计算为例
  2. 热榜!!!数据结构与算法:C语言版---数组与稀疏矩阵---强势来袭!
  3. 【Python可视化】Windows 10系统上Pyecharts安装教程
  4. 【学生选课系统经典】C#与SQLSERVER连接:ASP.NET网站(服务器端,IIS发布)
  5. 数据结构之广度优先搜索(队列实现)问题
  6. 《看聊天记录都学不会C语言?太菜了吧》(15)你学了一节课的函数我5分钟搞定了,还很熟
  7. sql INNER JOIN 取得两个表中存在连接匹配关系的记录(mysql)
  8. fanuc机器人码垛编程实例_FANUC 机器人码垛编程详细讲解 记得收藏!
  9. 史上最低估自己的天才科学家!预言自己的发现无用,没想到影响全世界,可他却在37岁..........
  10. 985硕博士:你为什么比我差?