【Scala】尾遞歸優化

以遞歸方式思考

遞歸通過靈巧的函數定義,告訴計算機做什么。在函數式編程中,隨處可見遞歸思想的運用。
下面給出幾個遞歸函數的例子:

object RecursiveExample extends App{
  // 數列求和例子
  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)


  // 翻轉字符串
  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))
  }
}

我們以上面代碼最后一個快速排序函數為例,使用遞歸的方式,其代碼實現非常的簡潔和通俗易懂。遞歸函數的核心是設計好遞歸表達式,并且確定算法的邊界條件。上面的快速排序中,認為空列表就是排好序的列表,這就是遞歸的邊界條件,這個條件是遞歸終止的標志。

尾遞歸

遞歸算法需要保持調用堆棧,效率較低,如果調用次數較多,會耗盡內存或棧溢出。然而,尾遞歸可以克服這一缺點。
尾遞歸是指遞歸調用是函數的最后一個語句,而且其結果被直接返回,這是一類特殊的遞歸調用。由于遞歸結果總是直接返回,尾遞歸比較方便轉換為循環,因此編譯器容易對它進行優化。

遞歸求階乘的經典例子

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

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

上面的代碼,由于每次遞歸調用n-1的階乘時,都有一次額外的乘法計算,這使得堆棧中的數據都需要保留。在新的遞歸中要分配新的函數棧。
運行過程就像這樣:

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

而下面是一個尾遞歸版本,在效率上,和循環是等價的:

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)
}

這里的運行過程如下:

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

該函數中的_loop在最后一步,要么返回遞歸邊界條件的值,要么調用遞歸函數本身。
改寫成尾遞歸版本的關鍵:
尾遞歸版本最重要的就是找到合適的累加器,該累加器可以保留最后一次遞歸調用留在堆棧中的數據,積累之前調用的結果,這樣堆棧數據就可以被丟棄,當前的函數棧可以被重復利用。
在這個例子中,變量acc就是累加器,每次遞歸調用都會更新該變量,直到遞歸邊界條件滿足時返回該值。
對于尾遞歸,Scala語言特別增加了一個注釋@tailrec,該注釋可以確保程序員寫出的程序是正確的尾遞歸程序,如果由于疏忽大意,寫出的不是一個尾遞歸程序,則編譯器會報告一個編譯錯誤,提醒程序員修改自己的代碼。

菲波那切數列的例子

原始的代碼很簡單:

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

尾遞歸版本用了兩個累加器,一個保存較小的項acc1,另一個保存較大項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)
}

幾個列表操作中使用尾遞歸的例子

求列表的長度

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)
}

翻轉列表

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)
}

去除列表中多個重復的元素

這里要求去除列表中多個連續的字符,只保留其中的一個。

// 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)
}

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

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容