初級程序員面試不靠譜指南(五)


四、遞歸的第一次親密接觸

    我經常會想,如果給沒有學過計算機或者數學的人說遞歸這個詞他們腦中會怎樣理解這個詞的意思。遞歸這個概念在面試中出現的概率大於85%,而他和數據結構、算法那一塊的結合更是經常作為考察的重點,所以在還沒有寫到那里的時候,只能說目前只是第一次的接觸。

1.吊絲思維的轉換。對於遞歸,我覺得最精辟的一句話是“這是一種新的思維方式,把一個大問題分解成為很多小問題,並且你要相信,只要規則制定的是正確的,這些小問題就能自然的不斷得出正確的結果,從而得到最終大問題的正確結果。”我忘了是在哪本書上看到,可能和原文有些不一樣,但是很能表達我的感受。就像我第一次看到漢諾塔問題的解法的時候,我就覺得這樣就可以解決了嗎?感覺看上去也沒有做什么啊,所以說這是一種新的思維方式,你要相信小問題能夠自然的被解決,只要你分析的解決問題的規則都是一樣且正確的。

在正式討論遞歸之前,我覺得我應該再來說一下這種思維方式,因為這也是我花了好大的功夫才解決的一個問題,如果不能有這種思維方式,就會導致一個現象,看遞歸的代碼都可以看懂,但是要自己寫的話就寫不出來了。一般正常人解決問題的方法肯定是按照某一種順序一步一步的來,而且都是從底層一步一步的完成向上解決,畢竟大家一般都是吊絲嘛。但是設想一下如果你站在一個宏觀的方面,你也許就不會這樣解決問題,比如你是一個黑社會老大,要收點保護費啥的,你會不會自己一個一個去收?除非你有特殊的癖好,不然你應該是吩咐你下一層的高管人員去哪里收,怎么收,然后這些人員再一級一級的吩咐下去,規則都不變,直到最底層的羅羅們。這些底層的羅羅們收好鈔票之后再一層層的上繳(不管他按什么規則上繳),最后匯總到你這兒。在這個過程中,你需要相信下層人員會按照你制定的規則去收集這些費用,但是不用去考慮細節,這才是有高層風度的做法。所以,在處理遞歸問題的時候,你要把自己當作一個規則制定者,而不是執行者,相信自己的規則是沒有錯誤的,然后,所有子問題按照自己的規則有序的執行,最終匯總,就能解決自己的問題。

2.C語言的實現方式。C語言中函數實現遞歸的方法是通過堆棧,而一個線程分配的棧大小往往都是有限的,默認情況下是1MB,這是一個很小的空間,所以說,使用遞歸所要考慮的重要問題之一就是要保證棧空間不會被全部的消耗。下面這段小程序是為了簡單的展示遞歸是怎樣進行的,可以執行一下查看結果。

int MyFunc(int counter) {
    
    if(counter == 0) {
        printf("counter->before:%d,address:%x\r\n", counter,&counter);
        return counter;
    }
    else {
        printf("counter->before:%d,address:%x\r\n", counter,&counter);
        int valueToPrint = MyFunc(counter - 1);
       
		printf("counter->after:%d,address:%x\r\n", counter,&counter);
        printf("valueToPrint->after:%d,address:%x\r\n", valueToPrint,&valueToPrint);

        return counter;
    }
	
}

      其執行結果如下所示:

    

       可以看到,首先,在每次調用自己之前,函數將四個數依次保存到四個地址之中(堆棧),接着,直到遇到conuter=0,然后函數返回一個值,也就是0,這時候繼續執行調用自己后面的語句,從圖中的counter的值可以獲知是哪一次的繼續執行調用后的語句,從valueToPrint也可以獲知是哪一次的調用返回。可以看到整個過程大致就是執行---調用 ---執行,所以有人把遞歸的過程總結為以下的式子 遞歸 算法=(預處理步驟)(子問題解決調用)(后續處理步驟),每一個步驟執行結束才能進入下一個步驟。

3.細談遞歸步驟。怎么樣去用遞歸的思想解決一個問題呢?我想從一個實際的例子來說明比較容易理解,比如,判斷一個字符串是不是回文字符串,回文字符串就是類似”abcba”這種正着看反着看都一樣的字符串。如果用常規思想解決,無非就是從第一個字符出發,一直到中間的一個字符,依次判斷是否都是相同的,或者類似的解法。這是從微觀的方式看待這個問題,而遞歸就像前面描述的那樣,需要你從宏觀的方面看待這個問題。如果從兩側開始,每一個子字符串都是回文字符串,那么這個字符串一定就是回文字符串,但是這種關系應該有個終止點,也就是到什么情況下,停止這種判斷。如果按照上面的思路,從最長的字符串開始,每判斷一次便剝離兩側的字符,那么結束的條件應該是,最后沒有字符可以剝離,或者只剩一個字符,很明顯,如果能進行到這一步,說明前面的判斷都通過了(如果中間某處判斷不是回文字符串,那么直接可以以false返回了)。所以,遞歸的第一步就是:

  • 定義遞歸的終止條件。比如在這個例子中,就是if(strlen(testString)==0|| strlen(testString)==1) IsPalindrome=true.

     在這之后,就需要仔細思考你需要怎么遞歸了,所以說,第二步就是:

  • 仔細的思考和制定解決子問題的規則。首先,你需要能夠把原始問題准確的划分為采用同樣方法解決的小問題,這些小問題具有的特點就是比原始的大問題更加簡單,但是解決方法是一樣的。比如上面的回文字符問題,你可能會思考如何划分子字符串,按照這個問題本身的描述方法,明顯不能按照類似每次減少字符長度的方法取得字符串。所以,這里也是一個難點,但是也是有一定方法可以遵循的,比如這個問題的邏輯思考方式和第一步就是依次對比頭尾字符,那么應該遵循着這個思路,子問題的解決方法也應該是對比頭尾字符,這樣思考以后,明顯,這里的子問題明顯就是去掉頭尾字符的子字符串判斷是否是回文,如果是,程序繼續進行下一輪的判斷,如果不是,那么返回false。

按照上面的思路,我們可以寫出偽代碼:

Int IsPalindrome(char * testString)

{

       If 字符串長度為0或1

           返回true.

       Else

           If  字符串頭尾字符相同

              IsPalindrome(去掉頭尾字符的新子串)

          Else

             返回 false.

}

   按照上面的偽代碼,你可以寫出如下的代碼,你可以測試一下。

int IsPalindrome(char * testString)
{
   if(strlen(testString)==0||strlen(testString)==1) return 1;
   else 
    {
       if(testString[0]==testString[strlen(testString)-1]) 
       {
	 char *tmp=(char*)malloc((strlen(testString))*sizeof(char));
	 int i=1;
	 for(;i<strlen(testString)-1;i++) tmp[i-1]=testString[i];
	 tmp[strlen(testString)-2]='\0';
	 IsPalindrome(tmp);
       }
       else
	return 0;
   }
}

  還有一點這里可以順便提一下,c語言標准類型里其實沒有bool類型。關於遞歸的題目實在太多了,所以不是說看每一個題目的解法就行了,重要的是能夠按照這個思想去理解遞歸,不然到面試被問到新問題的時候往往會手足無措,這是我的切身體會。

4.遞歸和循環。從遞歸的執行方式上看,和循環總有那么一種說不明白的關系,所以對於遞歸和循環也是經常會被問到的一個問題,這其中最最常見的就是,什么時候使用遞歸,什么時候使用循環?

      首先,說明使用遞歸的情況,第一點就是,如果這個問題很復雜但是去可以分解成為一些遞歸解決的小問題的時候就適合用遞歸,比如漢諾塔問題。第二,算法本身 就是用遞歸的方式描述的,比如說,樹的遍歷,在問題算法的描述上,就是以遞歸的形勢,這一點在數據結構很多算法中有突出的體現。

      接着,什么時候使用循環比較好呢?第一點就是,問題本身就很簡單,如果一個簡單的問題用復雜的解法去解答,除非你有特殊的目的,不然的話那就說明你確實有 着不同常人的癖好。第二點就是,算法不能用拆解成子問題並且以遞歸的方式描述,這種就沒有必要用遞歸了,因為這樣只會徒增寫代碼的工作量。第三,就像前面 說的,遞歸要耗費棧空間,而在現代語言中,棧空間的大小大大小於堆中的空間,所以用循環可以節省很大的空間。

5.不同尋常的尾遞歸。尾遞歸就是一個函數中所有遞歸形式的調用都出現在函數的末尾,通俗點說就是,當遞歸調用是整個函數體中最后執行的語句且它的返回值不屬於表達式的一 部分時,這個遞歸調用就是尾遞歸。比如下面這種就是尾遞歸:

int F(int n, int acc)
{
    if (n == 0) return acc;
    return F(n - 1, acc * n);
}

    尾遞歸函數的特點是在回歸往上的過程中不用做任何操作,這個特性很重要,因為大多數現代的編譯器會利用這種特點自動生成優化的代碼。比如上面的內容就可以被優化成為:

int FOptimized(int n, int acc)
{
    while (true)
    {
        if (n == 0) return acc;
        acc *= n;
        n--;
    }
}

     關於遞歸還有很多內容,這里在還沒有涉及到數據結構之前,先來個預熱,在后面至少還有兩次還會深入的了解一下遞歸,我個人體會,理解遞歸對面試的幫助真的是巨大的。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM