用遞歸要當心---以遞歸法進行二分查找爲例

昨天面試的時候被問了好多問題,今天再作,有些部分居然連起來了:二分查找、遞歸、局部變量靜態變量(靜態局部變量),可能還有更多,待我慢慢總結。。git

OK進入正題。面試

 

1、 算法

首先 寫個二分查找的函數。由於以前只是瞭解過這個算法,實際本身寫還沒寫過,想了想,若是不用遞歸,一時沒啥思路,那就用遞歸吧數組

 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\n", binary_search(a, 0, 5, key));
22 
23     return 0;
24 }
25  
View Code

 

 

用Xcode試一試。咦,怎麼出錯了。。ide

看了幾遍沒想出個因此然來,那就一步一步寫吧。函數

1).學習

首先修改一下key,當key=3的時候,能輸出正確結果,其餘狀況都會輸出-1spa

 

2)..net

而後,以key=5爲例,下面是一個粗略的過程code

圖一

從圖一能夠看到,最後的確是找到了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

 

 

 2、

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\n", 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\n", binary_search(a, 0, 5, key));
23 
24     return 0;
25 }
26  
View Code

 

 

3、

好了,一切看來大功告成,讓咱們再來試一試。。好吧,前面已經提到了,仍是錯誤,輸出仍是-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上:)

另外明天再想一想不用遞歸的方式該如何進行二分查找。

 

 

 

 

4、

剛剛發現。。還有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]

相關文章
相關標籤/搜索