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

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

OK进入正题。

一、

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

// This is v0.1 and there may be errors.
#include <stdio.h>

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

    return -1;//not 0, or there may be conflict with the [index]-0 of array
}

int main(){
    int a[6]={1, 2, 3, 4, 5, 6};
    int key = 5;
    printf("%d\n", binary_search(a, 0, 5, key));

    return 0;
}

用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也无妨(其实这样不对。。不过先来看看这样做之后遇到了什么问题)

// This is v0.2 and there may be errors.
#include <stdio.h>

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])
            binary_search(a, mid, right, key);
        else if(key < a[mid])
            binary_search(a, left, mid, key);
        else
            key_index = mid;
    }

    return key_index;
}

int main(){
    int a[6]={1, 2, 3, 4, 5, 6};
    int key = 6;
    printf("%d\n", binary_search(a, 0, 5, key));

    return 0;
}

来让我们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]

3).

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

// This is v0.3 and there may be errors.
#include <stdio.h>

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])
            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;
}

int main(){
    int a[6]={1, 2, 3, 4, 5, 6};
    int key = 6;
    printf("%d\n", binary_search(a, 0, 5, key));

    return 0;
}

三、

好了,一切看来大功告成,让我们再来试一试。。好吧,前面已经提到了,还是错误,输出还是-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的方法(而且是能取得右边界的)

//binary search
//v0.4, bugs fixed
#include <stdio.h>

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

    return -1;//not 0, or there may be conflict with the [index]-0 of the array
}

int main(){
    int a[6]={1,2,3,4,5,6};
    int key=6;
    printf("%d", binary_search(a, 0, 5, key));

}

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

$ git push origin master

到 learngit上,这样一路改过来可以有好几个commit好开心:)

时间: 2024-08-05 20:32:59

用递归要小心---以递归二分查找为例的相关文章

Python 迭代器&amp;生成器,装饰器,递归,算法基础:二分查找、二维数组转换,正则表达式,作业:计算器开发

本节大纲 迭代器&生成器 装饰器  基本装饰器 多参数装饰器 递归 算法基础:二分查找.二维数组转换 正则表达式 常用模块学习 作业:计算器开发 实现加减乘除及拓号优先级解析 用户输入 1 - 2 * ( (60-30 +(-40/5) * (9-2*5/3 + 7 /3*99/4*2998 +10 * 568/14 )) - (-4*3)/ (16-3*2) )等类似公式后,必须自己解析里面的(),+,-,*,/符号和公式,运算后得出结果,结果必须与真实的计算器所得出的结果一致 迭代器&

二分查找 Binary Search

简单地用递归的方法实现了二分查找算法,适用于数组. 二分查找算法的前提条件是数组本身是有序的,比如int arr[6] = {2, 3, 5, 7, 11, 13}; 1 int 2 BinarySearch(int arr[], int key, int left, int right) 3 { 4 if (left > right) { 5 return -1; 6 } 7 8 int middle = (left + right) / 2; 9 10 if (arr[middle] ==

养成良好的编程风格--论二分查找的正确姿势

摘自:http://www.cnblogs.com/ider/archive/2012/04/01/binary_search.html 在学习算法的过程中,我们除了要了解某个算法的基本原理.实现方式,更重要的一个环节是利用big-O理论来分析算法的复杂度.在时间复杂度和空间复杂度之间,我们又会更注重时间复杂度. 时间复杂度按优劣排差不多集中在: O(1), O(log n), O(n), O(n log n), O(n2), O(nk), O(2n) 到目前位置,似乎我学到的算法中,时间复杂度

二分查找的实现和应用汇总(转载)

转载地址:http://www.cnblogs.com/ider/archive/2012/04/01/binary_search.html 二分查找法的实现和应用汇总 在学习算法的过程中,我们除了要了解某个算法的基本原理.实现方式,更重要的一个环节是利用big-O理论来分析算法的复杂度.在时间复杂度和空间复杂度之间,我们又会更注重时间复杂度. 时间复杂度按优劣排差不多集中在: O(1), O(log n), O(n), O(n log n), O(n2), O(nk), O(2n) 到目前位置

【转载】二分查找

[本文转自]http://www.cnblogs.com/ider/archive/2012/04/01/binary_search.html 在学习算法的过程中,我们除了要了解某个算法的基本原理.实现方式,更重要的一个环节是利用big-O理论来分析算法的复杂度.在时间复杂度和空间复杂度之间,我们又会更注重时间复杂度. 时间复杂度按优劣排差不多集中在: O(1), O(log n), O(n), O(n log n), O(n2), O(nk), O(2n) 到目前位置,似乎我学到的算法中,时间

二分查找与 bisect 模块

Python 的列表(list)内部实现是一个数组,也就是一个线性表.在列表中查找元素可以使用 list.index() 方法,其时间复杂度为O(n).对于大数据量,则可以用二分查找进行优化.二分查找要求对象必须有序,其基本原理如下: 1.从数组的中间元素开始,如果中间元素正好是要查找的元素,则搜素过程结束: 2.如果某一特定元素大于或者小于中间元素,则在数组大于或小于中间元素的那一半中查找,而且跟开始一样从中间元素开始比较. 3.如果在某一步骤数组为空,则代表找不到. 二分查找也成为折半查找,

递归和非递归的二分查找

思路很简单,代码注释已标注 #include <stdio.h> //递归二分查找 int binarySearch(int*start,int *end,intfindData){ if (start > end) {      // 递归边界条件 return -1; } int *mid = start + (end - start)/2;     //根据中间值不断二分缩小待查元素所在范围 if (findData == *mid) { return *mid; }else if

8.8 冒泡排序 选择排序 二分查找 递归使用

冒泡排序: #include <stdio.h> #include <stdlib.h> #include <time.h> #define N 100000 #define M 100000 void show_arr(int * a,int n) { int i; for(i = 0; i < n; i++) { printf("%d ",a[i]); } printf("\n"); } void init_arr(in

二分查找(递归与非递归)

递归的二分查找: 1 int search(int *a, int target, int p, int r) 2 { 3 if (p <= r) 4 { 5 int mid; 6 7 mid = (p + r) / 2; 8 if (*(a + mid) == target) 9 return 1; 10 else if (*(a + mid) > target) 11 return search(a, target, p, mid - 1); 12 else 13 return searc