漫谈递归和迭代
先講個(gè)故事吧。
從前有座山,山里有座廟,廟里有個(gè)老和尚,正在給小和尚講故事呢!故事是什么呢?“從前有座山,山里有座廟,廟里有個(gè)老和尚,正在給小和尚講故事呢!故事是什么呢?‘從前有座山,山里有座廟,廟里有個(gè)老和尚,正在給小和尚講故事呢!故事是什么呢?……’”。
這個(gè)故事永遠(yuǎn)也講不完,因?yàn)闆](méi)有遞歸結(jié)束條件。老師講遞歸時(shí)總是說(shuō),遞歸很簡(jiǎn)單,一個(gè)遞歸結(jié)束條件,一個(gè)自己調(diào)用自己。如果遞歸沒(méi)有結(jié)束條件,那么就會(huì)無(wú)限遞歸下去。在編程的時(shí)候,沒(méi)有遞歸結(jié)束條件或者遞歸過(guò)深,一般會(huì)造成棧溢出。
下面這個(gè)函數(shù),可以利用棧溢出來(lái)估測(cè)棧的大小:
| 1 2 3 4 5 6 7 8 | void?stack_size() { ????static?int?call_time = 0; ????char?dummy[1024*1024]; ????call_time++; ????printf("call time: %d\n",call_time); ????stack_size(); } |
這個(gè)函數(shù)定義了1M的局部變量,然后調(diào)用自己。棧溢出時(shí)會(huì)崩潰,根據(jù)最后打印出的數(shù)字可以算一下棧的大小。
遞歸算法一般用于解決三類(lèi)問(wèn)題:
(1)數(shù)據(jù)的定義是按遞歸定義的。(Fibonacci函數(shù))
(2)問(wèn)題解法按遞歸算法實(shí)現(xiàn)。(回溯)
(3)數(shù)據(jù)的結(jié)構(gòu)形式是按遞歸定義的。(樹(shù)的遍歷,圖的搜索)
對(duì)于求1+2+3+…+n這種問(wèn)題,大部分人不會(huì)用遞歸方式求解:
| 1 2 3 4 5 6 7 | int?sum1(int?n) { ????if(n == 0) ????????return?0; ????else ????????return?n+sum1(n-1); } |
而是使用迭代的方式:
| 1 2 3 4 5 6 7 | int?sum2(int?n) { ????int?ret =?0; ????for(int?i =?1;? i <= n; i++) ??????????????ret += i; ????return?ret; } |
迭代算法是用計(jì)算機(jī)解決問(wèn)題的一種基本方法。它利用計(jì)算機(jī)運(yùn)算速度快、適合做重復(fù)性操作的特點(diǎn),讓計(jì)算機(jī)對(duì)一組指令(或一定步驟)進(jìn)行重復(fù)執(zhí)行,在每次執(zhí)行這組指令(或這些步驟)時(shí),都從變量的原值推出它的一個(gè)新值。
為什么使用迭代而不用遞歸呢?
很明顯,使用遞歸時(shí)每調(diào)用一次,就需要在棧上開(kāi)辟一塊空間,而使用遞歸就不需要了,因此,很多時(shí)候設(shè)計(jì)出了遞歸算法,還要想法設(shè)法修改成迭代算法。
假如現(xiàn)在我們不考慮編程,我們僅僅看一下上面使用遞歸和迭代求1+2+3…+n的過(guò)程。
使用遞歸:
sum(5)
5+sum(4)
5+4+sum(3)
5+4+3+sum(2)
5+4+3+2+sum(1)
5+4+3+2+1+sum(0)
5+4+3+2+1+0
5+4+3+2+1
5+4+3+3
5+4+6
5+10
15
使用迭代
0+1=1
1+2=3
3+3=6
6+4=10
10+5=15
上面兩個(gè)計(jì)算過(guò)程所需的步驟都是O(n)。但是兩個(gè)計(jì)算過(guò)程的形狀不一樣。
遞歸過(guò)程是一個(gè)先逐步展開(kāi)而后收縮的形狀,在展開(kāi)階段,這一計(jì)算過(guò)程構(gòu)造起一個(gè)推遲進(jìn)行的操作所形成的的鏈條(這里是+),在收縮階段才會(huì)實(shí)際執(zhí)行這些操作。這種類(lèi)型的計(jì)算過(guò)程由一個(gè)推遲執(zhí)行的運(yùn)算鏈條刻畫(huà),稱(chēng)為一個(gè)遞歸計(jì)算過(guò)程。要執(zhí)行這種計(jì)算過(guò)程,就需要維護(hù)以后將要執(zhí)行的操作的軌跡。在計(jì)算1+2+3+…+n時(shí),推遲執(zhí)行的加法鏈條的長(zhǎng)度就是為了保存其軌跡需要保存的信息量,這個(gè)長(zhǎng)度隨著n值而線性增長(zhǎng),這樣的過(guò)程稱(chēng)為線性遞歸過(guò)程。
迭代過(guò)程的形成沒(méi)有任何增長(zhǎng)或收縮。對(duì)于任意一個(gè)n,在計(jì)算的每一步,我們需要保存的就只有i,ret,這個(gè)過(guò)程就是一個(gè)迭代計(jì)算過(guò)程。一般來(lái)說(shuō),迭代計(jì)算過(guò)程就是那種其狀態(tài)可以用固定數(shù)目的狀態(tài)變量描述的結(jié)算過(guò)程。在計(jì)算1+2+…+n時(shí),所需的計(jì)算步驟與n成正比,這種過(guò)程稱(chēng)為線性迭代過(guò)程。
現(xiàn)在再回到編程語(yǔ)言中。
上面提到的推遲執(zhí)行的運(yùn)算鏈條就存在棧里,由于棧很小,如果鏈條太長(zhǎng),就會(huì)溢出了。
那我們?cè)賮?lái)看下面的函數(shù)
| 1 2 3 4 5 6 7 | int?sum3(int?n,?int?acc) { ????if(n == 0) ????????return?acc; ????else ????????return?sum3(n-1,acc+n); } |
調(diào)用的時(shí)候acc=0,以sum(5,0)為例這是一個(gè)遞歸函數(shù),我們來(lái)看看它的計(jì)算過(guò)程。
sum(5,0)
sum(4,5)
sum(3,9)
sum(2,12)
sum(1,14)
sum(0,15)
15
這個(gè)計(jì)算過(guò)程是遞歸的還是迭代的呢?
是迭代的!
但是命名函數(shù)sum又調(diào)用了自己。
我們需要將遞歸計(jì)算過(guò)程與遞歸過(guò)程分隔開(kāi)。
當(dāng)我們說(shuō)一個(gè)過(guò)程(函數(shù))是遞歸的時(shí)候,論述的是一個(gè)語(yǔ)法形式上的事實(shí),說(shuō)明這個(gè)過(guò)程的定義中(直接或間接的)調(diào)用了自己。我們說(shuō)一個(gè)計(jì)算過(guò)程具有某種模式時(shí)(例如線性遞歸),我們說(shuō)的是這一計(jì)算過(guò)程的進(jìn)展方式,而不是過(guò)程說(shuō)些上的語(yǔ)法形式。
一個(gè)遞歸過(guò)程,如果它的計(jì)算過(guò)程是迭代的,那么我們稱(chēng)這種遞歸為尾遞歸。尾遞歸不需要保存遞歸的推遲計(jì)算鏈,那么是不是就意味著不會(huì)造成棧溢出了?
我們來(lái)試一下
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 | int?sum3(int?n,?int?acc) { ????if(n == 0) ????????return?acc; ????else ????????return?sum3(n-1,acc+n); } int?main() { ????int?n; ????scanf("%d",&n); ????printf("%d\n",sum(n,0)); ????return?0; } |
運(yùn)行結(jié)果
看來(lái)還是會(huì)棧溢出。
為啥呢?因?yàn)閏語(yǔ)言默認(rèn)不會(huì)對(duì)尾遞歸進(jìn)行優(yōu)化,即使你的程序是尾遞歸的,它還是按一般的遞歸進(jìn)行編譯。加上優(yōu)化選項(xiàng)就可以對(duì)尾遞歸進(jìn)行優(yōu)化。
下面哪些是尾遞歸呢?
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | int?fib(int?n) { ????if(n == 0 || n == 1) ????????return?1; ????else ????????return?fib(n-1) + fib(n-2); } void?qsort(int?A,?int?p,?int?q) { ????r = partition(A,p,q); ????qsort(A,p,r-1); ????qsort(A,r+1,q); } int?gcd(int?a,?int?b) { ????if(b == 0) ????????return?a; ????else ????????gcd(b, a%b); } |
在函數(shù)式編程語(yǔ)言中,不存在變量,因此任何的循環(huán)都需要用遞歸實(shí)現(xiàn)。如果遞歸使用了尾遞歸,那么編譯器或解釋器能自動(dòng)優(yōu)化,如果不是尾遞歸,那么就存在棧溢出的風(fēng)險(xiǎn)。前面兩個(gè)不是尾遞歸,第三個(gè)是尾遞歸。
任何遞歸都可以轉(zhuǎn)化成迭代,那么任何遞歸都可以轉(zhuǎn)化成尾遞歸。
斐波那契數(shù)列改成尾遞歸后如下
| 1 2 3 4 5 6 7 8 9 10 11 12 13 | int?fib(int?n,int?count,?int?a ,?int?b) { ????if(n == 0 || n == 1) ????????return?1; ????else?if?(count > n) ????????return?b; ????else ????????return?fib(n,count+1,b,a+b); } int?FIB(int?n) { ????return?fib(n,2,1,1); } |
下面這段代碼
| 1 2 3 | i = 1, ret = 0 for(;i <= n; i++) ????????ret += i; |
對(duì)應(yīng)的遞歸形式就是
| 1 2 3 4 5 6 | int?fun(int?i,?int?ret) { ????if(i > n) ????????return?ret; ????else ????????return?fun(ret+i,i+1); } |
fun(1,0)相當(dāng)于給i和ret賦初值。
如果將快速排序改成迭代的話,那么需要一個(gè)棧!它的變量個(gè)數(shù)是有限的嗎?我們可以把棧看成一個(gè)變量就可以了。
先修改成迭代形式
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 | void?qsort_iterate(int?a[],int?p,int?q) { ????????stack s; ????????s.push(p); ????????s.push(q); ????????while(!s.empty()) ????????{ ????????????????int?high = s.top(); ????????????????s.pop(); ????????????????int?low = s.top(); ????????????????s.pop(); ????????????????if(high > low) ????????????????{ ????????????????????????int?r = partition(a,low,high); ????????????????????????s.push(low); ????????????????????????s.push(r-1); ????????????????????????s.push(r+1); ????????????????????????s.push(high); ????????????????} ????????} } |
上面的迭代形式可以很容易的改成尾遞歸:
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | void?qsort_tail(int?a[],stack s) { ????????if(!s.empty()) ????????{ ????????????????int?high = s.top(); ????????????????s.pop(); ????????????????int?low = s.top(); ????????????????s.pop(); ????????????????if(high > low) ????????????????{ ????????????????????????int?r = partition(a,low,high); ????????????????????????s.push(low); ????????????????????????s.push(r-1); ????????????????????????s.push(r+1); ????????????????????????s.push(high); ????????????????} ????????????????qsort_tail(a,s); ????????} } |
那么在函數(shù)式編程語(yǔ)言里,快排是不是就是這樣實(shí)現(xiàn)的?答案是No。函數(shù)式編程為什么不能用循環(huán)?就是因?yàn)闆](méi)有變量,所以在函數(shù)式編程語(yǔ)言里不能進(jìn)行原地排序的。
| 1 2 3 4 5 6 7 8 | (define (qsort s) ??(cond ((null? s) s) ????????((null? (cdr s)) s) ????????(else ?????????(let ((h (car s)) ???????????????(left (filter (lambda (x) (<= x (car s))) (cdr s))) ???????????????(right (filter (lambda (x) (> x (car s))) (cdr s)))) ???????????(append (qsort left) (list h) (qsort right)))))) |
我們把這段代碼翻譯成Python(翻譯成C或者C++挺啰嗦的)上面這段代碼是用Lisp的方言Scheme實(shí)現(xiàn)的,不是尾遞歸的。
| 1 2 3 4 5 6 7 8 9 10 11 12 13 | def qsort_lisp(A): ????if?len(A) == 0 or len(A) == 1: ????????return?A ????left = [] ????right = [] ????pivot = A[0] ????for?i in range(1,len(A)): ????????if?A[i]???????????? left.append(A[i]); ????????else: ????????????right.append(A[i]); ????return?qsort_lisp(left) + [pivot] + qsort_lisp(right) x = [3,4,5,6,2,34,6,2,2,5,7,2,7] print qsort_lisp(x) |
其實(shí)剛才我說(shuō)謊了,大部分函數(shù)式編程語(yǔ)言,例如Scheme,Erlang,Clojure等都提供可變的變量,數(shù)據(jù)庫(kù)里有上G的數(shù)據(jù),不能把它拷貝一份在寫(xiě)回去,這時(shí)候就需要使用真正的變量了。函數(shù)式編程語(yǔ)言都是比較高級(jí)的語(yǔ)言,排序時(shí)一般使用自帶的sort函數(shù)就行了。上面這段代碼沒(méi)有對(duì)變量做修改的操作,所以可以看做是函數(shù)式編程。這個(gè)函數(shù)能改成尾遞歸嗎?應(yīng)該是可以的,但是挺麻煩的,我是沒(méi)找到好辦法。到網(wǎng)上找了找也沒(méi)找到好的方法。
總結(jié)一下尾遞歸:
(1)計(jì)算過(guò)程是迭代的
(2)在函數(shù)最后一步調(diào)用自己,而且是僅有調(diào)用語(yǔ)句,或者是一句fun(),或者是return fun(),不存在x = fun()這樣的情況
(3)函數(shù)執(zhí)行最后一句調(diào)用自己的語(yǔ)句時(shí),將狀態(tài)變量以參數(shù)形式傳遞給下一次調(diào)用,自己的棧沒(méi)用了,形象的說(shuō),它告訴下一次被調(diào)用的函數(shù),我已經(jīng)死了,你干完活后直接向我的上級(jí)報(bào)告就行了,不需要和我說(shuō)了
(4)gcc開(kāi)啟優(yōu)化選項(xiàng)后可以對(duì)尾遞歸進(jìn)行優(yōu)化,大部分函數(shù)式編程語(yǔ)言會(huì)對(duì)尾遞歸進(jìn)行優(yōu)化
本文轉(zhuǎn)自nxlhero 51CTO博客,原文鏈接:http://blog.51cto.com/nxlhero/1231228,如需轉(zhuǎn)載請(qǐng)自行聯(lián)系原作者
總結(jié)
- 上一篇: Nagios设置报警间隔
- 下一篇: 超级简单的配置虚拟机网络yum源