今天给大家带来的是二分查找及其变种的总结,大家一定要看到最后呀,非常非常用心的一篇文章,废话不多说,让导演帮我们把镜头切到袁记菜馆吧!
袁记菜馆内。。。。
店小二:掌柜的,您进货回来了呀,哟!今天您买这鱼挺大呀!
袁厨:那是,这是我今天从咱们江边买的,之前一直去菜市场买,那里的老贵了,你猜猜我今天买的多少钱一条。
店小二:之前的鱼,30个铜板一条,今天的我猜26个铜板。
袁厨:贵了。
店小二:还贵呀!那我猜20个铜板!
袁厨:还是贵了。
店小二:15个铜板。
袁厨:便宜了
店小二:18个铜板
袁厨:恭喜你猜对了
上面的例子就用到了我们的二分查找思想,如果你玩过类似的游戏,那二分查找理解起来肯定很轻松啦,下面我们一起征服二分查找吧!
完全有序
二分查找
二分查找也称折半查找(binary search),是一种在有序数组中查找某一特定元素的搜索算法。我们可以从定义可知,运用二分搜索的前提是数组必须是有序的,这里需要注意的是,我们的输入不一定是数组,也可以是数组中某一区间的起始位置和终止位置
通过上面二分查找的定义,我们知道了二分查找算法的作用及要求,那么该算法的具体执行过程是怎样的呢?
下面我们通过一个例子来帮助我们理解。我们需要在 nums 数组中,查询元素 8 的索引
(1)我们需要定义两个指针分别指向数组的头部及尾部,这是我们在整个数组中查询的情况,当我们在数组某一区间进行查询时,可以输入数组,起始位置,终止位置进行查询。
(2)找出mid,该索引为mid =(left + right)/ 2,但是这样写有可能溢出,所以我们需要改进一下写成mid = left +(right - left)/ 2 或者 left + ((right - left ) >> 1) 两者作用是一样的,都是为了找到两指针的中间索引,使用位运算的速度更快。那么此时的 mid = 0 + (8-0) / 2 = 4
(3)此时我们的 mid = 4,nums[mid] = 6 target,则需要移动我们的 right 指针, 则 right = mid - 1;则移动过后我们的 left 和 right 会重合,这里是我们的一个重点大家需要注意一下,后面会对此做详细叙述。
(6)我们需要在 left 和 right 之间继续计算 mid 值,则 mid = 5 +(5 - 5)/ 2 = 5 ,见下图,此时我们将 nums[mid] 和 target 比较,则发现两值相等,返回 mid 即可 ,如果不相等则跳出循环,返回 -1。
二分查找的执行过程如下
1.从已经排好序的数组或区间中,取出中间位置的元素,将其与我们的目标值进行比较,判断是否相等,如果相等则返回。
2.如果 nums[mid] 和 target 不相等,则对 nums[mid] 和 target 值进行比较大小,通过比较结果决定是从 mid的左半部分还是右半部分继续搜索。
如果 target > nums[mid] 则右半区间继续进行搜索,即 left = mid + 1; 若target < nums[mid] 则在左半区间继续进行搜索,即 right = mid -1;
动图解析
下面我们来看一下二分查找的代码,可以认真思考一下 if 语句的条件,每个都没有简写。
二分查找的思路及代码已经理解了,那么我们来看一下实现时容易出错的地方
1.计算 mid 时 ,不能使用 (left + right )/ 2,否则有可能会导致溢出
2. while (left < = right) 注意括号内为 left <= right ,而不是 left < right ,我们继续回顾刚才的例子,如果我们设置条件为 left < right 则当我们执行到最后一步时,则我们的 left 和 right 重叠时,则会跳出循环,返回 -1,区间内不存在该元素,但是不是这样的,我们的 left 和 right 此时指向的就是我们的目标元素 ,但是此时 left = right 跳出循环
3. left = mid + 1,right = mid - 1 而不是 left = mid 和 right = mid。我们思考一下这种情况,见下图,当我们的target 元素为 16 时,然后我们此时 left = 7 ,right = 8,mid = left + (right - left) = 7 + (8-7) = 7,那如果设置 left = mid 的话,则会进入死循环,mid 值一直为7 。
下面我们来看一下二分查找的递归写法
leetcode35搜索插入位置
题目描述
给定一个排序数组和一个目标值,在数组中找到目标值,并返回其索引。如果目标值不存在于数组中,返回它将会被按顺序插入的位置。
你可以假设数组中无重复元素。
示例 1:
输入: [1,3,5,6], 5 输出: 2
示例 2:
输入: [1,3,5,6], 2 输出: 1
示例 3:
输入: [1,3,5,6], 7 输出: 4
示例 4:
输入: [1,3,5,6], 0 输出: 0
题目解析
这个题目完全就和咱们的二分查找一样,只不过有了一点改写,那就是将咱们的返回值改成了 left,具体实现过程见下图
题目代码
查找元素第一个位置和最后一个位置
上面我们说了如何使用二分查找在数组或区间里查出特定值的索引位置。但是我们刚才数组里面都没有重复值,查到返回即可,那么我们思考一下下面这种情况
此时我们数组里含有多个 5 ,我们查询是否含有 5 可以很容易查到,但是我们想获取第一个 5 和 最后一个 5 的位置应该怎么实现呢?哦!我们可以使用遍历,当查询到第一个 5 时,我们设立一个指针进行定位,然后到达最后一个 5 时返回,这样我们就能求的第一个和最后一个五了?因为我们这个文章的主题就是二分查找,我们可不可以用二分查找来实现呢?当然是可以的。
leetcode 34在排序数组中查找元素的第一个和最后一个位置
题目描述
给定一个按照升序排列的整数数组 nums,和一个目标值 target。找出给定目标值在数组中的开始位置和结束位置。
如果数组中不存在目标值 target,返回 [-1, -1]。
示例 1:
输入:nums = [5,7,7,8,8,10], target = 8 输出:[3,4]
示例 2:
输入:nums = [5,7,7,8,8,10], target = 6 输出:[-1,-1]
示例 3:
输入:nums = [], target = 0 输出:[-1,-1]
题目解析
这个题目很容易理解,我们在上面说了如何使用遍历解决该题,但是这个题目的目的就是让我们使用二分查找,我们来逐个分析,先找出目标元素的下边界,那么我们如何找到目标元素的下边界呢?
我们来重点分析一下刚才二分查找中的这段代码
我们只需在这段代码中修改即可,我们再来剖析一下这块代码,nums[mid] == target 时则返回,nums[mid] target时则移动右指针,在左区间内进行查找。
那么我们思考一下,如果此时我们的 nums[mid] = target ,但是我们不能确定 mid 是否为该目标数的左边界,所以此时我们不可以返回下标。例如下面这种情况。
此时 mid = 4 ,nums[mid] = 5,但是此时的 mid 指向的并不是第一个 5,所以我们需要继续查找 ,因为我们要找的是数的下边界,所以我们需要在 mid 的值的左区间继续寻找 5 ,那我们应该怎么做呢?
我们只需在target <= nums[mid] 时,让 right = mid - 1即可,这样我们就可以继续在 mid 的左区间继续找 5 。是不是听着有点绕,我们通过下面这组图进行描述。
其实原理很简单,就是我们将小于和等于合并在一起处理,当 target = nums[left] 时,说明他俩落在一个数组里了,如果 nums[mid] nums[left] , 如果 nums[mid] < nums[left] ,且 mid 在 left 的后面,则说明 left 在数组1,mid 在数组2。
那么当left 和 mid 落在不同数组时,为什么不能是 left 在 数组2 ,mid 在 数组1 呢?
因为咱们的 mid 是通过 left 和 right 的下标求得,所以 mid 应该在 left 和 right 中间
如果我们的 mid 和 left 在同一个数组内时?咱们的 target 会有几种情况呢?
见下图
无非也是两种情况,用我们上面的例子来说,
1. 落在 mid 的左边,target = 5 ,当前例子中 情况是落在[4,7)区间内,即 4 <= target = nums[left] && target < nums[mid],此时我们让 right = mid -1,让 left 和 right 都落到数组 1 中,下次查找我们就是在数组1中进行了,完全有序,
2. 落在 mid 的右边,target = 1 , target = 8 ,此时例子中 target 不落在 [4,7)区间内,那就 target = 8或 0 <= target nums[mid] || target nums[right] || target < nums[mid] 这里就是和上面的第二种情况对应,落在 mid 的左半部分,我们尽量将两个指针赶到一个完全有序的区间内
通过上面的例子大家应该对该变种有所了解了,下面我们一起来做一下 leetcode 33 题吧。
leetcode33搜索旋转排序数组
题目描述
给你一个整数数组 nums ,和一个整数 target 。
该整数数组原本是按升序排列,但输入时在预先未知的某个点上进行了旋转。(例如,数组 [0,1,2,4,5,6,7] 可能变为 [4,5,6,7,0,1,2] )。
请你在数组中搜索 target ,如果数组中存在这个目标值,则返回它的索引,否则返回 -1 。
示例 1:
输入:nums = [4,5,6,7,0,1,2], target = 0 输出:4
示例 2:
输入:nums = [4,5,6,7,0,1,2], target = 3 输出:-1
示例 3:
输入:nums = [1], target = 0 输出:-1
题目解析
这个题目的解答方法,咱们在上面已经有所描述,下面我们来看一下下面这个例子的代码执行过程吧.大家可以结合代码和动图进行理解,代码中所有语句都没有进行简写,可以仔细阅读判断条件,很容易理解。
输入 nums = [4,5,6,7,8,0,1,2] target = 8
动图解析
下面我们看题目代码吧,如果还没有完全理解的同学,可以仔细阅读 if ,else if 里面的语句,(没有简写)还有文中注释,一定可以整透的。
题目代码
查找目标元素(含重复元素)我们通过刚才的例子了解了,如果在不完全有序的数组中查找目标元素,但是我们的不完全有序数组中是不包含重复元素的,那如果我们的数组中包含重复元素我们应该怎么做呢?见下图
如上图,如果我们继续使用刚才的代码,则会报错这是为什么呢?我们来分析一下。
所以我们需要对其进行改进,我们只需将重复元素去掉即可,当我们的 nums[left] == nums[mid] 时,让 left ++ 即可,比如 1,3,1,1,1此时 nums[mid] == nums[left] 则 left ++,那我们此时会不会错过目标值呢?其实并不会,只是去掉了某些重复元素,如果此时我们的目标元素是3,则我们left++,之后情况就变为了上题中的情况。 leetcode81. 搜索旋转排序数组 ii 题目描述
假设按照升序排序的数组在预先未知的某个点上进行了旋转。
( 例如,数组 [0,0,1,2,2,5,6] 可能变为 [2,5,6,0,0,1,2] )。
编写一个函数来判断给定的目标值是否存在于数组中。若存在返回 true,否则返回 false。
示例 1:
输入:nums = [2,5,6,0,0,1,2], target = 0 输出:true
示例 2:
输入:nums = [2,5,6,0,0,1,2], target = 3 输出:false
题目解析这个题目就比刚才的不含重复元素的题目多了一个去除某些重复元素的情况,当 nums[mid] == nums[left] 时,让 left++,并退出本次循环,其余部分完全相同,大家可以结合代码和图片进行理解。题目代码
寻找最小值 这种情况也很容易处理,和咱们的leetcode33搜索旋转排序数组,题目类似,只不过一个需要搜索目标元素,一个搜索最小值,我们搜索目标元素很容易处理,但是我们搜索最小值应该怎么整呢?见下图
我们需要在一个旋转数组中,查找其中的最小值,如果我们数组是完全有序的很容易,我们只需要返回第一个元素即可,但是此时我们是旋转过的数组。 我们需要考虑以下情况
我们见上图,我们需要考虑的情况是
数组完全有序 nums[left] > 1)此时 mid = 4 ,但是我们的二维矩阵下标最大是,nums[2,2]呀,你这求了一个 4 ,让我们怎么整呀。
如果我们理解了二分查找,那么这个题目考察我们的应该是如何将一维数组的下标,变为 二维坐标。其实也很简单,咱们看哈,此时咱们的 mid = 4,咱们的二维矩阵共有 3行, 3列,那我们 mid =4,肯定在第二行,那么这个应该怎么求得呢?
我们可以直接用 (mid / 列数),即可,因为我们 mid = 4,4 /3 = 1,说明在第二行,那如果 mid = 7 ,7/3=2,在第三行,我们第几行知道了,那么我们如何知道第几列呢?我们可以直接根据 (mid % 列数 )来求得呀,比如我们此时 mid = 7,7%3 = 1,那么在我们一维数组索引为 7 的元素(也就是10),其处于二维数组的第 3 行第 2 列,大家看看下图是不是呀!
下面我们来看一下 leetcode 74题,让我们给他整个通透
leetcode74搜索二维矩阵(中等)
题目描述
编写一个高效的算法来判断 m x n 矩阵中,是否存在一个目标值。该矩阵具有如下特性:
每行中的整数从左到右按升序排列。每行的第一个整数大于前一行的最后一个整数。
示例1
输入:matrix = [[1,3,5,7],[10,11,16,20],[23,30,34,50]], target = 3 输出:true
示例2
输入:matrix = [[1,3,5,7],[10,11,16,20],[23,30,34,50]], target = 13 输出:false
示例3
输入:matrix = [], target = 0 输出:false
题目解析
在上面我们已经解释了如何在二维矩阵中进行搜索,这里我们再对其进行一个总结,就是我们凭空想象一个一维数组,这个数组是有二维数组一层一层拼接来的,也是完全有序,然后我们定义两个指针一个指向一维数组头部,一个指向尾部,我们求得 mid 值然后将 mid 变成二维坐标,然后和 target 进行比较,如果大于则移动 left ,如果小于则移动 right 。
动图解析
题目代码
好啦 ,咱们的二分查找及其变种到这里就结束啦,希望通过这篇文章能让大家掌握二分查找及其变种
原文标题:穿了好几个马甲,差点没认出来是二分查找
文章出处:【微信公众号:算法与数据结构】欢迎添加关注!文章转载请注明出处。
iphone13mini值得买吗
澳大利亚1.2GW森林风电项目获批,首阶段预计将于2023年底投入运营
全国半导体物理学术会议在浙江杭州成功举行
硬件比拼放缓:大牌厂商等何去何从?
单片机最小系统的组成部分 使用proteus软件搭建最小单片机系统的操作方法
二分查找及其变种的总结
芯片设计:美国领先地位明显,中国升至第三
华为芯片大动作:英国投资3亿圈地剑桥,紧邻ARM自建工厂
如何挑选一款高素质的智能门锁,让TCL来告诉你
百度大脑发智能对话引擎推出客服、消费电子及出行三大方案
ODCC 2023 | 华为数据中心网络荣膺2023开放数据中心大会三项大奖
光合作用测定仪的应用有哪些
富士康或向苹果秘密交付iPad 3(图文)
智能汽车毫米波雷达电磁抗扰度测试方法研究
降低电容的音频噪声
Ulefone Power 5——真正的续航怪兽
贵州关岭自治县人民政府与广州大秦智能电网公司签订了战略合作协议
数字经济已成为车联网规模爆发的新引擎
为什么那么多智能家居产品,只有智能门锁是唯一火起来的行业?
AMD锐龙Threadripper 3990X处理器即将来临