还能不能正确的二分搜索了?

一. 几个比较犯二的错误:

二分搜索貌似很容易,但是想写正确了还真不容易啊!我之前就犯过很多错误!作为算法方面的屌丝,现在把我曾经的错误的想法和写法都一一写出来!

实际上,犯错误不可能一次只犯一个,但为了方面讨论,就一个情况只讨论写一个错误吧,写这些例子代码时,我尽量考虑不出现其他错误,但是真的也保不齐啊!

先定义题目 在已经递增排序的整型数组A[nlen] 里查找 一个整型数 key,如果key 在A里,返回key所在的数组下标,否则,返回-1. 

int BinSearch(int A[], size_t nlen, int key)

1. 一个错误的想法:先比较端点以排除 key不在一个区间里的这种可能,误以为这样做会提高效率:

 1 int BadSearch1(int A[], size_t nlen, int key)
 2 {
 3     size_t l = 0;
 4     size_t r = nlen - 1;
 5     while (true)
 6     {
 7         if (r < l || key < A[l] || key > A[r]) //bad idea
 8         {
 9             return -1;
10         }
11         size_t m = (l + r) / 2;
12         if (key == A[m])
13             return m;
14         else if (key < A[m])
15             r = m - 1;
16         else
17             l = m + 1;
18     }
19 }

上面的代码是正确的,但line 7 中key < A[l] || key > A[r] 就是属于过度的考虑。虽然在key不在区间的情况下减少了比较的次数,但是在正常情况下,额外增加了两次关键字的比较,如果关键字不是整型的,那么额外增加的比较以及A[l] 与A[r]的读取消耗将变得更加显著。而且对于一个相同的l或者r,会比较多次。

另外这种错误的想法增加了代码的复杂度,代码写得越多,出错的几率也就越大。相反,越简单的实现越不容易出错。

基于比较端点的这种错误思想,本人也写考虑过其他更加复杂,更狗血的实现代码,比如比较端点是否与key值相等的比较:

 1 int BadSearch2(int A[], size_t nlen, int key)
 2 {
 3     size_t l = 0;
 4     size_t r = nlen - 1;
 5     while (true)
 6     {
 7         if (r < l )
 8         {
 9             return -1;
10         }
11 
12         // bad idea start 
13         if (key == A[l])
14         {
15             return l;
16         }
17         else if (key < A[l])
18         {
19             return -1;
20         }
21 
22         if (key == A[r])
23         {
24             return r;
25         }
26         else if (key > A[r])
27         {
28             return -1;
29         }
30         // bad idea end
31 
32         size_t m = (l + r) / 2;
33         if (key == A[m])
34             return m;
35         else if (key < A[m])
36         {
37             r = m - 1;
38             ++l;
39         }
40         else
41         {
42             l = m + 1;
43             r--;
44         }
45     }

相比BadSearch1,BadSearch2更加复杂,相应的,出错的几率更大,时间复杂度也更高,虽然端点比较,缩小了一个元素的搜索区间,但是代价太大了。因为这样一个元素的比较只减少了一个元素的搜索区间,这实际上是把二分搜索的对数复杂度向线性复杂度靠近了。

不知道本人脑子为何会闪现出这样的念头,也许是其他的错误导致了去考虑端点能否搜索上,但是如果能好掌握并充分利用好时间复杂度分析的这一工具,应该就不头脑这样发热了。

也许出现这样奇葩想法的也就只有我一个了!

下面要说的这个错误若隐若现,直到最近这几天重新复习算法的时候才给摁住了,看到了其本质面目!!!

2. 一个比较隐蔽的bug,会导致端点搜索不上。

 1 int BadSearch3(int A[], size_t nlen, int key)
 2 {
 3     size_t l = 0;
 4     size_t r = nlen - 1;
 5     while (l < r )
 6     {
 7         size_t m = (l + r) / 2;
 8         if (key == A[m])
 9             return m;
10         else if (key < A[m])
11             r = m - 1;
12         else
13             l = m + 1;
14     }
15     return -1;
16 }

line5: 我把端点的校验放在了while条件里,但是对终止条件的理解还是不够深入透彻。如果l==r, 则遗漏了端点,直接返回-1. 说难发现,其实也比较容易,就是各种情况都考虑全就可以了!关于类似于<与<=的区别这样的情况,可以好好分析一下。如果在面试的时候,没有电脑帮你运行,那你自己就得充当电脑的角色,给上几种特殊的输入,就能发现可能的错误。

总结:终止条件是r比l小。如果两者相等,说明还有一个元素

 

3.一种容易发现的bug,影响收敛。

 1 int BadSearch4(int A[], size_t nlen, int key)
 2 {
 3     size_t l = 0;
 4     size_t r = nlen - 1;
 5     while (true) 
 6     {
 7         if (l > r)
 8         {
 9             return -1;
10         }
11         size_t m = (l + r) / 2;
12         if (key == A[m])
13             return m;
14         else if (key < A[m])
15             r = m;// bug
16         else
17             l = m;// bug
18     }19 }

line 15&17, 新的区间折半时没有排除已经比较过的元素A[m], 如果在电脑上运行,会很快发现死循环,但是如果面试的时候,可能会一时笔误或者对收敛条件理解不透彻而导致这样的错误。

这个在《算法珠玑》一书第五章中也举到了这个例子。同时第四章讲述的利用不变式的初始化,循环保存以及终止条件来检查算法实现的正确性,都是是非常有用的工具,值得去深入学习。

之前看书时只知道直接看算法的实现,却不看重作者的思路。相当于只注重了"术”,却忽略了“道”!捡了芝麻,丢了西瓜!!!

 

二. 写正确的二分搜索

 我给自己总结的关键几点:

1. 首先对算法有深刻的了解,分而治之,一次比较,搜索区间减半,最坏情况(如数组中不存在key,包括key不在[A[0], A[n-1]]范围内的情况, 或者在A[0]或者A[n-1], )下,lg(n)次结束。 所以第一种错误就没有必要发生。因为最坏情况也是完全可以接受的!

2. 学会使用检验算法正确性的方法 ----不变式

3. 学会构造简单的测试数据验证边界条件下的程序处理是否正确。

4. 一般正确的实现都是短小精悍,流畅工整。如果在写的时候,发现自己一会忘记比较这个,一会觉得要查那个,说明自己对算法还没有理解透彻,很可能写不出正确的代码!!!!

最后,把正确的二分搜索实现贴上吧:

 1 int BinSearch1(int A[], size_t nlen, int key)
 2 {
 3     size_t l = 0;
 4     size_t r = nlen - 1;
 5     while (l <= r)
 6     {
 7         size_t m = (l + r) / 2;
 8         if (key == A[m])
 9             return m;
10         else if (key < A[m])
11             r = m - 1;
12         else
13             l = m + 1;
14     }
15     return -1;
16 }

 

再写一个在也许严格递增也许严格循环递增的数组里实现二分查找:

 1 //给定一个数组,它或者严格递增,如{1,2,3,4,5,6},或者严格循环递增,如{4,5,6,1,2,3},查找某个给x在数组中位置下标。
 2 int BinSearchInLoopIncrementArray(int array[], size_t nlen, int key)
 3 {
 4     int left = 0;
 5     int right = nlen;
 6     while (left <= right)
 7     {
 8         int mid = (left + right) / 2;
 9         if (key == array[mid])
10             return mid;
11         else if (key < array[mid]) 
12         {
13             if (array[left] < array[mid] && key < array[left])
14             {
15                 // array[left...mid] strictly increment,
16                 // key smaller than all elements in array[left...mid] ---> 
17                 // So key can't be in array[left...mid]
18                 left = mid + 1;
19             }
20             else
21             {
22                 // case1: if array[left] > array[mid] > key --->
23                 // array[left...mid] loop-increment, ---> 
24                 // array[mid...right] have to be strictly increment, since key < array[mid] --->
25                 // key bigger than all element in array[left, mid] --->
26                 // key can't be in array[mid...right] 
27                 
28                 // case2: array[left] < array[mid] AND key >= array[left]--->
29                 // array[left...mid] strictly increment and key is between array[left] and array[mid] 
30                 right = mid - 1;
31             }
32         }
33         else
34         {
35             if (array[mid] < array[right] && array[right] < key)
36             {
37                 right = mid - 1;
38             }
39             else
40             {
41                 left = mid + 1;
42             }
43         }
44     }
45     return -1;
46 }

 

posted @ 2014-12-24 14:11  Lion_0  阅读(95)  评论(0)    收藏  举报