算法浅谈——人人皆知却很多人写不对的二分法

本文始发于个人公众号:TechFlow


  1  
二分法可以说是鼎鼎大名,哪怕是没有学过编程的同学,也许说不上来二分法这个名字,但是对于其中的精髓应该都是有所了解的。不了解的同学也没关系,我一句话就能交代清楚:我们每次将一个集合一分为二,每次舍弃其中一半。
早在两千多年前,庄子就搞清楚了二分法的精髓,他说:一尺之棰,日取其半,万世不竭。从这个角度来说,二分法可能是这个世界上最古老的算法之一了。
二分法不仅古老,而且在计算机系统当中非常常见,许多系统当中都用到了二分法的思想。除此之外,在面试的时候,二分法的算法题也是常客。因为二分法本身不复杂,几乎人人都会,但是对二分法的使用能力却各有不同。出二分法的题,可以真实考察面试者的算法能力和编程功底。
不说比较困难的算法题想不出思路,就说最简单没有任何难度的纯二分,在面试的时候,出错的写出bug的也大有人在。
很多人会觉得奇怪,二分法这么简单的算法,真的有人写不出来吗?
还真的有,原因也很简单,恰恰就是二分法太简单了。
无论是在算法导论还是在一些其他的算法教材当中,关于二分法的描述都不多,详细的会有一些图例展示一下二分法的思想,简单的就用几句话描述一下原理,最后再展示一下代码,就完事了。读者在学的时候也是一样,看了一眼原理,哦,非常简单,再看一眼代码,只有三四行,差不多一眼就能记住,那就丢在一边吧。到了真正上手的时候,问题一下就暴露了出来。

二分法最常见的问题有两个,一个是二分的区间边界不清楚,另一个是二分查找的结果不明确。我想,这两个问题是前几次实现二分法的时候,一定会遇到的。遗憾的是,目前的教材当中对于这两个问题介绍都不多,都只有代码,留给读者自行揣摩。

  2  
我们先说第一个问题——边界
早在小学我们就学过,用l表示区间左边界,r表示区间右边界,mid=(l + r) / 2表示二分的中间点。这个在数学里非常明确,但在编程的时候,有一个隐藏的问题被忽略了。究竟这个区间是闭区间呢,还是开区间呢,还是半开半闭区间或者是半闭半开区间?如果这个问题不想清楚,想要一次性写出没有bug的代码,老实说很不容易。
首先,二分终止的条件究竟怎么写,是while (l < r) 还是 while (l <= r) 还是别的?还有,在搜索的时候,我们究竟要不要将a[mid] == v的情况单独判断?我们是判断a[mid] < v还是a[mid] <= v?假设我们选择用a[mid] <= v,得到的结果为true。我们知道答案应该在区间的右半边,我们需要舍弃左边的区间。应该对l赋值,但是我们是赋值成l = m呢还是l=m + 1呢?又是为什么呢?
你看,如果l和r表示的区间不考虑清楚,我们在实际写代码的时候就会遇到这样棘手的问题。坑爹的是,当我们为这些边界头疼的时候,我们并不能意识到这是因为我们没有搞清楚表示区间的方法导致的。往往会觉得是自己不够熟悉。
显然,要解决这个问题需要确定l和r表示的区间种类。那么到底应该选择什么区间呢?是左闭右开,还是全闭,还是左开右闭呢?
答案有点出人意料,都行
理论上来说,不论选什么样的区间,只要代码得当,都是可以的,可以说是完全看个人喜好。不过我个人推荐左闭右开,原因很简单,这个和编程当中的数组定义的情况一致。我们都知道,在代码的世界里,数组是从0开始的,一个长度为10的数组,最后一个元素的下标是9。如果使用左闭右开区间,我们将l=0,r=数组长度,就完成了初始化,如果用闭区间,r=长度-1,不免显得有些多余。
假设我们确定了使用左闭右开区间,我们再来看前面说的两个问题。
区间确定了,终止条件也就明确了,左闭右开区间[l, r)不为空的话,r 至少大于等于l + 1。我们要在区间长度大于1的时候执行二分,所以二分的循环条件应该是while (l + 1 < r)。

  3  
那么while里的判断条件呢?
我们列举一下,a[mid] 和v的大小关系无非只有三种。
第一种a[mid] = v,很简单,mid就是我们要查找的结果,直接返回。
第二种a[mid] < v,说明我们应该取右边的区间,由于l的位置可以取到,而mid已经不是答案了,所以l = mid + 1。
第三种a[mid] > v,应该取左边的区间,mid不是答案,但是由于r指向的位置本身就不在候选区间里,所以r = mid,而不是mid-1,因为mid-1可能是答案,而r处的位置是取不到的。
到这里,似乎一切完美,我们可以很顺利地写出代码了。但是还没有结束,依然还有一个小问题。

前文说了,a[mid]和v的关系有三种,当a[mid] = v的时候,我们就找到了答案。从这个角度来看,我们二分的时候,通过l和r缩小区间的范围,通过mid来寻找答案。但是既然我们已经折半区间的大小了,那么当区间长度为1的时候,剩下的就是答案,我们为什么还需要通过mid去查找答案呢?如果我们就想通过区间本身来查找答案,那么应该怎么办呢?
也不难,我们需要把a[mid]小于和等于v的两种情况合并,由于a[mid]可能等于v,所以我们不能跳过mid这个位置,l = mid + 1 应该写成l = mid,于是整个代码也就出来了:

  1. def binary_search(a, v):
  2. l, r = 0, len(a)
  3. while l + 1 < r:
  4. m = (l + r) // 2
  5. if a[m] <= v:
  6. l = m
  7. else:
  8. r = m
  9. # 通过a[l] == v判断v不存在与a数组当中的情况
  10. return l

  4  
可能会有同学好奇,如果我不使用左闭右开,而使用闭区间呢,代码又该怎么写?

其实只要把区间想清楚了,写出来也不难。

  1. def binary_search(a, v):
  2. l, r = 0, len(a) - 1
  3. while l <= r:
  4. m = (l + r) // 2
  5. if a[m] == v:
  6. return m
  7. if a[m] < v:
  8. l = m + 1
  9. else:
  10. r = m - 1
  11. # 表示不存在
  12. return -1

不过还有一个小问题,为什么闭区间形式的二分法的判断推荐是while (l <= r)呢?换成while (l < r)行不行?这个问题就留给大家思考。
二分法虽然简单,但这些细节都理解清楚也并不容易,在算法领域当中,如果细节没有理解到位,阴沟里翻船是非常平常的事情。希望今天的文章能对大家有所帮助。

扫码关注,获取最新文章

原文地址:https://www.cnblogs.com/techflow/p/12101310.html

时间: 2024-11-29 07:30:57

算法浅谈——人人皆知却很多人写不对的二分法的相关文章

Kmp算法浅谈

Kmp算法浅谈 一.Kmp算法思想 在主串和模式串进行匹配时,利用next数组不改变主串的匹配指针而是改变模式串的匹配指针,减少大量的重复匹配时间.在Kmp算法中,next数组的构建是整个Kmp算法的核心所在. 二.Kmp核心之next数组的构建 (1)前缀,后缀的定义 (2)最长公共前后缀定义 (3)next数组的含义 next数组代表各个长度子串(这些字串的起始位置都为0)的最长公共前后缀长度,为了方便代码的编写next[0]定为-1,表示前0个字符根本不存在前后缀这一说法.其他的例如nex

算法浅谈——递归算法与海盗分金问题

本文始发于个人公众号:TechFlow 最近看到一道很有意思的问题,分享给大家. 还是老规矩,在我们聊算法问题之前,先来看一个故事. 传说中,有5个海盗组成了一支无敌的海盗舰队,他们在最后一次的寻宝当中找寻到了100枚价值连城的金币.于是,很自然的,这群海盗面临分赃的问题.为了防止海盗内讧,残忍的海盗们制定了一个奇怪的规则: 他们决定按照功劳大小对五个人进行编号,由编号小的海盗先提出分配方案.如果方案能够得到大多数人的同意,那么就按照他提出的方案进行分配.如果不能通过,说明他已经失去了威望,海盗

算法浅谈——分治算法与归并、快速排序(附代码和动图演示)

在之前的文章当中,我们通过海盗分金币问题详细讲解了递归方法. 我们可以认为在递归的过程当中,我们通过函数自己调用自己,将大问题转化成了小问题,因此简化了编码以及建模.今天这篇文章呢,就正式和大家聊一聊将大问题简化成小问题的分治算法的经典使用场景--排序. 排序算法 排序算法有很多,很多博文都有总结,号称有十大经典的排序算法.我们信手拈来就可以说上来很多,比如插入排序.选择排序.桶排序.希尔排序.快速排序.归并排序等等.老实讲这么多排序算法,但我们实际工作中并不会用到那么多,凡是高级语言都有自带的

Pollard Rho算法浅谈

Pollard Rho介绍 Pollard Rho算法是Pollard[1]在1975年[2]发明的一种将大整数因数分解的算法 其中Pollard来源于发明者Pollard的姓,Rho则来自内部伪随机算法固有的循环 Pollard Rho算法在其他因数分解算法[3]中不算太出众,但其空间复杂度Θ(1)的优势和好打的代码使得OIer更倾向于使用Pollard Rho算法 毕竟试除法太慢了,谁没事打Pollard Rho不打试除法 Pollard Rho原理 生日悖论 如果一年只有365天(不计算闰

模拟退火算法浅谈

模拟退火算法(Simulate Anneal,SA)是一种通用概率演算法,用来在一个大的搜寻空间内找寻命题的最优解.模拟退火是由S.Kirkpatrick, C.D.Gelatt和M.P.Vecchi在1983年所发明的.V.?erný在1985年也独立发明此演算法.模拟退火算法是解决TSP问题的有效方法之一. 模拟退火的出发点是基于物理中固体物质的退火过程与一般组合优化问题之间的相似性.模拟退火算法是一种通用的优化算法,其物理退火过程由加温过程.等温过程.冷却过程这三部分组成. ——百度百科

浅谈算法,一些感悟(1)

最近看到好几个同学在学算法,看了一些书,另外跟一个算法较好的同学讨论了一下,若有所悟,作此文,以求各位大神指教: 现在看到好多同学学算法,可是,事实上看起来,真正明白理解了算法是一种什么东西的极少,很多都是为了参加ACM而去学算法,并没有对算法有真正意义上的研究,甚至说,他们拿到了ACM的入场券的时候还不知道算法是什么,我感到很惋惜,从我个人的理解来说,算法其实一直都在我们身边,它就是一切可以通过逻辑解释的活动的一整个过程,这样说可能有点泛泛而谈了,总的来说,算法就是一个抽象的流程:而且在我看来

浅谈算法和数据结构系列汇总(转)

突然看到一个大神的系列文章讲的就是算法和数据结构,现在把它的文章集中分享给大家,向大神致敬: 浅谈算法和数据结构: 一 栈和队列 浅谈算法和数据结构: 二 基本排序算法 浅谈算法和数据结构: 三 合并排序 浅谈算法和数据结构: 四 快速排序 浅谈算法和数据结构: 五 优先级队列与堆排序 浅谈算法和数据结构: 六 符号表及其基本实现 浅谈算法和数据结构: 七 二叉查找树 浅谈算法和数据结构: 八 平衡查找树之2-3树 浅谈算法和数据结构: 九 平衡查找树之红黑树 浅谈算法和数据结构: 十 平衡查找

【公众号系列】浅谈SAP项目管理的技能

公众号:SAP Technical 本文作者:matinal 原文出处:http://www.cnblogs.com/SAPmatinal/ 原文链接:[[公众号系列]浅谈SAP项目管理的技能 写在前面 在SAP领域里,职业发展中一个比较普遍的方向就是项目经理,无论你是做业务顾问还是开发顾问,更或者是做售前,做技术和做项目经理有很大的不同,做技术只需要处理问题,而项目经理却需要面对的是人和事,需要具备更多的知识和技能才能够胜任. 项目经理是整个项目的负责人,一个项目的成功与否和项目管理有直接的关

浅谈iOS中的RunLoop

首先解释下为什么是浅谈,主要是RunLoop这个东西不单单是iOS的范畴,还涉及到操作系统,我指的浅谈仅仅针对ios上层应用,底层的东西概不涉及 ,所以只能浅谈浅谈了. 在浅谈RunLoop之前我们来写个小demo,超级简单,一个按钮,然后给按钮一个断点 这块标记了1,2,3,4 红色的字:其实这是这个APP启动的一个过程 但是说好了浅谈RunLoop为啥又扯到APP的启动了 ? 先不要在意这些细节... 我先来解释下我标出的1,2,3,4分别是啥东西 1,dyld :这是啥子鬼了? the d