http://fallenlord.blogbus.com/logs/61059167.html
最近终于可以闲下来看看《代码之美》了 ,一年半之前在第三极第一次看见这本书就爱不释手,两个月前从宁海那里借来,现在准备开始看了
随手翻开,第七章 漂亮的测试,Alberto Savoia,测试的目标是二分查找算法
很简单的算法,相信计算机相关专业的童鞋应该早在读书的时候就已经烂熟于胸了,我虽然第一次接触二分查找法是在初中二年级,但第一次真正自己手写已经是10年后在广州的事情了。隐约记得后来发现有bug,改了一次……
看了以上两段书中的描述后,我关上了书,决定先自己写一个二分查找算法试试,再接着往下看,于是有了下面一段程序:
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,翻开书接着看。原来是计算中间值时的溢出问题:
所以应该写成下面的方式:
而我写的时候为了效率使用了位操作,这同样的避免了溢出问题:
我的解答是作者给出的最佳解(一般实现中),歪打正着-_-#
以上的溢出问题是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):
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