【Scala】尾遞歸優(yōu)化

以遞歸方式思考

遞歸通過靈巧的函數(shù)定義溃卡,告訴計(jì)算機(jī)做什么。在函數(shù)式編程中,隨處可見遞歸思想的運(yùn)用参滴。
下面給出幾個(gè)遞歸函數(shù)的例子:

object RecursiveExample extends App{
  // 數(shù)列求和例子
  def sum(xs: List[Int]): Int =
    if (xs.isEmpty)
      1
    else
      xs.head + sum(xs.tail)


  // 求最大值例子
  def max(xs: List[Int]): Int =
    if (xs.isEmpty)
      throw new NoSuchElementException
    else if (xs.size == 1)// 遞歸的邊界條件
      xs.head
    else
      if (xs.head > max(xs.tail)) xs.head else max(xs.tail)


  // 翻轉(zhuǎn)字符串
  def str_reverse(xs: String): String =
    if (xs.length == 1)
      xs
    else
      str_reverse(xs.tail) + xs.head

  // 快速排序例子
  def quicksort(ls: List[Int]): List[Int] = {
    if (ls.isEmpty)
      ls
    else
      quicksort(ls.filter(_ < ls.head)) ::: ls.head :: quicksort(ls.filter(_ > ls.head))
      //quicksort(ls.filter(x =>  x < ls.head)) ::: ls.head :: quicksort(ls.filter(x => x > ls.head))
  }
}

我們以上面代碼最后一個(gè)快速排序函數(shù)為例,使用遞歸的方式锻弓,其代碼實(shí)現(xiàn)非常的簡(jiǎn)潔和通俗易懂砾赔。遞歸函數(shù)的核心是設(shè)計(jì)好遞歸表達(dá)式,并且確定算法的邊界條件青灼。上面的快速排序中暴心,認(rèn)為空列表就是排好序的列表,這就是遞歸的邊界條件杂拨,這個(gè)條件是遞歸終止的標(biāo)志专普。

尾遞歸

遞歸算法需要保持調(diào)用堆棧,效率較低弹沽,如果調(diào)用次數(shù)較多檀夹,會(huì)耗盡內(nèi)存或棧溢出。然而策橘,尾遞歸可以克服這一缺點(diǎn)炸渡。
尾遞歸是指遞歸調(diào)用是函數(shù)的最后一個(gè)語句,而且其結(jié)果被直接返回役纹,這是一類特殊的遞歸調(diào)用偶摔。由于遞歸結(jié)果總是直接返回,尾遞歸比較方便轉(zhuǎn)換為循環(huán)促脉,因此編譯器容易對(duì)它進(jìn)行優(yōu)化辰斋。

遞歸求階乘的經(jīng)典例子

普通遞歸求解的代碼如下:

def factorial(n: BigInt): BigInt = {
  if (n <= 1)
    1
  else
    n * factorial(n-1)
}

上面的代碼,由于每次遞歸調(diào)用n-1的階乘時(shí)瘸味,都有一次額外的乘法計(jì)算宫仗,這使得堆棧中的數(shù)據(jù)都需要保留。在新的遞歸中要分配新的函數(shù)棧旁仿。
運(yùn)行過程就像這樣:

factorial(4)
--------------
4 * factorial(3)
4 * (3 * factorial(2))
4 * (3 * (2 * factorial(1)))
4 * (3 * (2 * 1))

而下面是一個(gè)尾遞歸版本藕夫,在效率上孽糖,和循環(huán)是等價(jià)的:

import scala.annotation.tailrec

def factorialTailRecursive(n: BigInt): BigInt = {
  @tailrec
  def _loop(acc: BigInt, n: BigInt): BigInt =
    if(n <= 1) acc else _loop(acc*n, n-1)

  _loop(1, n)
}

這里的運(yùn)行過程如下:

factorialTailRecursive(4)
--------------------------
_loop(1, 4)
_loop(4, 3)
_loop(12, 2)
_loop(24, 1)

該函數(shù)中的_loop在最后一步,要么返回遞歸邊界條件的值毅贮,要么調(diào)用遞歸函數(shù)本身办悟。
改寫成尾遞歸版本的關(guān)鍵:
尾遞歸版本最重要的就是找到合適的累加器,該累加器可以保留最后一次遞歸調(diào)用留在堆棧中的數(shù)據(jù)滩褥,積累之前調(diào)用的結(jié)果病蛉,這樣堆棧數(shù)據(jù)就可以被丟棄,當(dāng)前的函數(shù)椆寮澹可以被重復(fù)利用铺然。
在這個(gè)例子中,變量acc就是累加器酒甸,每次遞歸調(diào)用都會(huì)更新該變量魄健,直到遞歸邊界條件滿足時(shí)返回該值。
對(duì)于尾遞歸插勤,Scala語言特別增加了一個(gè)注釋@tailrec沽瘦,該注釋可以確保程序員寫出的程序是正確的尾遞歸程序,如果由于疏忽大意农尖,寫出的不是一個(gè)尾遞歸程序其垄,則編譯器會(huì)報(bào)告一個(gè)編譯錯(cuò)誤,提醒程序員修改自己的代碼卤橄。

菲波那切數(shù)列的例子

原始的代碼很簡(jiǎn)單:

def fibonacci(n: Int): Int =
  if (n <= 2)
    1
  else
    fibonacci(n-1) + fibonacci(n-2)

尾遞歸版本用了兩個(gè)累加器绿满,一個(gè)保存較小的項(xiàng)acc1,另一個(gè)保存較大項(xiàng)acc2:

def fibonacciTailRecursive(n: Int): Int = {
  @tailrec
  def _loop(n: Int, acc1: Int, acc2: Int): Int =
    if(n <= 2)
      acc2
    else
      _loop(n-1, acc2, acc1+acc2)

  _loop(n, 1, 1)
}

幾個(gè)列表操作中使用尾遞歸的例子

求列表的長(zhǎng)度

def lengthTailRecursive[A](ls: List[A]): Int = {
  @tailrec
  def lengthR(result: Int, curList: List[A]): Int = curList match {
    case Nil => result
    case _ :: tail => lengthR(result+1, tail)
  }
  lengthR(0, ls)
}

翻轉(zhuǎn)列表

def reverseTailRecursive[A](ls: List[A]): List[A] = {
  @tailrec
  def reverseR(result: List[A], curList: List[A]): List[A] = curList match {
    case Nil        => result
    case h :: tail  => reverseR(h :: result, tail)
  }
  reverseR(Nil, ls)
}

去除列表中多個(gè)重復(fù)的元素

這里要求去除列表中多個(gè)連續(xù)的字符窟扑,只保留其中的一個(gè)喇颁。

// If a list contains repeated elements they should be replaced with
// a single copy of the element.
// The order of the elements should not be changed.
// Example:
// >> compress(List('a, 'a, 'a, 'a, 'b, 'c, 'c, 'a, 'a, 'd, 'e, 'e, 'e, 'e))
// >> List('a, 'b, 'c, 'a, 'd, 'e)

def compressTailRecursive[A](ls: List[A]): List[A] = {
  @tailrec
  def compressR(result: List[A], curList: List[A]): List[A] = curList match {
    case h :: tail  => compressR(h :: result, tail.dropWhile(_ == h))
    case Nil        => result.reverse
  }
  compressR(Nil, ls)
}

轉(zhuǎn)載請(qǐng)注明作者Jason Ding及其出處
Github博客主頁(http://jasonding1354.github.io/)
GitCafe博客主頁(http://jasonding1354.gitcafe.io/)
CSDN博客(http://blog.csdn.net/jasonding1354)
簡(jiǎn)書主頁(http://www.reibang.com/users/2bd9b48f6ea8/latest_articles)
**Google搜索jasonding1354進(jìn)入我的博客主頁

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市嚎货,隨后出現(xiàn)的幾起案子橘霎,更是在濱河造成了極大的恐慌,老刑警劉巖殖属,帶你破解...
    沈念sama閱讀 221,635評(píng)論 6 515
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件姐叁,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡洗显,警方通過查閱死者的電腦和手機(jī)外潜,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,543評(píng)論 3 399
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來挠唆,“玉大人处窥,你說我怎么就攤上這事⌒椋” “怎么了滔驾?”我有些...
    開封第一講書人閱讀 168,083評(píng)論 0 360
  • 文/不壞的土叔 我叫張陵谒麦,是天一觀的道長(zhǎng)。 經(jīng)常有香客問我哆致,道長(zhǎng)绕德,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 59,640評(píng)論 1 296
  • 正文 為了忘掉前任摊阀,我火速辦了婚禮迁匠,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘驹溃。我一直安慰自己,他們只是感情好延曙,可當(dāng)我...
    茶點(diǎn)故事閱讀 68,640評(píng)論 6 397
  • 文/花漫 我一把揭開白布豌鹤。 她就那樣靜靜地躺著,像睡著了一般枝缔。 火紅的嫁衣襯著肌膚如雪布疙。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 52,262評(píng)論 1 308
  • 那天愿卸,我揣著相機(jī)與錄音灵临,去河邊找鬼。 笑死趴荸,一個(gè)胖子當(dāng)著我的面吹牛儒溉,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播发钝,決...
    沈念sama閱讀 40,833評(píng)論 3 421
  • 文/蒼蘭香墨 我猛地睜開眼顿涣,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來了酝豪?” 一聲冷哼從身側(cè)響起涛碑,我...
    開封第一講書人閱讀 39,736評(píng)論 0 276
  • 序言:老撾萬榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎孵淘,沒想到半個(gè)月后蒲障,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 46,280評(píng)論 1 319
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡瘫证,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 38,369評(píng)論 3 340
  • 正文 我和宋清朗相戀三年揉阎,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片背捌。...
    茶點(diǎn)故事閱讀 40,503評(píng)論 1 352
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡余黎,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出载萌,到底是詐尸還是另有隱情惧财,我是刑警寧澤巡扇,帶...
    沈念sama閱讀 36,185評(píng)論 5 350
  • 正文 年R本政府宣布,位于F島的核電站垮衷,受9級(jí)特大地震影響厅翔,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜搀突,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,870評(píng)論 3 333
  • 文/蒙蒙 一刀闷、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧仰迁,春花似錦甸昏、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,340評(píng)論 0 24
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至雌隅,卻和暖如春翻默,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背恰起。 一陣腳步聲響...
    開封第一講書人閱讀 33,460評(píng)論 1 272
  • 我被黑心中介騙來泰國(guó)打工修械, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人检盼。 一個(gè)月前我還...
    沈念sama閱讀 48,909評(píng)論 3 376
  • 正文 我出身青樓肯污,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親吨枉。 傳聞我的和親對(duì)象是個(gè)殘疾皇子仇箱,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,512評(píng)論 2 359

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