1. 程式人生 > >什麼是尾遞迴,尾遞迴的優勢以及語言支援情況說明

什麼是尾遞迴,尾遞迴的優勢以及語言支援情況說明

今天在進行資料排序時候用到遞迴,但是耗費記憶體太大,於是想找一找有沒有既提升效率又節省記憶體的演算法,然後發現尾遞迴確實不錯,只可惜php並沒有對此作優化支援.

雖然如此,但還是學習了,下面總結一下:

尾遞迴 --概念

如果一個函式中所有遞迴形式的呼叫都出現在函式的末尾,我們稱這個遞迴函式是尾遞迴的。當遞迴呼叫是整個函式體中最後執行的語句且它的返回值不屬於表示式的一部分時,這個遞迴呼叫就是尾遞迴。尾遞迴函式的特點是在迴歸過程中不用做任何操作,這個特性很重要,因為大多數現代的編譯器會利用這種特點自動生成優化的程式碼。

例項

為了理解尾遞迴是如何工作的,讓我們再次以遞迴的形式計算階乘。首先,這可以很容易讓我們理解為什麼之前所定義的遞迴不是尾遞迴。回憶之前對計算n!的定義:在每個活躍期計算n倍的(n-1)!的值,讓n=n-1並持續這個過程直到n=1為止。這種定義不是尾遞迴的,因為每個活躍期的返回值都依賴於用n乘以下一個活躍期的返回值,因此每次呼叫產生的棧幀將不得不儲存在棧上直到下一個子呼叫的返回值確定。現在讓我們考慮以尾遞迴的形式來定義計算n!的過程。 這種定義還需要接受第二個引數a,除此之外並沒有太大區別。a(初始化為1)維護遞迴層次的深度。這就讓我們避免了每次還需要將返回值再乘以n。然而,在每次遞迴呼叫中,令a=na並且n=n-1。繼續遞迴呼叫,直到n=1,這滿足結束條件,此時直接返回a即可。 程式碼例項3-2給出了一個C函式facttail,它接受一個整數n並以尾遞迴的形式計算n的階乘。這個函式還接受一個引數a,a的初始值為1。facttail使用a來維護遞迴層次的深度,除此之外它和fact很相似。讀者可以注意一下函式的具體實現和尾遞迴定義的相似之處。 示例3-2:以尾遞迴的形式計算階乘的一個函式實現
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 /*facttail.c*/ #include"facttail.h" /*facttail*/ int facttail(int n, int a) { /*Compute a factorialina tail - recursive manner.*/ if (n < 0) return 0;     else if (n == 0) return 1;     else if (n == 1) return a; else return facttail(n - 1, n * a); }
示例3-2中的函式是尾遞迴的,因為對facttail的單次遞迴呼叫是函式返回前最後執行的一條語句。在facttail中碰巧最後一條語句也是對facttail的呼叫,但這並不是必需的。換句話說,在遞迴呼叫之後還可以有其他的語句執行,只是它們只能在遞迴呼叫沒有執行時才可以執行 。 尾遞迴是極其重要的,不用尾遞迴,函式的堆疊耗用難以估量,需要儲存很多中間函式的堆疊。比如f(n, sum) = f(n-1) + value(n) + sum; 會儲存n個函式呼叫堆疊,而使用尾遞迴f(n, sum) = f(n-1, sum+value(n)); 這樣則只保留後一個函式堆疊即可,之前的可優化刪去。 也許在C語言中有很多的特例,但程式語言不只有
C語言
,在函式式語言Erlang中(亦是棧語言),如果想要保持語言的高併發特性,就必須用尾遞迴來替代傳統的遞迴。

尾遞迴與傳統遞迴比較

以下是具體例項: 線性遞迴:
1 2 3 4 5 long Rescuvie(long n) { return (n == 1) ? 1 : n * Rescuvie(n - 1); }
尾遞迴:
1 2 3 4 5 6 7 8 9 10 11 12 long TailRescuvie(long n, long a) { return (n == 1) ? a : TailRescuvie(n - 1, a * n);
} long TailRescuvie(long n) {//封裝用的 return (n == 0) ? 1 : TailRescuvie(n, 1); }
當n = 5時 對於傳統線性遞迴, 他的遞迴過程如下:
Rescuvie(5)

{5 * Rescuvie(4)}

{5 * {4 * Rescuvie(3)}}

{5 * {4 * {3 * Rescuvie(2)}}}

{5 * {4 * {3 * {2 * Rescuvie(1)}}}}

{5 * {4 * {3 * {2 * 1}}}}

{5 * {4 * {3 * 2}}}

{5 * {4 * 6}}

{5 * 24}

120

對於尾遞迴, 他的遞迴過程如下:
TailRescuvie(5)                  // 所以在運算上和記憶體佔用上節省了很多,直接傳回結果

TailRescuvie(5, 1)                         return 120
                                                 ↑
TailRescuvie(4, 5)                         return 120
                                                 ↑
TailRescuvie(3, 20)                        return 120
                                                 ↑
TailRescuvie(2, 60)                        return 120
                                                 ↑
TailRescuvie(1, 120)                       return 120
                                                 ↑
120                                //當執行到最後時,return a => return 120 ,將120返回上一級

說明:

尾遞迴的效果就是去除了將下層的結果再次返回給上層,需要上層繼續計算才得出結果的弊端,如果仔細觀看例子就可以看出,其實每個遞迴的結果是儲存在第二個引數a中的,到最後一次計算的時候,會只返回一個a的值,但是因為是遞迴的原理雖然仍然要返回給上層,依次到頂部才給出結果,但是不需要再做計算了,這點的好處就是每次分配的記憶體不會因為遞迴而擴大。

在效率上,兩者的確差不多。


尾遞迴的優勢

        與普通遞迴相比,由於尾遞迴的呼叫處於方法的最後,因此方法之前所積累下的各種狀態對於遞迴呼叫結果已經沒有任何意義,因此完全可以把本次方法中留在堆疊中的資料完全清除,把空間讓給最後的遞迴呼叫。這樣的優化便使得遞迴不會在呼叫堆疊上產生堆積,意味著即時是“無限”遞迴也不會讓堆疊溢位。這便是尾遞迴的優勢。

編譯器優化支援尾遞迴說明:

尾遞迴在某些語言的實現上,能避免上述所說的問題,注意是某些語言上,尾遞迴本身並不能消除函式呼叫棧過長的問題,那什麼是尾遞迴呢?在上面寫的一般遞迴函式 func() 中,我們可以看到,func(n)  是依賴於 func(n-1) 的,func(n) 只有在得到 func(n-1) 的結果之後,才能計算它自己的返回值,因此理論上,在 func(n-1) 返回之前,func(n),不能結束返回。因此func(n)就必須保留它在棧上的資料,直到func(n-1)先返回,而尾遞迴的實現則可以在編譯器的幫助下,消除這個限制

讓我們先回顧一下函式呼叫的大概過程:

1)呼叫開始前,呼叫方(或函式本身)會往棧上壓相關的資料,引數,返回地址,區域性變數等。

2)執行函式。

3)清理棧上相關的資料,返回。

因此,在函式 A 執行的時候,如果在第二步中,它又呼叫了另一個函式 B,B 又呼叫 C.... 棧就會不斷地增長不斷地裝入資料,當這個呼叫鏈很深的時候,棧很容易就滿 了,這就是一般遞迴函式所容易面臨的大問題。

一直在強調,尾遞迴的實現依賴於編譯器的幫助(或者說語言的規定),為什麼這樣說呢?先看下面的程式:

複製程式碼
 1 #include <stdio.h>
 2 
 3 int tail_func(int n, int res)
 4 {
 5      if (n <= 1) return res;
 6 
 7      return tail_func(n - 1, n * res);
 8 }
 9 
10 
11 int main()
12 {
13     int dummy[1024*1024]; // 儘可能佔用棧。
14     
15     tail_func(2048*2048, 1);
16     
17     return 1;
18 }
複製程式碼

上面這個程式在開了編譯優化和沒開編譯優化的情況下編出來的結果是不一樣的,如果不開啟優化,直接 gcc -o tr func_tail.c 編譯然後執行的話,程式會爆棧崩潰,但如果開優化的話:gcc -o tr -O2 func_tail.c,上面的程式最後就能正常執行。

這裡面的原因就在於,尾遞迴的寫法只是具備了使當前函式在呼叫下一個函式前把當前佔有的棧銷燬,但是會不會真的這樣做,是要具體看編譯器是否最終這樣做,如果在語言層面上,沒有規定要優化這種尾呼叫,那編譯器就可以有自己的選擇來做不同的實現,在這種情況下,尾遞迴就不一定能解決一般遞迴的問題。

我們可以先看看上面的例子在開優化與沒開優化的情況下,編譯出來的彙編程式碼有什麼不同,首先是沒開優化編譯出來的彙編tail_func:

複製程式碼
            
           

相關推薦

什麼是,優勢以及語言支援情況說明

今天在進行資料排序時候用到遞迴,但是耗費記憶體太大,於是想找一找有沒有既提升效率又節省記憶體的演算法,然後發現尾遞迴確實不錯,只可惜php並沒有對此作優化支援. 雖然如此,但還是學習了,下面總結一下: 尾遞迴 --概念 如果一個函式中所有遞迴形式的呼叫都出現在函式的末尾

LeetCode刷題Easy篇斐波那契數列問題(,,非和動態規劃解法)

題目 斐波那契數列:  f(n)=f(n-1)+f(n-2)(n>2) f(0)=1;f(1)=1;  即有名的兔子繁衍問題  1 1 2 3 5 8 13 21 .... 我的解法 遞迴 public static int Recursion

呼叫與

1、什麼是尾呼叫?        尾呼叫用一句話說清楚就是,指某個函式的最後一步是呼叫另一個函式。 例1:function     f(x) {        return g(x); }        上面程式碼中,函式f最後一步是呼叫函式g,這就是尾呼叫了。

js實現優化),防止棧溢位

一、一版的遞迴實現 n!,比如 5!= 5 * 4 * 3 * 2 *1       function fact(n) {             if(n == 1) {

關於呼叫和

1. (1)尾呼叫:指某個函式的最後一步是呼叫另一個函式。 例如: function a(n){ return b(n); } (最後一步呼叫並不意味著在函式的尾部,只要是最後一步即可) function a(n){ if(n>

DocumentBuilder 解析xml,適合android 以及java

package cn; import java.io.File; import javax.xml.parsers.DocumentBuilder; import javax.xml.parsers.DocumentBuilderFactory; import org.w

python中的函式,以及函式的可變引數,函式和高階函式以及練習題目

函式作用:實現程式碼的複用 函式概念:函式是組織好的,可重複使用的,用來實現單一,或相關聯功能的程式碼段。 函式能提高應用的模組性,和程式碼的重複利用率。你已經知道Python提供了許多內建函式,比如print()。但你也可以自己建立函式,這被叫做使用者自定義函式。 系統的幾

建立二叉樹以及一些基本操作

基本內容 二叉樹的基本概念和遍歷方式 使用遞迴建立一個簡單的二叉樹 二叉樹使用遞迴遍歷時的呼叫棧幀 實現程式碼 一些基本操作的遞迴實現 一、一些基本的東西 首先我們要明確,二叉樹是一種資料結構,相比於之前的順序表和連結串列,二叉樹是一種非線性結

oracle 查詢屬於本部門以及下屬部門

oracle使用遞迴查詢要使用 connect by語句 下面給出一個例項: select PUBCB_id,PUBCB001,PUBCB002 from TB_PUBCB start with PUBCB_id = 136 connect by prior PUBCB_

二叉樹基本演算法,遍歷以及求高度、寬度等

二叉樹基本演算法,遍歷以及求高度、寬度等路徑 轉自Powered by: C++部落格   Copyright © 天一程 //二叉樹的相關演算法,《資料結構習題與解析》7.3 //演算法 49個,程式碼量1200+ ,時間9小時 #include<

歸,歸,回溯

total 方法調用 返回 系列 其實在 dfs 容易 遞歸 做什麽 一、首先我們講講遞歸 遞歸的本質是,某個方法中調用了自身。本質還是調用一個方法,只是這個方法正好是自身而已 遞歸因為是在自身中調用自身,所以會帶來以下三個顯著特點: 調用的是同一個方法 因為1,所以

二叉樹前序、中序、後序( / 非)遍歷

前語  二叉樹的遍歷是指按一定次序訪問二叉樹中的每一個結點,且每個節點僅被訪問一次。 前序遍歷  若二叉樹非空,則進行以下次序的遍歷:   根節點—>根節點的左子樹—>根節點的右子樹   若要遍歷左子樹和右子樹,仍然需要按照以上次序進行,所以前序遍歷也是一個遞

java實現二分查詢演算法,兩種方式實現,非

java實現二分查詢演算法 1、概念 2、前提 3、思想 4、過程 4、複雜度 5、實現方式 1. 非遞迴方式 2. 遞迴方式

_CH0303_實現排列型列舉_演算法正確性證明範例

點此開啟題目頁面 先給出AC程式碼, 然後給出程式正確性的形式化證明. //CH0303_遞迴實現排列型列舉 #include <iostream> #include <cstdio> #include <vector> using namespace

_CH0302_實現組合型列舉_演算法正確性證明範例

點此開啟題目頁面 先給出AC程式碼, 然後給出程式正確性的形式化證明 //CH0302_遞迴實現組合型列舉 #include <iostream> #include <cstdio> #include <vector> using namespace

_CH0301_實現指數型列舉_演算法正確性證明範例

點此開啟題目頁面     簡而言之本題要求列印集{1, 2,..., n}的所有子集(列印時每個子集中的所有元素位於同一行, 每行中的元素遞增列印, 空集對應空行) 先給出如下AC程式碼, 然後給出其正確性的形式化證明 //CH0301_遞迴實現指數型列舉 #in

數的計數(推、揹包、規律、優化、複雜度)

題目描述 我們要求找出具有下列性質數的個數(包括輸入的自然數n)。先輸入一個自然數n(n≤1000),然後對此自然數按照如下方法進行處理: 不作任何處理; 在它的左邊加上一個自然數,但該自然數不能超過原數的一半; 加上數後,繼續按此規則進行處理,直到不能再加自然數為止。 輸入輸出格

【程式設計學習記錄】迴轉非

想要知道怎麼遞迴轉非遞迴,就得先弄明白遞迴函式呼叫和返回的步驟(來源於網課): 呼叫 儲存呼叫資訊(引數,返回地址) 分配資料區(區域性變數) 控制轉移給被調函式的入口 返回 儲存返回資訊 釋放資料區 控制轉移到上級函式 因為遞迴滿足L

小論c語言

分享一下我老師大神的人工智慧教程!零基礎,通俗易懂!http://blog.csdn.net/jiangjunshow 也歡迎大家轉載本篇文章。分享知識,造福人民,實現我們中華民族偉大復興!        

二叉樹的前中後序遍歷(+非

/** * 二叉樹節點類 * @author wj * */ class TreeNode{ int value; TreeNode left_Node; TreeNode right_Node; public TreeNode(int value) { this.value