关于二分搜索的写法以及正确性证明的思考
2011-08-18 23:52
357 查看
今晚看到算法引论关于二分搜索的相关问题,想起了当年看编程珠玑的“无处不在的二分搜索”那章,记得作者说过能完全写对二分搜索的程序员寥寥无几,当时自己也写了下,确实不容易写,主要的难点在于写对,大致的框架可能大家都非常熟悉,但是里面的下标怎么确定是正确的呢?不对的下标很有可能造成死循环。不过,算法引论所推崇的数学归纳法的思想还是很普适的,反应在程序上就是先写n=1的情况,再写归纳阶段的代码,这样的方法用在写二分搜索感觉很有效,例如书中最普通的二分搜索代码如下:
这里对程序稍微做了点修改,但是大意是一样的,可以看到,程序书写的逻辑上,先处理n=1的情况,即l==r,这时候只有1个元素,对于1个元素的判断是trivial的,然后在else里面,算出middle并递归判断,这里是上取整,那么为什么要上取整?
要明白这个问题就是要理解,如果下取整会出现什么情况?死循环!让我们来验证下,假如某个时刻l=r-1,且A[l]<=z,这个时候就是死循环的时候,因为每次middle都会等于l(下取整)而A[l]<=z所以会走else那个分支,又继续递归l(此时middle是等于l的)和r,从而一直死循环下去。
仔细分析上面的情况不难发现,关键的地方在middle是下取整的情况,如果下取整会出现一个关键的问题就是l有可能等于middle,我们如果抓住这个问题去分析,就会很容易发现在else分支会出现死循环。下面用同样的方法分析下上取整,如果上取整的话,middle则可能会等于r,如果A[middle]>z,会直接导致r等于l,即在下次进入n=1的判断;如果A[middle]<=z,则同样会有middle等于r进入n=1的分支,这也证明了这个程序一定会在n=1的时候退出。其实在下取整和上取整发生不同的地方就是临界的位置,这也是容易造成死循环的时候。
通过以上分析,我们总结下如何快速判断一个二分搜索程序是否会出现死循环:
查看middle的选取是那种取整类型,是上取整还是下取整
根据类型,假定临界条件的发生,对上取整就是middle等于r了,对下取整就是middle等于l了
用临界的情况去检验if的两个分支,看看会不会出现循环调用,如果会则一定会有死循环,否则可大致判断程序是正确的
其实,编程珠玑中也介绍了程序验证学的方法,即assert,这个方法也是很好的一种方法,特别的是写短小程序的时候。
用这个方法,我发现图6.3中,二叉搜索的特殊下标问题中的程序是错误的,即middle的取法应该是下取整,书上是上取整,我用程序跑书的例子,果然华丽的堆栈溢出了,看来这种方法还是挺有作用的,不知道有人和我有同样的疑问吗,勘误上并没有说程序的问题。
[Update] 最近做Leetjob 的online judegment又遇到类似的,问题,继续总结,二分查找可以在有序数组中找任意满足某些特定条件的一个数,先根据条件,确定middle比较的形式,之后就可以确定是用上取整还是下取整,上取整,上限j必须要变,下取整,下限i必须要变,例如第一个找某个数x使得x>=target,那么我们就要写A[middle]<target,i=middle+1。例子可见https://oj.leetcode.com/problems/search-for-a-range/
(完)
int BinarySearch(int* A, int l, int r, int z) { if(l == r) { if(A[l] == z) return l; else return -1; } else { int middle = (l + r + 1)/2; if(z < A[middle]) { BinarySearch(A,l, middle-1,z); } else { BinarySearch(A,middle,r,z); } } }
这里对程序稍微做了点修改,但是大意是一样的,可以看到,程序书写的逻辑上,先处理n=1的情况,即l==r,这时候只有1个元素,对于1个元素的判断是trivial的,然后在else里面,算出middle并递归判断,这里是上取整,那么为什么要上取整?
要明白这个问题就是要理解,如果下取整会出现什么情况?死循环!让我们来验证下,假如某个时刻l=r-1,且A[l]<=z,这个时候就是死循环的时候,因为每次middle都会等于l(下取整)而A[l]<=z所以会走else那个分支,又继续递归l(此时middle是等于l的)和r,从而一直死循环下去。
仔细分析上面的情况不难发现,关键的地方在middle是下取整的情况,如果下取整会出现一个关键的问题就是l有可能等于middle,我们如果抓住这个问题去分析,就会很容易发现在else分支会出现死循环。下面用同样的方法分析下上取整,如果上取整的话,middle则可能会等于r,如果A[middle]>z,会直接导致r等于l,即在下次进入n=1的判断;如果A[middle]<=z,则同样会有middle等于r进入n=1的分支,这也证明了这个程序一定会在n=1的时候退出。其实在下取整和上取整发生不同的地方就是临界的位置,这也是容易造成死循环的时候。
通过以上分析,我们总结下如何快速判断一个二分搜索程序是否会出现死循环:
查看middle的选取是那种取整类型,是上取整还是下取整
根据类型,假定临界条件的发生,对上取整就是middle等于r了,对下取整就是middle等于l了
用临界的情况去检验if的两个分支,看看会不会出现循环调用,如果会则一定会有死循环,否则可大致判断程序是正确的
其实,编程珠玑中也介绍了程序验证学的方法,即assert,这个方法也是很好的一种方法,特别的是写短小程序的时候。
用这个方法,我发现图6.3中,二叉搜索的特殊下标问题中的程序是错误的,即middle的取法应该是下取整,书上是上取整,我用程序跑书的例子,果然华丽的堆栈溢出了,看来这种方法还是挺有作用的,不知道有人和我有同样的疑问吗,勘误上并没有说程序的问题。
[Update] 最近做Leetjob 的online judegment又遇到类似的,问题,继续总结,二分查找可以在有序数组中找任意满足某些特定条件的一个数,先根据条件,确定middle比较的形式,之后就可以确定是用上取整还是下取整,上取整,上限j必须要变,下取整,下限i必须要变,例如第一个找某个数x使得x>=target,那么我们就要写A[middle]<target,i=middle+1。例子可见https://oj.leetcode.com/problems/search-for-a-range/
(完)
相关文章推荐
- POJ 1744 Elevator Stopping Plan 以及对二分搜索的思考
- 关于二分搜索的一点思考
- poj3273 二分,以及关于二分的一些思考
- 关于测试工具以及前端性能测试的一些思考
- 关于cron和crond,crontab以及bash脚本的写法等知识 cron的语法用法
- 面试杂题(三)有序数组中连续k的个数(含二分搜索的递归非递归写法)
- 关于自定义SeekBar写法以及seekbar不显示的问题
- 关于工作后定居城市的思考以及房价…
- 关于dijkstra的贪心思想的正确性的证明
- python实现二分搜索树以及改进后的AVL Tree
- 做第一个小项目的一点思考(关于备份文件的妙用以及误删文件解决办法)
- 关于静态方法为什么不能被重写的一点思考以及overload的一些坑
- 二分查找之美:二分查找及其变体的正确性以及构造方式
- 关于HE-AAC中 QMF的设计以及对时频分析的思考
- 关于Qt的事件循环以及状态机事件循环的思考
- 关于动态添加TabPanel遇到的问题以及思考
- 关于Google中国,百度搜索,Bing搜索,雅虎,搜搜,以及有道的简单测试
- (转载)如何写出正确的二分查找?——利用循环不变式理解二分查找及其变体的正确性以及构造方式
- log4j学习笔记(一)——slf4j以及log4j引发的关于java日志的思考
- 阿里集团搜索和推荐关于效率&稳定性的思考和实践