日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪問 生活随笔!

生活随笔

當前位置: 首頁 > 编程资源 > 编程问答 >内容正文

编程问答

漫谈递归和迭代

發布時間:2025/3/21 编程问答 28 豆豆
生活随笔 收集整理的這篇文章主要介紹了 漫谈递归和迭代 小編覺得挺不錯的,現在分享給大家,幫大家做個參考.

先講個故事吧。

從前有座山,山里有座廟,廟里有個老和尚,正在給小和尚講故事呢!故事是什么呢?“從前有座山,山里有座廟,廟里有個老和尚,正在給小和尚講故事呢!故事是什么呢?‘從前有座山,山里有座廟,廟里有個老和尚,正在給小和尚講故事呢!故事是什么呢?……’”。

這個故事永遠也講不完,因為沒有遞歸結束條件。老師講遞歸時總是說,遞歸很簡單,一個遞歸結束條件,一個自己調用自己。如果遞歸沒有結束條件,那么就會無限遞歸下去。在編程的時候,沒有遞歸結束條件或者遞歸過深,一般會造成棧溢出。

下面這個函數,可以利用棧溢出來估測棧的大小:

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

這個函數定義了1M的局部變量,然后調用自己。棧溢出時會崩潰,根據最后打印出的數字可以算一下棧的大小。

遞歸算法一般用于解決三類問題:

(1)數據的定義是按遞歸定義的。(Fibonacci函數)

(2)問題解法按遞歸算法實現。(回溯)

(3)數據的結構形式是按遞歸定義的。(樹的遍歷,圖的搜索)

對于求1+2+3+…+n這種問題,大部分人不會用遞歸方式求解:

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


迭代算法是用計算機解決問題的一種基本方法。它利用計算機運算速度快、適合做重復性操作的特點,讓計算機對一組指令(或一定步驟)進行重復執行,在每次執行這組指令(或這些步驟)時,都從變量的原值推出它的一個新值。

為什么使用迭代而不用遞歸呢?

很明顯,使用遞歸時每調用一次,就需要在棧上開辟一塊空間,而使用遞歸就不需要了,因此,很多時候設計出了遞歸算法,還要想法設法修改成迭代算法。

假如現在我們不考慮編程,我們僅僅看一下上面使用遞歸和迭代求1+2+3…+n的過程。

使用遞歸:

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

上面兩個計算過程所需的步驟都是O(n)。但是兩個計算過程的形狀不一樣。

遞歸過程是一個先逐步展開而后收縮的形狀,在展開階段,這一計算過程構造起一個推遲進行的操作所形成的的鏈條(這里是+),在收縮階段才會實際執行這些操作。這種類型的計算過程由一個推遲執行的運算鏈條刻畫,稱為一個遞歸計算過程。要執行這種計算過程,就需要維護以后將要執行的操作的軌跡。在計算1+2+3+…+n時,推遲執行的加法鏈條的長度就是為了保存其軌跡需要保存的信息量,這個長度隨著n值而線性增長,這樣的過程稱為線性遞歸過程。

迭代過程的形成沒有任何增長或收縮。對于任意一個n,在計算的每一步,我們需要保存的就只有i,ret,這個過程就是一個迭代計算過程。一般來說,迭代計算過程就是那種其狀態可以用固定數目的狀態變量描述的結算過程。在計算1+2+…+n時,所需的計算步驟與n成正比,這種過程稱為線性迭代過程。

現在再回到編程語言中。

上面提到的推遲執行的運算鏈條就存在棧里,由于棧很小,如果鏈條太長,就會溢出了。

那我們再來看下面的函數

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

調用的時候acc=0,以sum(5,0)為例這是一個遞歸函數,我們來看看它的計算過程。

sum(5,0)
sum(4,5)
sum(3,9)
sum(2,12)
sum(1,14)
sum(0,15)
15

這個計算過程是遞歸的還是迭代的呢?

是迭代的!

但是命名函數sum又調用了自己。

我們需要將遞歸計算過程與遞歸過程分隔開。

當我們說一個過程(函數)是遞歸的時候,論述的是一個語法形式上的事實,說明這個過程的定義中(直接或間接的)調用了自己。我們說一個計算過程具有某種模式時(例如線性遞歸),我們說的是這一計算過程的進展方式,而不是過程說些上的語法形式。

一個遞歸過程,如果它的計算過程是迭代的,那么我們稱這種遞歸為尾遞歸。尾遞歸不需要保存遞歸的推遲計算鏈,那么是不是就意味著不會造成棧溢出了?

我們來試一下

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


運行結果

看來還是會棧溢出。

為啥呢?因為c語言默認不會對尾遞歸進行優化,即使你的程序是尾遞歸的,它還是按一般的遞歸進行編譯。加上優化選項就可以對尾遞歸進行優化。

下面哪些是尾遞歸呢?

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

在函數式編程語言中,不存在變量,因此任何的循環都需要用遞歸實現。如果遞歸使用了尾遞歸,那么編譯器或解釋器能自動優化,如果不是尾遞歸,那么就存在棧溢出的風險。前面兩個不是尾遞歸,第三個是尾遞歸。

任何遞歸都可以轉化成迭代,那么任何遞歸都可以轉化成尾遞歸。

斐波那契數列改成尾遞歸后如下

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;

對應的遞歸形式就是

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)相當于給i和ret賦初值。

如果將快速排序改成迭代的話,那么需要一個棧!它的變量個數是有限的嗎?我們可以把棧看成一個變量就可以了。

先修改成迭代形式

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

那么在函數式編程語言里,快排是不是就是這樣實現的?答案是No。函數式編程為什么不能用循環?就是因為沒有變量,所以在函數式編程語言里不能進行原地排序的。

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實現的,不是尾遞歸的。

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)

其實剛才我說謊了,大部分函數式編程語言,例如Scheme,Erlang,Clojure等都提供可變的變量,數據庫里有上G的數據,不能把它拷貝一份在寫回去,這時候就需要使用真正的變量了。函數式編程語言都是比較高級的語言,排序時一般使用自帶的sort函數就行了。上面這段代碼沒有對變量做修改的操作,所以可以看做是函數式編程。這個函數能改成尾遞歸嗎?應該是可以的,但是挺麻煩的,我是沒找到好辦法。到網上找了找也沒找到好的方法。

總結一下尾遞歸:
(1)計算過程是迭代的
(2)在函數最后一步調用自己,而且是僅有調用語句,或者是一句fun(),或者是return fun(),不存在x = fun()這樣的情況
(3)函數執行最后一句調用自己的語句時,將狀態變量以參數形式傳遞給下一次調用,自己的棧沒用了,形象的說,它告訴下一次被調用的函數,我已經死了,你干完活后直接向我的上級報告就行了,不需要和我說了
(4)gcc開啟優化選項后可以對尾遞歸進行優化,大部分函數式編程語言會對尾遞歸進行優化



本文轉自nxlhero 51CTO博客,原文鏈接:http://blog.51cto.com/nxlhero/1231228,如需轉載請自行聯系原作者


總結

以上是生活随笔為你收集整理的漫谈递归和迭代的全部內容,希望文章能夠幫你解決所遇到的問題。

如果覺得生活随笔網站內容還不錯,歡迎將生活随笔推薦給好友。