1. 程式人生 > >遞迴演算法詳細分析-> C (轉)

遞迴演算法詳細分析-> C (轉)

C通過執行時堆疊支援遞迴函式的實現。遞迴函式就是直接或間接呼叫自身的函式。
許多教科書都把計算機階乘和菲波那契數列用來說明遞迴,非常不幸我們可愛的著名的老潭老師的《C語言程式設計》一書中就是從階乘的計算開始的函式遞迴。導致讀過這本經書的同學們,看到階乘計算第一個想法就是遞迴。但是在階乘的計算裡,遞歸併沒有提供任何優越之處。在菲波那契數列中,它的效率更是低的非常恐怖。

這裡有一個簡單的程式,可用於說明遞迴。程式的目的是把一個整數從二進位制形式轉換為可列印的字元形式。例如:給出一個值4267,我們需要依次產生字元‘4’,‘2’,‘6’,和‘7’。就如在printf函式中使用了%d格式碼,它就會執行類似處理。

我們採用的策略是把這個值反覆除以10,並列印各個餘數。例如,4267除10的餘數是7,但是我們不能直接列印這個餘數。我們需要列印的是機器字符集中表示數字‘7’的值。在ASCII碼中,字元‘7’的值是55,所以我們需要在餘數上加上48來獲得正確的字元,但是,使用字元常量而不是整型常量可以提高程式的可移植性。‘0’的ASCII碼是48,所以我們用餘數加上‘0’,所以有下面的關係:

‘0’+ 0 =‘0’
‘0’+ 1 =‘1’
‘0’+ 2 =‘2’
    ...

  從這些關係中,我們很容易看出在餘數上加上‘0’就可以產生對應字元的程式碼。接著就打印出餘數。下一步再取商的值,4267/10等於426。然後用這個值重複上述步驟。

  這種處理方法存在的唯一問題是它產生的數字次序正好相反,它們是逆向列印的。所以在我們的程式中使用遞迴來修正這個問題。

  我們這個程式中的函式是遞迴性質的,因為它包含了一個對自身的呼叫。乍一看,函式似乎永遠不會終止。當函式呼叫時,它將呼叫自身,第2次呼叫還將呼叫自身,以此類推,似乎永遠呼叫下去。這也是我們在剛接觸遞迴時最想不明白的事情。但是,事實上並不會出現這種情況。

  這個程式的遞迴實現了某種型別的螺旋狀while迴圈。while迴圈在迴圈體每次執行時必須取得某種進展,逐步迫近迴圈終止條件。遞迴函式也是如此,它在每次遞迴呼叫後必須越來越接近某種限制條件。當遞迴函式符合這個限制條件時,它便不在呼叫自身

在程式中,遞迴函式的限制條件就是變數quotient為零。在每次遞迴呼叫之前,我們都把quotient除以10,所以每遞迴呼叫一次,它的值就越來越接近零。當它最終變成零時,遞迴便告終止。

/*接受一個整型值(無符號0,把它轉換為字元並列印它,前導零被刪除*/

#include <stdio.h>

int binary_to_ascii( unsigned int value)
{
unsigned int quotient;

  quotient = value / 10;
  if( quotient != 0)
    binary_to_ascii( quotient);
  putchar ( value % 10 + '0' );
}


遞迴是如何幫助我們以正確的順序列印這些字元呢?下面是這個函式的工作流程。
1. 將引數值除以10
2. 如果quotient的值為非零,呼叫binary-to-ascii列印quotient當前值的各位數字

  3. 接著,列印步驟1中除法運算的餘數

  注意在第2個步驟中,我們需要列印的是quotient當前值的各位數字。我們所面臨的問題和最初的問題完全相同,只是變數quotient的值變小了。我們用剛剛編寫的函式(把整數轉換為各個數字字元並打印出來)來解決這個問題。由於quotient的值越來越小,所以遞迴最終會終止。

  一旦你理解了遞迴,閱讀遞迴函式最容易的方法不是糾纏於它的執行過程,而是相信遞迴函式會順利完成它的任務。如果你的每個步驟正確無誤,你的限制條件設定正確,並且每次呼叫之後更接近限制條件,遞迴函式總是能正確的完成任務。

  但是,為了理解遞迴的工作原理,你需要追蹤遞迴呼叫的執行過程,所以讓我們來進行這項工作。追蹤一個遞迴函式的執行過程的關鍵是理解函式中所宣告的變數是如何儲存的。當函式被呼叫時,它的變數的空間是創建於執行時堆疊上的。以前呼叫的函式的變數扔保留在堆疊上,但他們被新函式的變數所掩蓋,因此是不能被訪問的。

  當遞迴函式呼叫自身時,情況於是如此。每進行一次新的呼叫,都將建立一批變數,他們將掩蓋遞迴函式前一次呼叫所建立的變數。當我追蹤一個遞迴函式的執行過程時,必須把分數不同次呼叫的變數區分開來,以避免混淆。

  程式中的函式有兩個變數:引數value和區域性變數quotient。下面的一些圖顯示了堆疊的狀態,當前可以訪問的變數位於棧頂。所有其他呼叫的變數飾以灰色的陰影,表示他們不能被當前正在執行的函式訪問。

假定我們以4267這個值呼叫遞迴函式。當函式剛開始執行時,堆疊的內容如下圖所示:

執行除法之後,堆疊的內容如下:


接著,if語句判斷出quotient的值非零,所以對該函式執行遞迴呼叫。當這個函式第二次被呼叫之初,堆疊的內容如下:

堆疊上建立了一批新的變數,隱藏了前面的那批變數,除非當前這次遞迴呼叫返回,否則他們是不能被訪問的。再次執行除法運算之後,堆疊的內容如下:

quotient的值現在為42,仍然非零,所以需要繼續執行遞迴呼叫,並再建立一批變數。在執行完這次呼叫的出發運算之後,堆疊的內容如下:

此時,quotient的值還是非零,仍然需要執行遞迴呼叫。在執行除法運算之後,堆疊的內容如下:

  不算遞迴呼叫語句本身,到目前為止所執行的語句只是除法運算以及對quotient的值進行測試。由於遞迴呼叫這些語句重複執行,所以它的效果類似迴圈:當quotient的值非零時,把它的值作為初始值重新開始迴圈。但是,遞迴呼叫將會儲存一些資訊(這點與迴圈不同),也就好是儲存在堆疊中的變數值。這些資訊很快就會變得非常重要。

  現在quotient的值變成了零,遞迴函式便不再呼叫自身,而是開始列印輸出。然後函式返回,並開始銷燬堆疊上的變數值。

每次呼叫putchar得到變數value的最後一個數字,方法是對value進行模10取餘運算,其結果是一個0到9之間的整數。把它與字元常量‘0’相加,其結果便是對應於這個數字的ASCII字元,然後把這個字元打印出來。
輸出4:

接著函式返回,它的變數從堆疊中銷燬。接著,遞迴函式的前一次呼叫重新繼續執行,她所使用的是自己的變數,他們現在位於堆疊的頂部。因為它的value值是42,所以呼叫putchar後打印出來的數字是2。
輸出42:

接著遞迴函式的這次呼叫也返回,它的變數也被銷燬,此時位於堆疊頂部的是遞迴函式再前一次呼叫的變數。遞迴呼叫從這個位置繼續執行,這次列印的數字是6。在這次呼叫返回之前,堆疊的內容如下:
輸出426:

現在我們已經展開了整個遞迴過程,並回到該函式最初的呼叫。這次呼叫打印出數字7,也就是它的value引數除10的餘數。
輸出4267:

然後,這個遞迴函式就徹底返回到其他函式呼叫它的地點。
如果你把打印出來的字元一個接一個排在一起,出現在印表機或螢幕上,你將看到正確的值:4267

漢諾塔問題遞迴演算法分析:

  一個廟裡有三個柱子,第一個有64個盤子,從上往下盤子越來越大。要求廟裡的老和尚把這64個盤子全部移動到第三個柱子上。移動的時候始終只能小盤子壓著大盤子。而且每次只能移動一個。

  1、此時老和尚(後面我們叫他第一個和尚)覺得很難,所以他想:要是有一個人能把前63個盤子先移動到第二個柱子上,我再把最後一個盤子直接移動到第三個柱子,再讓那個人把剛才的前63個盤子從第二個柱子上移動到第三個柱子上,我的任務就完成了,簡單。所以他找了比他年輕的和尚(後面我們叫他第二個和尚),命令:

① 你丫把前63個盤子移動到第二柱子上

② 然後我自己把第64個盤子移動到第三個柱子上後

③ 你把前63個盤子移動到第三柱子上

2、第二個和尚接了任務,也覺得很難,所以他也和第一個和尚一樣想:要是有一個人能把前62個盤子先移動到第三個柱子上,我再把最後一個盤子直接移動到第二個柱子,再讓那個人把剛才的前62個盤子從第三個柱子上移動到第三個柱子上,我的任務就完成了,簡單。所以他也找了比他年輕的和尚(後面我們叫他第三和尚),命令:

① 你把前62個盤子移動到第三柱子上

② 然後我自己把第63個盤子移動到第二個柱子上後

③ 你把前62個盤子移動到第二柱子上

  3、第三個和尚接了任務,又把移動前61個盤子的任務依葫蘆話瓢的交給了第四個和尚,等等遞推下去,直到把任務交給了第64個和尚為止(估計第64個和尚很鬱悶,沒機會也命令下別人,因為到他這裡盤子已經只有一個了)。

  4、到此任務下交完成,到各司其職完成的時候了。完成回推了:

第64個和尚移動第1個盤子,把它移開,然後第63個和尚移動他給自己分配的第2個盤子。
第64個和尚再把第1個盤子移動到第2個盤子上。到這裡第64個和尚的任務完成,第63個和尚完成了第62個和尚交給他的任務的第一步。

  從上面可以看出,只有第64個和尚的任務完成了,第63個和尚的任務才能完成,只有第2個和尚----第64個和尚的任務完成後,第1個和尚的任務才能完成。這是一個典型的遞迴問題。 現在我們以有3個盤子來分析:

第1個和尚命令:

① 第2個和尚你先把第一柱子前2個盤子移動到第二柱子。(藉助第三個柱子)

② 第1個和尚我自己把第一柱子最後的盤子移動到第三柱子。

③ 第2個和尚你把前2個盤子從第二柱子移動到第三柱子。

   很顯然,第二步很容易實現(哎,人總是自私地,把簡單留給自己,困難的給別人)。

其中第一步,第2個和尚他有2個盤子,他就命令:

① 第3個和尚你把第一柱子第1個盤子移動到第三柱子。(藉助第二柱子)

② 第2個和尚我自己把第一柱子第2個盤子移動到第二柱子上。

③ 第3個和尚你把第1個盤子從第三柱子移動到第二柱子。

   同樣,第二步很容易實現,但第3個和尚他只需要移動1個盤子,所以他也不用在下派任務了。(注意:這就是停止遞迴的條件,也叫邊界值)

第三步可以分解為,第2個和尚還是有2個盤子,命令:

① 第3個和尚你把第二柱子上的第1個盤子移動到第一柱子。

② 第2個和尚我把第2個盤子從第二柱子移動到第三柱子。

③ 第3個和尚你把第一柱子上的盤子移動到第三柱子。

分析組合起來就是:1→3 1→2 3→2藉助第三個柱子移動到第二個柱子 |1→3自私人留給自己的活|2→1 2→3 1→3藉助第一個柱子移動到第三個柱子|共需要七步。

如果是4個盤子,則第一個和尚的命令中第1步和第3步各有3個盤子,所以各需要7步,共14步,再加上第1個和尚的1步,所以4個盤子總共需要移動7+1+7=15步,同樣,5個盤子需要15+1+15=31步,6個盤子需要31+1+31=64步……由此可以知道,移動n個盤子需要(2的n次方)-1步。

   從上面整體綜合分析可知把n個盤子從1座(相當第一柱子)移到3座(相當第三柱子):

(1)把1座上(n-1)個盤子藉助3座移到2座。
(2)把1座上第n個盤子移動3座。
(3)把2座上(n-1)個盤子藉助1座移動3座。

下面用hanoi(n,a,b,c)表示把1座n個盤子藉助2座移動到3座。

很明顯: (1)步上是 hanoi(n-1,1,3,2)
(3)步上是 hanoi(n-1,2,1,3)
用C語言表示出來,就是:
#include <stdio.h>
int method(int n,char a, char b)
{
printf("number..%d..form..%c..to..%c.."n",n,a,b);
return 0;
}
int hanoi(int n,char a,char b,char c)
{
if( n==1 ) move (1,a,c);
else
{
hanoi(n-1,a,c,b);
move(n,a,c);
hanoi(n-1,b,a,c);
};
return 0;
}
int main()
{
int num;
scanf("%d",&num);
hanoi(num,'A','B','C');
return 0;
}

相關推薦

演算法詳細分析> C

C通過執行時堆疊支援遞迴函式的實現。遞迴函式就是直接或間接呼叫自身的函式。 許多教科書都把計算機階乘和菲波那契數列用來說明遞迴,非常不幸我們可愛的著名的老潭老師的《C語言程式設計》一書中就是從階乘的計算開始的函式遞迴。導致讀過這本經書的同學們,看到階乘計算第一個想法就是遞迴

for迴圈和演算法的執行效率比較c語言

實驗目的 在程式語言中,對比不同程式設計風格的程式碼寫法,或者通過使用不同的編譯器和編譯優化引數,通過編譯器生成彙編程式碼,靜態分析所生成彙編程式碼的執行效率。 實驗平臺、工具 在window 7平臺下,採用vc++ 6.0編譯器來編寫相應的C程式,然後通過UltraCom

演算法求老鼠走迷宮C語言

  /*說明老鼠走迷宮是遞迴求解的基本題型,我們在二維陣列中使用2表示迷宮牆壁, 使用1來表示老鼠的行走路徑,試以程式求出由入口至出口的路徑。 解法老鼠的走法有上、左、下、右四個方向,在每前進一格之後就選一個方向前進, 無法前進時退回選擇下一個可前進方向,如此在陣列中依序測

C語言實現n的階乘n!

非負整數n的階乘可以表示為n! (讀作:n的階乘),其定義如下: n! = n·(n - 1)• (n - 2)· …·1 (n大於或等於l),且n = 0時,n! = l 例如,5 ! = 5·4·3·2·1 = 120。 請編寫一個程式,讀入一個非負整數,

演算法幾個例項---C/C++

//1.斐波那契數列 int fibo(int n) { if(n==1 || n==2) { return 1; } else { return fibo(n-1) + fibo(n-2); } } //2.

求組合數的實現,即求Cn,m

此法借鑑了2009年華為一筆試題我寫的一個遞迴演算法http://blog.csdn.net/challenge_c_plusplus/article/details/6640530排列數的遞迴實現見我的另一篇http://blog.csdn.net/challenge_c_

編譯原理--下降語法分析原始碼(C Language)

    花了一晚上寫的編譯原理作業,遞迴下降語法分析,實現'i'字元進行的+ - * / 操作,錯誤跳出(未完善錯誤提示),語法分析過程.  現把源程式貼出來,時間倉促,難免有錯誤請給與指正.   執行,例如輸入:i+i#                           

演算法案例分析

一、遞迴練習(斐波那契數列) 不死神兔 故事得從西元1202年說起,話說有一位義大利青年,名叫斐波那契。 在他的一部著作中提出了一個有趣的問題:假設一對剛出生的小兔一個月後就能長成大兔,再過一個月就能生下一對小兔,並且此後每個月都生一對小兔,一年內沒有發生死亡, 問:一

三種快速排序演算法的實現演算法、非演算法、三路劃分快速排序

快速排序的三個步驟: 1、分解:將陣列A[l...r]劃分成兩個(可能空)子陣列A[l...p-1]和A[p+1...r],使得A[l...p-1]中的每個元素都小於等於A(p),而且,小於等於A[p

求最大素因數java

可能經常進群會問這個群號的最大素因數是多少,或者演算法題中也會遇到。今天就寫一下求最大質因數的模板。 首先分析,怎麼求一個數的最大素因數。首先,我們以前求過最大因數,求最大因數的最暴力為2—n-1暴力查詢,但是這樣太超時了,後來發現在根號n前或者後某個區域查詢就行了。

原來可以so easy|-連載3

本期我們再通過幾個例子,加深遞迴的理解和熟練度。 上期有一個練習題:用遞迴逆序輸出一個包含整型資料的連結串列。 先完成這個練習題。 對於程式設計師來說,程式碼是最好的溝通工具,什麼都不說,上程式碼: public class Hello { public static void mai

原來可以so easy|-連載4

對遞迴進行優化--記憶化 遞迴可以很方便的解決很多問題,讓程式變得很簡潔。 但是,在遞迴解決問題的過程成,有時候會有很多重複計算,使得計算量很大,耗時很長。 比如,使用遞迴求斐波那契數列。 如果用普通的遞迴來解,當n值很大時,時間會很長而超時。 如圖,當n等於45時,需要執行5秒才能求出結果。

劍指offer演算法分析與整理

下面整理一下我在刷劍指offer時,自己做的和網上大神做的各種思路與答案,自己的程式碼是思路一,保證可以通過,網友的程式碼提供出處連結。 目錄 1、陣列中的逆序對 在陣列中的兩個數字,如果前面一個數字大於後面的數字,則這兩個數字組成一個逆序對。輸

《推薦系統》基於使用者和Item的協同過濾演算法分析與實現Python

開啟微信掃一掃,關注《資料與演算法聯盟》1:協同過濾演算法簡介2:協同過濾演算法的核心3:協同過濾演算法的應用方式4:基於使用者的協同過濾演算法實現5:基於物品的協同過濾演算法實現一:協同過濾演算法簡介    關於協同過濾的一個最經典的例子就是看電影,有時候不知道哪一部電影是

常見演算法分析---java實現

第一次部落格,有錯誤勿噴,謝謝! 一,如何不用比較運算子,比較兩個數的大小  分析:在計算機中判斷兩個數不用比較運算子,一般兩個數做減法 a-b,然後再去可以使用位運算子來進行判斷這個數的正負,從而知道大小,有兩個位運算,>>和>>>,>

需求分析、概要設計、詳細設計析義

<一> 需求分析(requirement analysis)       需求分析是當前軟體工程中的關鍵問題。需求分析階段的任務是:在可行性分析的基礎上,進一步瞭解、確定使用者需求,準確地回答“系統必須做什麼”的問題。獲得需求規格說明書。還涉及到軟體系統的

Android啟動過程分析——init.c

《Android框架揭祕》這本書是基於Android2.2原始碼的,但是手頭上只有Android4.4的原始碼。這兩個版本的啟動過程基本一致,但是在具體的編碼上,還是有一些區別的,下面,對照著這本書,分析一下4.4的init程序。 分析從main開始 首

Android啟動過程分析——init.c

Part 4 // ================================================== // Part 4 union selinux_callback cb; cb.func_log = klog_write;

PostgreSQL CPU滿(100%)性能分析及優化

mark ike -- 過多 mar 是不是 影響 sas sql日誌 PostgreSQL CPU滿(100%)性能分析及優化 轉自:https://help.aliyun.com/knowledge_detail/43562.html 在數據庫運維當中,

用於求最近公共祖先(LCA)的 Tarjan演算法–以POJ1986為例

給定有向無環圖(就是樹,不一定有沒有根),給定點U,V,找出點R,保證點R是U,V的公共祖先,且深度最深;或者理解為R離這兩個點的距離之和最小.如何找出R呢? 最一般的演算法是DFS(DFS本是深度優先搜尋,在這裡姑且把深度優先遍歷也叫做DFS,其實是