二分算法详解(马智颖) #38
Mzying2001
started this conversation in
Algorithm
Replies: 0 comments
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
-
二分算法详解
如果看不到图片,看这里:点击跳转
目录:
算法简介
二分查找也称折半查找(Binary Search),它是一种效率较高的查找方法,前提是数据结构必须先排好序,可以在数据规模的对数时间复杂度内完成查找。但是,二分查找要求线性表具有有随机访问的特点(例如数组),也要求线性表能够根据中间元素的特点推测它两侧元素的性质,以达到缩减问题规模的效果。
算法思路
mid = (left + right) / 2target和arr[mid]做比较下图为运用二分算法在数组
[1, 3, 4, 6, 7, 8, 10, 13, 14]中查找数字4的过程图解。图片来源:百度百科
时间复杂度
由于二分查找每一次都会排除掉一半的值,在最坏的情况下即排除到最后只剩一个值,即
2^m=n,所以其时间复杂度为log2(n),即对数阶O(logn)。代码示例
以下以
std::vector为例,数组的话传个length参数就行了。递归版本
非递归版本
注意事项
二分算法的思路和代码很简单,基本上没什么好讲的,最可能出错的点就是循环条件
left <= right,注意是<=,而不是<,原因也很好理解,举个例子就知道了:比如我要通过二分查找数组
[1, 2]中的2,很明显,第一次循环后 left 和 right 都是1,即都指向数组中的第二个值2,此时显然还不能跳出循环,因为此时计算出 mid 也是1,所指向的就是要找的数,应返回 mid 而不是直接返回-1,由此则可以看出应该是<=,而不是<。还有一个点就是
mid的计算方式,最直接的就是mid = (left + right) / 2,但这样有一定的风险,就是当left和right很大时,直接相加有可能会溢出,即超出int的范围,在上面的代码中mid = left + (right - left) / 2就可以避免这种情况,虽然这种情况大多数时候是不会出现的,但这样写保险点。二分法假定解
前面讲得是最简单的二分搜索,但大多数题目可不会这么简单,二分法还有一个很重要的方法,就是运用二查找假定一个解并判断是否可行。一个简单的应用就是知道函数值求自变量的值,假设有一个单调增函数 f(x),现在要在区间 [left, right] 上求出函数值 y 对应的 x,运用二分的思想,我们可以前定义让
mid = (left + right) / 2,假设它就是要求的x,先求出 f(mid),与 y 比较,若 f(mid) < y,说明 mid 的值取小了,那么就让left = mid;同样,若 f(mid)>y 则应是right = mid,通过这样逐步缩减区间 [left, right],当 left 和 right 相差很小时就可以近似地认为这个 left 或者 right 就是要求的 x。代码示例:
结束条件
循环结束的条件就是 left 和 right 很接近,达到精度要求。那么就有以下两种思路:
以上两种均可达到目的,但如果使用思路1,有可能由于浮点数精度的问题导致死循环。
二分算法的应用
例题1、求平方根
代码如下:
题目来源:LeetCode
例题2、猜数字大小
代码如下:
题目来源:LeetCode
例题3、搜索旋转排序数组
代码如下:
题目来源:LeetCode
例题4、Cable master(POJ - 1064)
代码如下:
题目来源:POJ - 1064
发现问题
尽管上面的二分算法大部分能够满足我们的要求,但是当数组中有重复的元素时(注意看看前面的几道题都是没有重复的元素),返回值就不一定是我们想要的。
举个例子,当 arr =
[0, 1, 1, 1, 2],target =1时通过以上的代码所返回的值是2,但是在数组中下标为1、2、3的都是1,有时候我们需要拿到第一个出现的元素下标,亦或者是最后一个,那么前面的算法就明显不满足要求。暴力解法
最直接的解决方法就是在找到值后,让后再向左向右线性搜索,代码如下:
以上代码就解决了前面的问题,但是很明显,这样写存在一个很大的问题:数组中间可能有很多个与要找的数相等的元素,这样做就无法保证二分查找对数级的时间复杂度了。
缓存结果
实际上,只要在原来代码的基础上加一个变量
ans来缓存结果就行了,代码如下:这种方法很好理解,在找到 target 后没有立即返回下标,而是将结果存放在变量
ans中,然后继续进行二分,直到二分完整个数组。下面,我再介绍另外一种实现方法。
寻找左侧边界的二分搜索
代码如下:
这种方法其实也很好理解,可以类比前一种做法,这个其实就是直接用变量
right来保存结果。由于
right要用来保存结果,二分搜索的区间不应该包括这个right,即只在区间 [left, right - 1] 上进行二分搜索,因此循环条件就是left < right而不是left <= right,当找到目标元素时,就让 right 指向这个元素,继续向左进行二分搜索。在二分完整个数组后就只需要判断right指向的元素是否为想要找到的元素就行了。寻找右侧边界的二分搜索
代码如下:
思路和前面一个是一样的,这里就不再赘述了。
算法变式
插值查找算法
不要去看插值查找这个名字,实际上它的思想跟二分查找算法基本一样,只是对于中间值的获取策略不同而已。
在有序数组中查找值时,由于数据是排好序的,那么我们可以利用它大概的变化率来确定
mid的值,在一定的情况下能比直接进行二分相率更高。二分查找我们通过以下策略获取
mid值:在插值查找中,我们把策略改成:
(对于这个公式的理解可以参考我们初中学过的相似三角形)
算法代码如下(递归):
插值查找注意事项:
斐波那契查找算法(黄金分割法)
斐波那契查找原理与前两种相似,仅仅改变了中间节点
mid的获取策略,mid不再是中间或插值得到,而是位于黄金分割点附近。黄金分割点可以通过斐波那契数列获取近似值,即斐波那契的前后两项相除所得结果,越往后越趋近于黄金分割率。对于这个算法这里不多阐述,
个人感觉这就是一个玄学算法。代码如下:
三分算法
在前面的算法都要求所查找的数组或者函数都必须是单调增或者单调减的,当数组或者函数不是单调时(如二次函数),二分算法就无能为力了,因为在这种情况下,无法确定要找的点到底是在 mid 的左边还是右边,为了解决这种问题,这里介绍另外一种算法 —— 三分算法。
三分算法是用于寻找凹/凸函数(如二次函数)在某个区间的最值的算法,这种算法的思路跟二分法也很相似,顾名思义三分法就是将一个区间分成三个部分,通过中间的两个点的数值关系来逐步缩小区间,当区间足够小是,区间的边界就可以认为是函数的解。
三分算法的关键就是如何根据两个
mid来确定最值的位置,用文字不太容易描述,这里用图片列出了mid_l(靠左边的中间点)和mid_r(靠右边的中间点)与最值点位置的几种可能的情况情况:left = mid_lright = mid_rleft = mid_l或
right = mid_r表格的第三行表示可行的策略,我们需要判断
mid_l和mid_r分别所对应的函数值的关系,根据这个关系来排除掉一部分区间,从而逐步缩小区间。综合三种情况得知我们每次循环只需要排除掉与最值点较远的一个区间就行了(因为无法判断是上面的哪一种情况)。算法代码如下:
这段代码可以用于计算凹函数的最小值,同样的,计算凸函数的最大值也是用相同的方法,只需要改一下条件即可。
参考资料
https://leetcode-cn.com/tag/binary-search/
https://leetcode-cn.com/leetbook/detail/binary-search/
https://baike.baidu.com/item/%E4%BA%8C%E5%88%86%E6%90%9C%E7%B4%A2%E7%AE%97%E6%B3%95
https://www.cnblogs.com/kyoner/p/11080078.html
https://www.cnblogs.com/techflow/p/12131376.html
Beta Was this translation helpful? Give feedback.
All reactions