尾遞歸


尾調(diào)用

計算機科學(xué)里冲甘,尾調(diào)用是指一個函數(shù)里的最后一個動作是一個函數(shù)調(diào)用的情形:即這個調(diào)用的返回值直接被當(dāng)前函數(shù)返回的情形荷憋。這種情形下稱該調(diào)用位置為尾位置写半。若這個函數(shù)在尾位置調(diào)用本身(或是一個尾調(diào)用本身的其他函數(shù)等等)吏恭,則稱這種情況為尾遞歸惠豺,是遞歸的一種特殊情形辽故。尾調(diào)用不一定是遞歸調(diào)用徒仓,但是尾遞歸特別有用,也比較容易實現(xiàn)誊垢。

在程序運行時掉弛,計算機會為應(yīng)用程序分配一定的內(nèi)存空間症见;應(yīng)用程序則會自行分配所獲得的內(nèi)存空間,其中一部分被用于記錄程序中正在調(diào)用的各個函數(shù)的運行情況狰晚,這就是函數(shù)的調(diào)用棧筒饰。常規(guī)的函數(shù)調(diào)用總是會在調(diào)用棧最上層添加一個新的堆棧幀(stack frame,也翻譯為“棧幀”或簡稱為“幀”)壁晒,這個過程被稱作“入棿擅牵”或“壓棧”(意即把新的幀壓在棧頂)秒咐。當(dāng)函數(shù)的調(diào)用層數(shù)非常多時谬晕,調(diào)用棧會消耗不少內(nèi)存,甚至?xí)伪瑑?nèi)存空間(棧溢出[1]携取,造成程序嚴重卡頓或意外崩潰攒钳。尾調(diào)用的調(diào)用棧則特別易于優(yōu)化,從而可減少內(nèi)存空間的使用雷滋,也能提高運行速度不撑。[1]其中,對尾遞歸情形的優(yōu)化效果最為明顯晤斩,尤其是遞歸算法非常復(fù)雜的情形焕檬。[1]

一般來說,尾調(diào)用消除是可選的澳泵,可以用实愚,也可以不用。然而兔辅,在函數(shù)編程語言中腊敲,語言標準通常會要求編譯器或運行平臺實現(xiàn)尾調(diào)用消除。這讓程序員可以用遞歸取代循環(huán)而不喪失性能维苔。

定義

尾調(diào)用 (tail call) 指的是一個函數(shù)的最后一條語句也是一個返回調(diào)用函數(shù)的語句碰辅。在函數(shù)體末尾被返回的可以是對另一個函數(shù)的調(diào)用,也可以是對自身調(diào)用(即自身遞歸調(diào)用)


尾遞歸

若函數(shù)在尾位置調(diào)用自身(或是一個尾調(diào)用本身的其他函數(shù)等等)蕉鸳,則稱這種情況為尾遞歸乎赴。尾遞歸也是遞歸的一種特殊情形。尾遞歸是一種特殊的尾調(diào)用潮尝,即在尾部直接調(diào)用自身的遞歸函數(shù)榕吼。對尾遞歸的優(yōu)化也是關(guān)注尾調(diào)用的主要原因。尾調(diào)用不一定是遞歸調(diào)用勉失,但是尾遞歸特別有用羹蚣,也比較容易實現(xiàn)。

尾遞歸在普通尾調(diào)用的基礎(chǔ)上乱凿,多出了2個特征:

  • 在尾部調(diào)用的是函數(shù)自身 (Self-called)顽素;
  • 可通過優(yōu)化咽弦,使得計算僅占用常量棧空間 (Stack Space)胁出。

優(yōu)化尾遞歸的分析與示例

對函數(shù)調(diào)用在尾位置的遞歸或互相遞歸的函數(shù)型型,由于函數(shù)自身調(diào)用次數(shù)很多,遞歸層級很深全蝶,尾遞歸優(yōu)化則使原本 O(n) 的調(diào)用椖炙猓空間只需要 O(1)。因此一些編程語言的標準要求語言實現(xiàn)進行尾調(diào)用消除
Swift為例

 func sum(_ n: UInt) -> UInt {
        if n == 0 {
            return 0
        }
        return n + sum(n - 1)
    }

調(diào)用sum(5)為例抑淫。相應(yīng)的棻谅洌空間變化

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)))
5 + (4 + (3 + 3))
5 + (4 + 6)
5 + 10
15

可觀察,堆棧從左到右始苇,增加到一個峰值后再計算從右到左縮小砌烁,這往往是我們不希望的,所以在C語言等語言中設(shè)計for, while, goto等特殊結(jié)構(gòu)語句催式,使用迭代函喉、尾遞歸,對普通遞歸進行優(yōu)化荣月,減少可能對內(nèi)存的極端消耗函似。修改以上代碼,可以成為尾遞歸:

    func tailSum(_ n: UInt) -> UInt {
        func sumInternal(_ n: UInt, current: UInt) -> UInt {
            if n == 0 {
                return current
            } else {
                return sumInternal(n - 1, current: current + n)
            }
        }
        
        return sumInternal(n, current: 0)
    }

對比后者尾遞歸對內(nèi)存的消耗

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

則是線性的喉童。


調(diào)用棧

計算機科學(xué)中存儲有關(guān)正在運行的子程序的消息的。有時僅稱“”顿天,但棧中不一定僅存儲子程序消息堂氯。幾乎所有計算機程序都依賴于調(diào)用棧,然而高級語言一般將調(diào)用棧的細節(jié)隱藏至后臺牌废。

調(diào)用棧最經(jīng)常被用于存放子程序的返回地址咽白。在調(diào)用任何子程序時,主程序都必須暫存子程序運行完畢后應(yīng)該返回到的地址鸟缕。因此晶框,如果被調(diào)用的子程序還要調(diào)用其他的子程序,其自身的返回地址就必須存入調(diào)用棧懂从,在其自身運行完畢后再行取回授段。在遞歸程序中,每一層次遞歸都必須在調(diào)用棧上增加一條地址番甩,因此如果程序出現(xiàn)無限遞歸(或僅僅是過多的遞歸層次)侵贵,調(diào)用棧就會產(chǎn)生棧溢出


Swift中編譯器在Debug 模式下并不會對尾遞歸進行優(yōu)化缘薛。我們可以在 scheme 設(shè)置中將 Run 的配置 改為 Release窍育。


階乘的例子

    //普通遞歸
    func notailFactorial(_ n:Int) -> Int {
        if n == 1 {
            return 1
        }
        return n * notailFactorial(n-1)
    }
    //尾遞歸
    func factorial(_ n: Int) -> Int {
        func iter(product:Int , counter: Int = 1) -> Int {
            if product <= 1{
               return counter
            } else {
                return iter(product: product-1, counter: counter+product)
            }
        }
        return iter(product: n)
    }
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末卡睦,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子漱抓,更是在濱河造成了極大的恐慌表锻,老刑警劉巖,帶你破解...
    沈念sama閱讀 211,817評論 6 492
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件乞娄,死亡現(xiàn)場離奇詭異瞬逊,居然都是意外死亡,警方通過查閱死者的電腦和手機补胚,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,329評論 3 385
  • 文/潘曉璐 我一進店門码耐,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人溶其,你說我怎么就攤上這事骚腥。” “怎么了瓶逃?”我有些...
    開封第一講書人閱讀 157,354評論 0 348
  • 文/不壞的土叔 我叫張陵束铭,是天一觀的道長。 經(jīng)常有香客問我厢绝,道長契沫,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 56,498評論 1 284
  • 正文 為了忘掉前任昔汉,我火速辦了婚禮懈万,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘靶病。我一直安慰自己会通,他們只是感情好,可當(dāng)我...
    茶點故事閱讀 65,600評論 6 386
  • 文/花漫 我一把揭開白布娄周。 她就那樣靜靜地躺著涕侈,像睡著了一般。 火紅的嫁衣襯著肌膚如雪煤辨。 梳的紋絲不亂的頭發(fā)上裳涛,一...
    開封第一講書人閱讀 49,829評論 1 290
  • 那天,我揣著相機與錄音众辨,去河邊找鬼端三。 笑死,一個胖子當(dāng)著我的面吹牛鹃彻,可吹牛的內(nèi)容都是我干的技肩。 我是一名探鬼主播,決...
    沈念sama閱讀 38,979評論 3 408
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼虚婿!你這毒婦竟也來了旋奢?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 37,722評論 0 266
  • 序言:老撾萬榮一對情侶失蹤然痊,失蹤者是張志新(化名)和其女友劉穎至朗,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體剧浸,經(jīng)...
    沈念sama閱讀 44,189評論 1 303
  • 正文 獨居荒郊野嶺守林人離奇死亡锹引,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,519評論 2 327
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了唆香。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片嫌变。...
    茶點故事閱讀 38,654評論 1 340
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖躬它,靈堂內(nèi)的尸體忽然破棺而出腾啥,到底是詐尸還是另有隱情,我是刑警寧澤冯吓,帶...
    沈念sama閱讀 34,329評論 4 330
  • 正文 年R本政府宣布倘待,位于F島的核電站,受9級特大地震影響组贺,放射性物質(zhì)發(fā)生泄漏凸舵。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 39,940評論 3 313
  • 文/蒙蒙 一失尖、第九天 我趴在偏房一處隱蔽的房頂上張望啊奄。 院中可真熱鬧,春花似錦掀潮、人聲如沸增热。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,762評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至公黑,卻和暖如春邑商,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背凡蚜。 一陣腳步聲響...
    開封第一講書人閱讀 31,993評論 1 266
  • 我被黑心中介騙來泰國打工人断, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人朝蜘。 一個月前我還...
    沈念sama閱讀 46,382評論 2 360
  • 正文 我出身青樓恶迈,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子暇仲,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 43,543評論 2 349

推薦閱讀更多精彩內(nèi)容

  • 調(diào)用棧(Call Stack) 調(diào)用棧(Call Stack)是一個基本的計算機概念奈附,這里引入一個概念:棧幀全度。 棧...
    SherHoooo閱讀 4,197評論 2 14
  • 編程很復(fù)雜,編程也很簡單斥滤。簡單的邏輯将鸵,通過代碼組織,就可以變成復(fù)雜程序或者系統(tǒng)佑颇。以前學(xué)物理的時候顶掉,老師就說考試的物...
    人世間閱讀 3,380評論 4 15
  • 1.遞歸與尾遞歸 1.1 遞歸 1.1.1 遞歸定義 遞歸大家都不陌生,一個函數(shù)直接或間接的調(diào)用它自己本身挑胸,就是遞...
    楓葉_huazhe閱讀 2,891評論 0 3
  • 前言 眾所周知痒筒,遞歸函數(shù)容易爆棧,究其原因嗜暴,便是函數(shù)調(diào)用前需要先將參數(shù)凸克、運行狀態(tài)壓棧,而遞歸則會導(dǎo)致函數(shù)的多次無返...
    灼弦閱讀 918評論 1 4
  • 第一次在公眾號上發(fā)文章闷沥,僅僅只是記錄自己的生活小事和一些日常感受萎战,可能有小女兒情節(jié)的矯情可能有過于虛妄的看法,有什...
    leeali閱讀 138評論 0 0