遞歸算法轉(zhuǎn)換為非遞歸算法的技巧

原文地址:http://www.cnblogs.com/coderkian/p/3758068.html
遞歸函數(shù)具有很好的可讀性和可維護(hù)性郊供,但是大部分情況下程序效率不如非遞歸函數(shù),所以在程序設(shè)計(jì)中一般喜歡先用遞歸解決問題,在保證方法正確的前提下再轉(zhuǎn)換為非遞歸函數(shù)以提高效率科展。
函數(shù)調(diào)用時(shí),需要在棧中分配新的幀述呐,將返回地址菇晃,調(diào)用參數(shù)和局部變量入棧。所以遞歸調(diào)用越深呆万,占用的椛淘矗空間越多。如果層數(shù)過深谋减,肯定會(huì)導(dǎo)致棧溢出牡彻,這也是消除遞歸的必要性之一。遞歸函數(shù)又可以分為尾遞歸和非尾遞歸函數(shù)出爹,前者往往具有很好的優(yōu)化效率庄吼,下面我們分別加以討論缎除。

尾遞歸函數(shù)

尾遞歸函數(shù)是指函數(shù)的最后一個(gè)動(dòng)作是調(diào)用函數(shù)本身的遞歸函數(shù),是遞歸的一種特殊情形总寻。尾遞歸具有兩個(gè)主要的特征:

  1. 調(diào)用自身函數(shù)(Self-called)磺浙;
  2. 計(jì)算僅占用常量椝跬幔空間(Stack Space)萨咕。
    為什么尾遞歸可以做到常量椀铮空間,我們用著名的fibonacci數(shù)列作為例子來說明殊轴。
    fibonacci數(shù)列實(shí)現(xiàn)方法一般是這樣的衰倦,
int FibonacciRecur(int n) {
  if (0==n) return 0;
  if (1==n) return 1;
  return FibonacciRecur(n-1)+FibonacciRecur(n-2);
}

不過需要注意的是這種實(shí)現(xiàn)方法并不是尾遞歸袒炉,因?yàn)槲策f歸的最后一個(gè)動(dòng)作必須是調(diào)用自身旁理,這里最后的動(dòng)作是加法運(yùn)算,所以我們要修改一下我磁,

int FibonacciTailRecur(int n, int acc1, int acc2) {
  if (0==n) return acc1;
  return FibonacciTailRecur(n-1, acc2, acc1+acc2);
}

好了孽文,現(xiàn)在符合尾遞歸的定義了,用gcc分別加-O和-O2選項(xiàng)編譯夺艰,下面是部分匯編代碼芋哭,

  • O2匯編代碼
FibonacciTailRecur:
.LFB12:
        testl   %edi, %edi
        movl    %esi, %eax
        movl    %edx, %esi
        je      .L4
        .p2align 4,,7
.L7:
        leal    (%rax,%rsi), %edx
        decl    %edi
        movl    %esi, %eax
        testl   %edi, %edi
        movl    %edx, %esi
        jne     .L7   // use jne .L4:
        rep ; ret
  • O匯編代碼

FibonacciTailRecur:
.LFB2:
        pushq   %rbp
.LCFI0:
        movq    %rsp, %rbp
.LCFI1:
        subq    $16, %rsp
.LCFI2:
        movl    %edi, -4(%rbp)
        movl    %esi, -8(%rbp)
        movl    %edx, -12(%rbp)
        cmpl    $0, -4(%rbp)
        jne     .L2
        movl    -8(%rbp), %eax
        movl    %eax, -16(%rbp)
        jmp     .L1
.L2:
        movl    -12(%rbp), %eax
        movl    -8(%rbp), %edx
        addl    %eax, %edx
        movl    -12(%rbp), %esi
        movl    -4(%rbp), %edi
        decl    %edi
        call     FibonacciTailRecur  //use call
        movl    %eax, -16(%rbp)
.L1:
        movl    -16(%rbp), %eax
        leave
        ret

可以看到-O2時(shí)用了jne命令,每次調(diào)用下層遞歸并沒有申請(qǐng)新的椨舾保空間减牺,而是更新當(dāng)前幀的局部數(shù)據(jù),重復(fù)使用當(dāng)前幀存谎,所以不管有多少層尾遞歸調(diào)用都不會(huì)棧溢出拔疚,這也是使用尾遞歸的意義所在。
而-O使用的是call命令既荚,這會(huì)申請(qǐng)新的椫墒В空間,也就是說gcc默認(rèn)狀態(tài)下并沒有優(yōu)化尾遞歸恰聘,這么做的一個(gè)主要原因是有時(shí)候我們需要保留幀信息用于調(diào)試句各,而加-O2優(yōu)化后,不管多少層尾遞歸調(diào)用晴叨,使用的都是第一層幀凿宾,是得不到當(dāng)前幀的信息的,大家可以用gdb調(diào)試下就知道了兼蕊。
除了尾遞歸初厚,F(xiàn)ibonacci數(shù)列很容易推導(dǎo)出循環(huán)實(shí)現(xiàn)方式,

int fibonacciNonRecur(int n) {
  int acc1 = 0, acc2 = 1;
  for(int i=0; i<n; i++){
    int t = acc1;
    acc1 = acc2;
    acc2 += t;
  }
  return acc1;
}

在我的機(jī)器上遍略,全部加-O2選項(xiàng)優(yōu)化編譯惧所,運(yùn)行時(shí)間如下(單位微秒)

n fibonacciNonRecur FibonacciTailRecur FibonacciRecur
20 1 1 123
30 1 1 14144

將fibonacci函數(shù)的迭代骤坐,尾遞歸和遞歸函數(shù)性能比較,可以發(fā)現(xiàn)迭代和尾遞歸時(shí)間幾乎一致下愈,n的大小對(duì)迭代和尾遞歸運(yùn)行時(shí)間影響很小纽绍,因?yàn)橹皇嵌鄨?zhí)行O(n)條機(jī)器指令而已。但是n對(duì)遞歸函數(shù)影響非常大势似,這是由于遞歸需要頻繁分配回收棸柘模空間所致。正是由于尾遞歸的高效率履因,在一些語言如lua中就明確建議使用尾遞歸(參照《lua程序設(shè)計(jì)第二版》第6章)障簿。

非尾遞歸函數(shù)

編譯器無法自動(dòng)優(yōu)化一般的遞歸函數(shù),不過通過模擬遞歸函數(shù)的過程栅迄,我們可以借助于棧將任何遞歸函數(shù)轉(zhuǎn)換為迭代函數(shù)站故。直觀點(diǎn),遞歸的過程其實(shí)是編譯器幫我們處理了壓棧和出棧的操作毅舆,轉(zhuǎn)換為迭代函數(shù)就需要手動(dòng)地處理壓棧和出棧西篓。
下面我們以經(jīng)典的快速排序?yàn)槔印?/p>

int partition(int *array, int low, int high) {
    int val = array[low];
    while(low < high) {
      while(low<high && array[high]>=val) --high;
     swap(&array[low], &array[high]);
     while(low<high && array[low]<=val) ++low;
     swap(&array[low], &array[high]);
   }
   return low;
 }
  void Quicksort(int *array, int b, int e) {
    if (b >= e) return;
   int p = partition(array, b, e);
    Quicksort(array, b, p-1);
    Quicksort(array, p+1, e);
 }

其實(shí)不難看出快速排序的遞歸算法就是一個(gè)二叉樹的先序遍歷過程,先處理當(dāng)前根節(jié)點(diǎn)憋活,然后依次處理左子樹和右子樹岂津。將快速排序遞歸算法轉(zhuǎn)換為非遞歸相當(dāng)于將二叉樹先序遍歷遞歸算法轉(zhuǎn)為非遞歸算法。
二叉樹先序遍歷遞歸算法偽碼

void PreorderRecursive(Bitree root){
  if (root) {
    visit(root);
    PreorderRecursive(root->lchild); 
    PreorderRecursive(root->rchild); 
  }
}

二叉樹先序遍歷非遞歸偽碼

void PreorderNonRecursive(Bitree root){
  stack stk;
  stk.push(root);
  while(!stk.empty()){
    p = stk.top();
    visit(p);
    stk.pop();
    if(p.rchild) stk.push(stk.rchild);
    if(p.lchild) stk.push(stk.lchild);
  }
}

每次處理完當(dāng)前節(jié)點(diǎn)后將右子樹和左子樹分別入棧悦即,類似地吮成,我們也很容易得到快速排序的非遞歸算法實(shí)現(xiàn)。partition將數(shù)組分為左右兩部分辜梳,相當(dāng)與處理當(dāng)前節(jié)點(diǎn)粱甫,接下來要做的就是將左右子樹入棧,那么左右子樹需要保存什么信息呢冗美?這個(gè)是處理非遞歸函數(shù)的關(guān)鍵魔种,因?yàn)楸徽{(diào)用函數(shù)信息需要壓入棧中》弁荩快速排序只需要保存子數(shù)組的邊界即可节预。

void QuicksortNonRecur(int *array, int b, int e) {
  if (b >= e) return;
  std::stack< std::pair<int, int> > stk;
  stk.push(std::make_pair(b, e));
  while(!stk.empty()) {
    std::pair<int, int> pair = stk.top();
    stk.pop();
    if(pair.first >= pair.second) continue;
    int p = partition(array, pair.first, pair.second);
    if(p < pair.second) stk.push(std::make_pair(p+1, e));
    if(p > pair.first) stk.push(std::make_pair(b, p-1));
  }
}

總結(jié)

雖然將遞歸函數(shù)轉(zhuǎn)換為迭代函數(shù)可以提高程序效率,但是轉(zhuǎn)換后的迭代函數(shù)往往可讀性差属韧,難以理解安拟,不易維護(hù)。所以只有在特殊情況下宵喂,比如對(duì)椏飞猓空間有嚴(yán)格要求的嵌入式系統(tǒng),才需要轉(zhuǎn)換遞歸函數(shù)。大部分情況下拙泽,遞歸并不會(huì)成為系統(tǒng)的性能瓶頸淌山,一個(gè)代碼簡單易讀的遞歸函數(shù)常常比迭代函數(shù)更易維護(hù)。

Reference:

https://secweb.cs.odu.edu/~zeil/cs361/web/website/Lectures/recursionConversion/page/recursionConversion.html
http://en.wikipedia.org/wiki/Tail_Recursion
http://c2.com/cgi/wiki?TailRecursion
http://c2.com/cgi/wiki?RecursionVsLoop

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末顾瞻,一起剝皮案震驚了整個(gè)濱河市泼疑,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌荷荤,老刑警劉巖退渗,帶你破解...
    沈念sama閱讀 221,198評(píng)論 6 514
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異蕴纳,居然都是意外死亡会油,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,334評(píng)論 3 398
  • 文/潘曉璐 我一進(jìn)店門古毛,熙熙樓的掌柜王于貴愁眉苦臉地迎上來翻翩,“玉大人,你說我怎么就攤上這事喇潘√逭叮” “怎么了?”我有些...
    開封第一講書人閱讀 167,643評(píng)論 0 360
  • 文/不壞的土叔 我叫張陵颖低,是天一觀的道長。 經(jīng)常有香客問我弧烤,道長忱屑,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 59,495評(píng)論 1 296
  • 正文 為了忘掉前任暇昂,我火速辦了婚禮莺戒,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘急波。我一直安慰自己从铲,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 68,502評(píng)論 6 397
  • 文/花漫 我一把揭開白布澄暮。 她就那樣靜靜地躺著名段,像睡著了一般。 火紅的嫁衣襯著肌膚如雪泣懊。 梳的紋絲不亂的頭發(fā)上伸辟,一...
    開封第一講書人閱讀 52,156評(píng)論 1 308
  • 那天,我揣著相機(jī)與錄音馍刮,去河邊找鬼信夫。 笑死,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的静稻。 我是一名探鬼主播警没,決...
    沈念sama閱讀 40,743評(píng)論 3 421
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼振湾!你這毒婦竟也來了惠奸?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,659評(píng)論 0 276
  • 序言:老撾萬榮一對(duì)情侶失蹤恰梢,失蹤者是張志新(化名)和其女友劉穎佛南,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體嵌言,經(jīng)...
    沈念sama閱讀 46,200評(píng)論 1 319
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡嗅回,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 38,282評(píng)論 3 340
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了摧茴。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片绵载。...
    茶點(diǎn)故事閱讀 40,424評(píng)論 1 352
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖苛白,靈堂內(nèi)的尸體忽然破棺而出娃豹,到底是詐尸還是另有隱情,我是刑警寧澤购裙,帶...
    沈念sama閱讀 36,107評(píng)論 5 349
  • 正文 年R本政府宣布懂版,位于F島的核電站,受9級(jí)特大地震影響躏率,放射性物質(zhì)發(fā)生泄漏躯畴。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,789評(píng)論 3 333
  • 文/蒙蒙 一薇芝、第九天 我趴在偏房一處隱蔽的房頂上張望蓬抄。 院中可真熱鬧,春花似錦夯到、人聲如沸嚷缭。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,264評(píng)論 0 23
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽阅爽。三九已至,卻和暖如春逼争,著一層夾襖步出監(jiān)牢的瞬間优床,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,390評(píng)論 1 271
  • 我被黑心中介騙來泰國打工誓焦, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留胆敞,地道東北人着帽。 一個(gè)月前我還...
    沈念sama閱讀 48,798評(píng)論 3 376
  • 正文 我出身青樓,卻偏偏與公主長得像移层,于是被迫代替她去往敵國和親仍翰。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,435評(píng)論 2 359

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

  • 感謝社區(qū)中各位的大力支持观话,譯者再次奉上一點(diǎn)點(diǎn)福利:阿里云產(chǎn)品券予借,享受所有官網(wǎng)優(yōu)惠,并抽取幸運(yùn)大獎(jiǎng):點(diǎn)擊這里領(lǐng)取 在...
    HetfieldJoe閱讀 1,813評(píng)論 0 14
  • 前言 眾所周知频蛔,遞歸函數(shù)容易爆棧灵迫,究其原因,便是函數(shù)調(diào)用前需要先將參數(shù)晦溪、運(yùn)行狀態(tài)壓棧瀑粥,而遞歸則會(huì)導(dǎo)致函數(shù)的多次無返...
    灼弦閱讀 929評(píng)論 1 4
  • 函數(shù)參數(shù)的默認(rèn)值 基本用法 在ES6之前,不能直接為函數(shù)的參數(shù)指定默認(rèn)值三圆,只能采用變通的方法狞换。 上面代碼檢查函數(shù)l...
    呼呼哥閱讀 3,398評(píng)論 0 1
  • 1.函數(shù)參數(shù)的默認(rèn)值 (1).基本用法 在ES6之前,不能直接為函數(shù)的參數(shù)指定默認(rèn)值舟肉,只能采用變通的方法修噪。
    趙然228閱讀 693評(píng)論 0 0
  • 本文有七千字黄琼,閱讀大約需要占用你10分鐘時(shí)間。 好吧磷籍。适荣。隨便寫的,我也不知道會(huì)花多久看完院领。因?yàn)閷懙谋容^爛,而且只是...
    鍋與盆閱讀 8,105評(píng)論 5 36