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

二分查找的定义如下(引自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 - left) // 2if nums[mid] < target:left = mid + 1else:right = midif nums[left] == target:return leftreturn -1

分法二:

class Solution:def search(self, nums: List[int], target: int) -> int:left = 0right = len(nums) - 1while left < right:mid = left + (right - left + 1) // 2if nums[mid] > target:right = mid - 1else:left = midif nums[right] == target:return rightreturn -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 在左边区间,目标元素在右边区间,nums[mid] < target, 则 left = mid + 1
第二种情况:mid 在左边区间,目标元素在左边区间,nums[mid] >= target, 则 right = mid

分法二 mid = (left + right + 1) // 2
第三种情况:mid 在右边区间,目标元素在右边区间,nums[mid] <= target,则 left = mid
第四种情况:mid 在右边区间,目标元素在左边区间,nums[mid] > target,则 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 个数(框)。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/307663.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

Magicodes.IE 2.3重磅发布——.NET Core开源导入导出库

在2.3这一版本的更新中&#xff0c;我们迎来了众多的使用者、贡献者&#xff0c;在这个里程碑中我们也添加并修复了一些功能。对于新特点的功能我将在下面进行详细的描述&#xff0c;当然也欢迎更多的人可以加入进来&#xff0c;再或者也很期待大家来提issues或者PR&#xff0c…

听说用 C# 写 TensorFlow 更高效?

经过半年呕心沥血的努力&#xff0c;SciSharp STACK终于把Tensorflow .NET绑定升级到可以使用 tensorflow 2.3, 新版本最大的优势是实现了Eager模式, 这个特性是让.NET C#/ F#成为机器学习模型开发工具的重要前置条件。NugGet包下载:https://www.nuget.org/packages/TensorFlow…

leetcode279. 完全平方数

一:题目 二:上码 class Solution { public:/**思路:1.分析题意这个就是将一个数分成几个数的和;然而的话,这几个数必须的是完全平方数,我们要求的是最少数量的完全平方数这个满足答案的有好几个;但是我们要求的是最少的数量2.动态规划五步走1>:确定dp数组的含义以及下标的含…

跟我一起学.NetCore之Options实例演示及分析

前言来啦&#xff01;来啦&#xff01;上一节一堆代码&#xff0c;是不是感觉甚是无味啊&#xff1f;没关系&#xff0c;这里结合上一节内容专注举例演示&#xff0c;绝不废话&#xff01;走起~~~~~正文老规矩&#xff0c;一个WebApi项目走起&#xff0c;项目结构如下&#xff…

leetcode139. 单词拆分

一:题目 二:上码 class Solution { public:/**思路:1.分析题意单词就是物品;字符串就是背包;单词能否组成字符串就是在问,物品能不能将背包装满单词可以重复使用那么说明这是一个完全背包2.动态规划五步走1>:确定dp数组的与下标的含义&#xff08;这里用下标i是由我们的遍历…

大数据下的质量体系建设

一、背景大数据、人工智能是当前也是未来几年IT部门的重点建设方向&#xff0c;新的技术可以为业务突破盈利瓶颈&#xff0c;带来新的增长点&#xff0c;同时我们也发现数据中台也频频在最近的企业财报予以体现&#xff0c;相关的技术岗位需求也是供不应求&#xff0c;与之形成…

Pandas中的 transform() 结合 groupby() 用法示例

首先&#xff0c;假设我们有如下餐厅数据集&#xff1a; import pandas as pddf pd.DataFrame({restaurant_id: [101,102,103,104,105,106,107],address: [A,B,C,D, E, F, G],city: [London,London,London,Oxford,Oxford, Durham, Durham],sales: [10,500,48,12,21,22,14] })…

跟我一起学.NetCore之日志(Log)模型核心

前言鲁迅都说&#xff1a;没有日志的系统不能上线(鲁迅说&#xff1a;这句我没说过&#xff0c;但是在理)&#xff01;日志对于一个系统而言&#xff0c;特别重要&#xff0c;不管是用于事务审计&#xff0c;还是用于系统排错&#xff0c;还是用于安全追踪.....都扮演了很重要的…

Numpy中数组创建函数的辨析

首先推荐Numpy官方的教程&#xff0c;网址。 很多人会对数组创建函数的参数中什么时候要用括号np.zeros((2, 3))&#xff0c;什么时候不用括号np.eye(3, 5)感到疑惑&#xff0c;这里对它们统一进行梳理。&#xff08;按照官方文档的分类方法&#xff09; 1. 一维数组创建函数…

leetcode213. 打家劫舍 II

一:题目 二:上码 class Solution { public:/**思路:1.既然成环了,我们如果选取得一条偷取路径是从头开始得那么我们就不能偷取最后一个,那就不算最后一个偷取一遍2.同理我们也可以不算第一个 偷取一遍计算一次偷取得结果*/int rob(vector<int>& nums) {if(nums.size…

一文弄懂Numpy中ndarray的维度(dimension)/轴数(axis/axes)问题

Numpy库的核心是ndarray&#xff0c;实际上就是N维数组&#xff08;N-dimensional array&#xff09;&#xff0c;关于这个数据对象的详细介绍&#xff0c;参考官方文档最为合适。有一点要注意的是&#xff0c;ndarray的内置方法只有30多个&#xff0c;常用的如求平均值可以写a…

leetcode337. 打家劫舍 III

一:题目 二:上码 /*** Definition for a binary tree node.* struct TreeNode {* int val;* TreeNode *left;* TreeNode *right;* TreeNode() : val(0), left(nullptr), right(nullptr) {}* TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}*…

asp.net core 从 3.1 到 5.0

asp.net core 从 3.1 到 5.0Intro就在前几天&#xff0c;微软宣布了 .NET5 发布了 RC1 版本&#xff0c;这也意味着 .NET5 的开发基本稳定了&#xff0c;正式发布之前&#xff0c;不会再新增新的 Feature&#xff0c;只会专注于修复 BUG 提高稳定性。对于开发者来说&#xff0c…

leetcoed123. 买卖股票的最佳时机 III

一&#xff1a;题目 二:上码 class Solution { public:/**思路:1.动态规划五步走1>:确定dp数组以及下标的含义因为题目给出至多完成两笔交易 那么我们一天的状态就有5种0 无操作1 第一次买入2 第一次卖出3 第二次买入4 第二次卖出dp[i][j] 表示的是在第i天 [0,4] 其中某个…

送福利 | 送书3本 ASP.NET Core 真机拆解

小编&#xff1a;最近.NET相关图书在多年沉寂后重新恢复&#xff0c;本书作者提供3本送给公众号粉丝&#xff0c;所以参与方式&#xff1a;文章下方留言&#xff0c;你可以聊聊.NET Core 这几年的发展给你的印象&#xff0c;你的感想&#xff0c;点赞最多的前5位获奖。活动截止…

异方差 的 BP检验 方法及原理详解

异方差 的 BP检验详解            文章目录 1. `BP`检验的步骤2. 场景示例步骤 ①步骤 ②BP检验,也称为Breusch-Pagan检验,是一种用于检验线性回归模型中异方差性(即误差项方差不恒定)的统计方法。该方法由Trevor S. Breusch和Adrian R. Pagan在1980年提出。 1.…

leetcode309. 最佳买卖股票时机含冷冻期

一&#xff1a;题目 二:上码 class Solution { public:/**思路:1.分析题意那么我们会有四种状态0 买入股票(或者说是之前就买入了股票但是也一直没有操作)1 卖出股票的状态一 两天前就卖出了股票 但是一直没有操作 2 卖出股票状态二 今天卖出股票3 冷冻期 持续一天2.动态规…

.NET Core 下使用 Exceptionless 记录日志

ExceptionLess是一套免费开源分布式系统日志收集框架&#xff0c;也是我无意中发现的&#xff0c;支持自己部署和平台托管的方式接入使用。ExceptionLess官网&#xff1a;https://exceptionless.comExceptionLess开源地址&#xff1a;https://github.com/exceptionless/Excepti…

你没有看错,爬网页数据,C# 也可以像 Jquery 那样

一&#xff1a;背景 1. 讲故事前段时间搞了一个地方性民生资讯号&#xff0c;资讯嘛&#xff0c;都是我抄你的&#xff0c;你抄官媒的&#xff0c;小市民都喜欢奇闻异事&#xff0c;所以就存在一个需求&#xff0c;如何去定向抓取奇闻异事的地方号上的新闻&#xff0c;其实做起…

leetcode300. 最长递增子序列

一:题目 二:上码 class Solution { public:/**思路:1.分析题意:我们在求取答案的过程中;我们的结果是动态的; 如果从某个数有一个递增序列 但是在这个数的后面又有一个数又可以是递增的 而且可能还比起长 2.动态规划五步走1>:确定dp数组的含义以及下标的含义dp[i] 表示的是…