用递归要小心---以递归法进行二分查找为例

昨天面试的时候被问了好多问题,今天再做,有些部分竟然连起来了:二分查找、递归、局部变量静态变量(静态局部变量),可能还有更多,待我慢慢总结。。

OK进入正题。

一、 

首先 写个二分查找的函数。因为之前只是了解过这个算法,实际自己写还没写过,想了想,如果不用递归,一时没啥思路,那就用递归吧

 1 // This is v0.1 and there may be errors.
 2 #include <stdio.h>
 3 
 4 int binary_search(int a[], int left, int right, int key){
 5     if(left < right){
 6         int mid = (left+right)/2;
 7         if(key > a[mid])
 8             binary_search(a, mid, right, key);
 9         else if(key < a[mid])
10             binary_search(a, left, mid, key);
11         else
12             return mid;
13     }
14 
15     return -1;//not 0, or there may be conflict with the [index]-0 of array   
16 }
17 
18 int main(){
19     int a[6]={1, 2, 3, 4, 5, 6};
20     int key = 5;
21     printf("%d
", binary_search(a, 0, 5, key));
22 
23     return 0;
24 }
25  
View Code

用Xcode试一试。咦,怎么出错了。。

看了几遍没想出个所以然来,那就一步一步写吧。

1).

首先修改一下key,当key=3的时候,能输出正确结果,其他情况都会输出-1

2).

然后,以key=5为例,下面是一个粗略的过程

图一

从图一可以看到,最后的确是找到了key的下标的,如果我们加入一句如下的 printf("Succeed!");

if(key > a[mid])
    return binary_search(a, mid, right, key);
else if(key < a[mid])
    return binary_search(a, left, mid, key);
else{
    printf("Succeed!");
    return mid;
}

最后是能在控制台打印出来"Succeed!"的。

3).

对比其他地方找到的递归代码可以发现,问题主要出在

if(key > a[mid])
    return binary_search(a, mid, right, key);
else if(key < a[mid])
    return binary_search(a, left, mid, key);
else
    return mid;

缺少红字部分return

缺少红字部分return

缺少红字部分return

这导致什么结果呢?我们知道, return了就会结束函数,但是这不是结束“整个函数”,只是结束这次调用,就像嵌套循环,里面的break只会打破里面一层循环,不会一次打破两层循环。这样,如果只有最后一个else里面有return, 那么,在图一中找到下标后,只是return返回结束了第三块调用,回到第二块以后没有就此结束,还是会继续走完第二块的if(left<right){...}剩下的内容,然后return(第二块的) -1; 这里才结束第二块,回到第一块,然后同理,继续走完第一块的if(left<right){...}剩下的内容,然后return(第一块的) -1; 结束“整个函数”调用,因此最后还是reutrn了找不到下标的情况,而不是在找到下标之后一层层往上退出并不执行后面剩下的内容最后返回期望的下标。好吧这就是基础不扎实的后果。慢慢补吧,每天补一个漏洞,每天就是一分进步。在之前练排序的时候没有发现这个问题,是因为直接把地址传进来了,然后在相应地址的元素已经修改好了,只需要能(而且不对已排序的数组再做改动地)停止递归就好了。虽然还是有点危险。

//TODO:等会儿把那些用到递归的函数再修改一下,排完就return

 二、

1).

OK再回到主题,想通上面一点之后,我又突然想到,可不可以设定一个flag,把最后下标的值给flag呢,或者这样,直接一开始把这个flag初始化为-1,然后如果找到了那就把flag修改为下标值,如果没找到那还是原来的-1,最后返回flag,那这样不要if(left<right){...}里面的那些return也无妨(其实这样不对。。不过先来看看这样做之后遇到了什么问题)

 1 // This is v0.2 and there may be errors.
 2 #include <stdio.h>
 3 
 4 int binary_search(int a[], int left, int right, int key){
 5     int key_index = -1;
 6     if(left < right){
 7         int mid = (left+right)/2;
 8         if(key > a[mid])
 9             binary_search(a, mid, right, key);
10         else if(key < a[mid])
11             binary_search(a, left, mid, key);
12         else
13             key_index = mid;
14     }
15 
16     return key_index;
17 }
18 
19 int main(){
20     int a[6]={1, 2, 3, 4, 5, 6};
21     int key = 6;
22     printf("%d
", binary_search(a, 0, 5, key));
23 
24     return 0;
25 }
26  
View Code

来让我们run一下,咦。。为什么死掉了。。再重复还是这样,但是如果把key换成4,就可以运行了,虽然结果。。是-1

怎么会这样,原来问题没解决,又遇到了其他问题。。哎不急,有问题就一个一个解决呗,总是会有办法的,在error中学习,然后把过程整理记录下来备忘。

2).

再对比一下别人的程序,并且按照图一的方法再做一遍,可以找到死掉的原因:缺少如下红字部分

if(key > a[mid])
    binary_search(a, mid+1, right, key);

这个mid+1不仅是使下一次搜索的区间更小一点((mid+1)->right 区间比 mid->right 要小),而且是必要的,顺带指出另一个漏洞(红字部分)

if(left<=right)

这两个地方的写法会使程序出问题,原因在于无法取到右边界

int mid = (left+right)/2

这里取中间值的下标用的是整除,用一个例子说明问题

(3+5)/2=4    //OK
(3+3)/2=4    //OK
(3+4)/2=3    //整除取不到右边界

而在if-else中,我们已经把  (key == a[mid]) 的情况写出来了,如果是相等,那就进入最后一个else,如果不相等,那也没必要在下一次搜索的时候把这个边界写进去,大于的时候左边界直接是mid+1。如果还是用图一的方法,而且key=6,那么程序会一直在 binary_search(a, 4, 5, 6)出不去

图二

Ps:其实应该想起来的,因为之前在归并排序的时候也考虑过这个问题,

一个数组,一开始是a[0]..a[n-1]  left=0, right=n-1

从中间分成两段,mid = (left+right)/2 ,那么

左边的区间就是 a[left]..a[mid]    

右边的区间下标就是 a[mid+1], a[right]

/***      9.21更新   有点问题, 参见第四部分      ***/ 

3).

因此我们要如下修复bug(红字部分)

 1 // This is v0.3 and there may be errors.
 2 #include <stdio.h>
 3 
 4 int binary_search(int a[], int left, int right, int key){
 5     int key_index = -1;
 6     if(left <= right){
 7         int mid = (left+right)/2;
 8         if(key > a[mid])
 9             binary_search(a, mid+1, right, key);
10         else if(key < a[mid])
11             binary_search(a, left, mid, key);
12         else
13             key_index = mid;
14     }
15 
16     return key_index;
17 }
18 
19 int main(){
20     int a[6]={1, 2, 3, 4, 5, 6};
21     int key = 6;
22     printf("%d
", binary_search(a, 0, 5, key));
23 
24     return 0;
25 }
26  
View Code

三、

好了,一切看来大功告成,让我们再来试一试。。好吧,前面已经提到了,还是错误,输出还是-1

warum?!

OK废话少说,这里的逻辑错误要涉及到 变量的作用域

1).

这里说道:

局部变量也只有局部作用域,它是自动对象(auto),它在程序运行期间不是一直存在,而是只在函数执行期间存在,函数的一次调用执行结束后,变量被撤销,其所占用的内存也被收回。

还是以图一为例,由于 key_index定义的位置,三块区域里面每一块都会创建一个 key_index,我们记为key_index1, key_index2, key_index3。由于只在最后一块,也就是满足(key == a[mid])的部分进入了有key_index赋值的语句(其他地方不满足条件进不去),也就是说key_index3=mid,好了,在return key_index3回到第二块的时候,key_index3被消灭了,而由于key_index2没有来接收key_index3,所以key_index2还是-1,更不用说key_index1 了,因此在最外面一层 return key_index; 的时候,实际上返回的是没有变化过的key_index1,所以仍旧是 -1

2).

对应的解决方案

I.这里加个static,结果就OK了~

int binary_search(int a[], int left, int right, int key){
    static int key_index=-1;
    if(left<=right){
        int mid=(left+right)/2;
        if(key>a[mid])
            binary_search(a, mid+1, right, key);
        else if (key<a[mid])
            binary_search(a, left, mid, key);
        else {
            key_index=mid;
        }
    }
    
    return key_index;
}

因为这里说道:

静态局部变量具有局部作用域,它只被初始化一次,自从第一次被初始化直到程序运行结束都一直存在,它和全局变量的区别在于,全局变量对所有的函数都是可见的,而静态局部变量只对定义自己的函数体始终可见。

也就是说加个static就相当于在函数里面关联了key_index1, 2, 3,它们真正地实际上变成了同一个变量,因此在最后key

II.或者(红字部分)

int binary_search(int a[], int left, int right, int key){
    int key_index = -1;
    if(left <= right){
        int mid = (left+right)/2;
        if(key > a[mid])
            key_index = binary_search(a, mid+1, right, key);
        else if(key < a[mid])
            key_index = binary_search(a, left, mid, key);
        else {
            key_index = mid;
        }
    }
    
    return key_index;
}

III.或者是外面传址传进来一个flag,应该也可以。不过这些啊研究下就好了,最好还是用通用的,不要减少程序的易读性,也不要再增加复杂性。

所以现在可以给出一个bug-fixed 版本的程序了,还是用逐层return的方法(而且是能取得右边界的) 

(**-- 9.21更新,还是有点问题 :(    --**)

 1 //binary search
 2 //v0.4, bugs fixed - oh no...
 3 //opppppppps... there are still bugs (found in 2015.09.21)
 4 
 5 #include <stdio.h>
 6 
 7 int binary_search(int a[], int left, int right, int key){
 8     if(left<=right){
 9         int mid=(left+right)/2;
10         if(key>a[mid])
11             return binary_search(a, mid+1, right, key);
12         else if(key<a[mid])
13             return binary_search(a, left, mid, key);
14         else
15             return mid;
16     }
17     
18     return -1;//not 0, or there may be conflict with the [index]-0 of the array
19 }
20 
21 int main(){
22     int a[6]={1,2,3,4,5,6};
23     int key=6;
24     printf("%d", binary_search(a, 0, 5, key));
25 
26 }
27  
View Code

暂时先到这儿,晚上再回顾一遍,然后

$ git push origin master

到 learngit上:)

另外明天再想想不用递归的方式该如何进行二分查找。

四、

刚刚发现。。还有bug。。关于无法退出递归

1).

第二部分解决了:当给定的key不属于数组中的元素,且大于最大值的时候,最后要能退出结束递归。

在这个情况下,最后是会到达 left == right的情况的,

从而 mid=(left+right)/2=left,

下一次的左边界应该是 left_2=mid+1,也就是

if(key > a[mid])
    return binary_search(a, mid+1, right, key);

这样便会不满足(left<=right)的条件,从而到达最后的 return -1 返回退出

2).

当给定的key不属于数组中的元素,且大于最大值的时候,道理和第二部分相同,不过是在另外一边要做相应处理 把下一层的右边界变成 right_2=mid-1

if(key < a[mid])
    return binary_search(a, left, mid-1, key);

3).

 1 //binary search
 2 //v0.5, bugs fixed
 3 #include <stdio.h>
 4 
 5 int binary_search(int a[], int left, int right, int key){
 6     if(left<=right){
 7         int mid=(left+right)/2;
 8         if(key>a[mid])
 9             return binary_search(a, mid+1, right, key);
10         else if(key<a[mid])
11             return binary_search(a, left, mid-1, key);
12         else
13             return mid;
14     }
15     
16     return -1;//not 0, or there may be conflict with the [index]-0 of the array
17 }
18 
19 int main(){
20     int a[6]={1,2,3,4,5,6};
21     int key=6;
22     printf("%d", binary_search(a, 0, 5, key));
23 
24 }

每次对比完key与a[mid]之后

下一层的左区间是 [left].. [mid-1]

    右区间是 [mid+1].. [right]

原文地址:https://www.cnblogs.com/Cmfvacks-IsLjj/p/4823024.html