尾遞迴(tail recursion) 的簡單使用
摘要
本文首先論述了尾遞迴的定義,然後通過例項進行講解尾遞迴的本質,最後給出了兩個例子完整程式碼。
引導:
什麼是遞迴?通俗的說,就是在一個函式中不斷呼叫自己的這麼一種呼叫形式。遞迴函式的設計,就是截止條件+呼叫本身。大家都知道,遞迴的資源消耗是發生在棧中,在呼叫函式時候,通常需要儲存相應的資源,比如引數,返回地址,區域性變數等。那麼進行大量的遞迴,就會造成棧的溢位。windows下棧為 1M。那麼是什麼導致了溢位呢?就是在進行遞迴的時候,本身遞迴函式的狀態,沒有完全執行完畢,需要藉助於下一個狀態來完成,那麼上一個狀態必然要進行儲存。這樣就導致了大量的中間過程。才肯能在回溯遞迴的時候,一一使用,最後拼湊出原來的結果。
比如階乘函式:
int factorial(int n)
{
if(n==0)
return 1;
else
return n*factorial(n-1);
}
在上述的函式中,返回的值並不是一個完全獨立的狀態,而需要儲存n這樣一種額外需維護的變數,每層遞迴都需要這樣一個變數,那麼在下次呼叫的時候,就會重新建立新的棧幀來進行儲存。每次都是這樣。所以對於大數而言,就可能溢位了。那麼仔細觀察這種遞迴的本質,就是因為建立了很多中間過程需要儲存,才導致了這樣一種空間的浪費。那麼我們在設計遞迴函式的時候,能不能直接省略中間變數,直接返回結果呢?下面就是尾遞迴的設計思路了。什麼是尾遞迴:
本質就是不產生出中間過程,將中間過程作為函式的引數,傳入到下次呼叫中去。比如上面階乘函式,使用尾遞迴設計後。
int factorial_tailcursion(int n,int acc)
{
if(n==0)
return acc;
else
return factorial_tailcursion(n-1,acc*n);
}
在呼叫的時候 factorial_tailcursion(n,1) 需要一個初始累計器acc=1。可以看出,在每次呼叫完畢尾遞迴形式的階乘函式後,根本沒有產生人中間過程,中間變數都通過引數傳遞給下一次呼叫。從而節省了棧的開銷。因為對於某些編譯器而言,會自動識別到這一動作,並且認為每次呼叫過程都是獨立的,那麼就不需要每次都開闢新的 棧幀來儲存當前值,而是直接就地覆蓋調之前的棧。那麼其空間複雜度將為O(1)。這就是尾遞迴的本質。說白了,也就是在進行遞迴設計的時候,儘量省略中間過程。下面是兩個例子的完整程式碼:
/***********************************************************************
two examples of tail recursion
1. factorial
2. fibonacci
*********************************************************************/
#include <iostream>
using namespace std;
int factorial(int n) // usual recursion call
{
if(n==0)
return 1;
else
return n*factorial(n-1);
}
int factorial_tailrecursion(int n,int acc) // tail recursion call
{
if(n==0)
return acc;
else
return factorial_tailrecursion(n-1,acc*n);
}
int fibinacci(int n)
{
if(n<2)
return n;
else
return fibinacci(n-1)+fibinacci(n-2);
}
int fibinacci_tailrecursion(int n,int acc1,int acc2)
{
if(n==0)
return acc1;
else
return fibinacci_tailrecursion(n-1,acc1+acc2,acc1);
}
int main()
{
puts("usual factor recursion 4!");
cout<<factorial(4)<<endl;
puts("tail recursion 4!");
cout<<factorial_tailrecursion(4,1)<<endl;
puts("usual fibinacc ");
for(int i=0;i<10;i++)
cout<<fibinacci(i)<<" ";
cout<<endl;
puts("tail recursion ");
for(int i=0;i<10;i++)
cout<<fibinacci_tailrecursion(i,0,1)<<" ";
return 0;
}
小結:
遞迴函式的使用,無疑對一些複雜的問題,可以清晰的反應其本質,使得易於理解。everything is banlance!有利必有弊,我們必須儘量減少這中弊。
更詳細的參考:老趙這篇文章
相關文章
- JavaScript, ABAP和Scala裡的尾遞迴(Tail Recursion)JavaScript遞迴AI
- 通過階乘的例子,練習在JavaScript, Scala和ABAP裡實現尾遞迴(Tail Recursion)JavaScript遞迴AI
- 遞迴和尾遞迴遞迴
- JavaScript之遞迴的簡單使用JavaScript遞迴
- 尾呼叫和尾遞迴遞迴
- ?30 秒瞭解尾遞迴和尾遞迴優化遞迴優化
- 淺談尾遞迴遞迴
- JavaScript和ABAP的尾遞迴JavaScript遞迴
- 簡單的加減乘除(遞迴)遞迴
- 遞迴尾呼叫優化遞迴優化
- 尾遞迴以及優化遞迴優化
- 演算法小專欄:遞迴與尾遞迴演算法遞迴
- Javascript中的尾遞迴及其優化JavaScript遞迴優化
- 二分法的簡單實現——-遞迴和非遞迴遞迴
- 遞迴優化:尾呼叫和Memoization遞迴優化
- 尾遞迴實現深複製遞迴
- Python技法:實現簡單的遞迴下降ParserPython遞迴
- 函數語言程式設計之尾呼叫和尾遞迴函數程式設計遞迴
- 簡單C#遞迴(向前查詢上工序)C#遞迴
- Python3之遞迴函式簡單示例Python遞迴函式
- 週而復始,往復迴圈,遞迴、尾遞迴演算法與無限極層級結構的探究和使用(Golang1.18)遞迴演算法Golang
- JS尾遞迴優化斐波拉契數列JS遞迴優化
- C# 遞迴的使用案例C#遞迴
- Vue 遞迴多級選單Vue遞迴
- Java遞迴演算法的使用Java遞迴演算法
- 尾遞迴 - 杜絕記憶體洩漏溢位爆棧遞迴記憶體
- 簡單連結串列——尾插法
- Linux 基本命令 -------- tail 的使用LinuxAI
- 舉例說明你對尾遞迴的理解,有哪些應用場景遞迴
- 什麼是遞迴?遞迴和迴圈的異同遞迴
- 面試官:用“尾遞迴”優化斐波那契函式面試遞迴優化函式
- 快速排序【遞迴】【非遞迴】排序遞迴
- el-menu使用遞迴元件實現多級選單元件遞迴元件
- 使用遞迴實現樹狀選單(無限級分類)遞迴
- Oracle with使用方法以及遞迴Oracle遞迴
- Linux中tail命令的使用詳解!LinuxAI
- Understanding Recursion
- Java實現多級選單(遞迴)Java遞迴
- 遞迴遞迴