《代码之美》读书笔记之二分查找算法

http://fallenlord.blogbus.com/logs/61059167.html


最近终于可以闲下来看看《代码之美》了 ,一年半之前在第三极第一次看见这本书就爱不释手,两个月前从宁海那里借来,现在准备开始看了

随手翻开,第七章 漂亮的测试,Alberto Savoia,测试的目标是二分查找算法

很简单的算法,相信计算机相关专业的童鞋应该早在读书的时候就已经烂熟于胸了,我虽然第一次接触二分查找法是在初中二年级,但第一次真正自己手写已经是10年后在广州的事情了。隐约记得后来发现有bug,改了一次……

“Jon Bentley在他的《Programming Pearls》一书中,记述了他在多年的时间里先后让上百位专业程序员实现二分查找法,而且每次都是在他给出算法的基本描述后,他很慷慨,每次给他们两个小时来实现它,而且允许他们使用他们自己选择的高级语言(包括伪代码)。令人惊讶的是,大约只有10%的专业程序员正确的实现了二分查找法。”

 

“Donald Knuth在他的《Sorting and Searching》一书中指出,尽管第一次二分查找算法早在1946年就被发表,但第一个没有bug的二分查找算法却是十二年后才被发表出来”

看了以上两段书中的描述后,我关上了书,决定先自己写一个二分查找算法试试,再接着往下看,于是有了下面一段程序:

public static int binarySearch(int[] arr, int target) {
    int l = 0;
    int r = arr.length - 1;
    while (l <= r) {
        int h = (l + r) >>> 1;
        if (target > arr[h]) {
            l = h + 1;
        } else if (target < arr[h]) {
            r = h - 1;
        } else {
            return target;
        }
    }
 
    return -1;
}

想了一会儿,不觉得自己哪里有bug,翻开书接着看。原来是计算中间值时的溢出问题:

 

int h = (r + l) / 2;  // 如果 (r + l) > Integer.MAX_INTEGER会产生溢出(Java是2^31 - 1)

所以应该写成下面的方式:

int h = l + ((r - l) / 2);

而我写的时候为了效率使用了位操作,这同样的避免了溢出问题:

int h = (r + l) >>> 1;

我的解答是作者给出的最佳解(一般实现中),歪打正着-_-#

以上的溢出问题是Joshua Bloch 2006年在他的blog中指出的:http://googleresearch.blogspot.com/2006/06/extra-extra-read-all-about-it-nearly.html

到了这里本来告一段落了,第七章之后都在描述如何给予JUnit对这个实现进行测试,也写得很不错。但随后我发现书中提及二分查找的远不止这一处。

第四章,查找,Tim Bray,查找的内容是作者博客的access_log

前面讲正则表达式,并用Ruby和Java分别使用Map实现了快速查找——Map加载性能和资源消耗都太大,或许可以试试Array+二分查找,于是我看到了下面一段程序(同时来自于Gaston Gonnet):

public static int binarySearchAnother(String[] keys, String target) {
    int high = keys.length;
    int low = -1;
    while (high - low > 1) {
        int probe = (low + high) >>> 1;
        if (keys[probe].compareTo(target) > 0)
            high = probe;
        else
            low = probe;
    }
    if (low == -1 || keys[low].compareTo(target) != 0)
        return -1;
    else
        return low;
}

看起来有些奇怪,它的循环体里面没有判断目标是否被找到的代码!还有什么?

1. low和high初始化时都不是一个有效的索引值,这消除了所有边界判断问题
2. 同时提到了之前的溢出问题,这里用位操作解决
3. 最后当循环执行完成后再进行判断目标是否被找到

“在看了上面的二分查找算法的实现后,有人可能会问,为什么我们要将算法中的循环运行到结束处,而不是在检测到了目标值的时候就退出循环呢?实际上,上述代码的行为才是正确的行为;虽说对于该行为的正确性数学证明已经超出了本章的范围,但只要经过一些简单的思考,我们就应该可以从直觉中获得这个结果——从以往共同工作过的许多伟大的程序员中,我不止一次地发现过他们的这种直觉

我们先来考虑循环的执行步骤。假设我们有一个有着n个元素的数组(此处n是一个很大的数值),那么从该数组中第一次找到目标的概率为1/n(一个很小的数值),下一次(经过一次二分)的概率则是1/(n/2)——仍然不是很大——以此类推下去。事实上,只有当元素的个数减少到了10到20的时候,一次找到目标的概率才变得有意义,而对于10到20个元素进行查找需要的只是大概4次循环。当查找失败时(在大多数的应用中很普遍),那些额外的测试就将变成纯粹的额外开销。

我们也可以来计算一下,在什么时候找到目标值的概率能接近50%,但请你扪心自问:在一个复杂度为O(log2N)的算法中,对于它的每一步都增添一个额外的复杂计算,而目的仅仅是为了减少最后的几次计算,这样做有意义吗?”

以上是作者的解释,已经相当的清楚了。

最后

运气很背,翻开这书正好看到的两章都是讲二分查找的,想了想就当一篇笔记写下来了,看看之后若还有相关的描述再更新这里


历史上的今天:


收藏到: Del.icio.us















  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值