在本文中,您將學(xué)習(xí)創(chuàng)建遞歸函數(shù)。一個自我調(diào)用的函數(shù)。此外,您還將了解尾遞歸函數(shù)。
調(diào)用自身的函數(shù)稱為遞歸函數(shù)。并且,這種技術(shù)稱為遞歸。
一個物理世界的實例是將兩個平行的鏡子相對放置。它們之間的任何對象都將被遞歸地反射。
fun main(args: Array<String>) { ... .. ... recurse() ... .. ... } fun recurse() { ... .. ... recurse() ... .. ... }
在這里,從recurse()函數(shù)本身的主體中調(diào)用recurse()函數(shù)。 該程序的工作原理如下:
在這里,遞歸調(diào)用永遠持續(xù)下去,從而導(dǎo)致無限遞歸。
為了避免無限遞歸,可以在一個分支進行遞歸調(diào)用而其他分支不遞歸調(diào)用的情況下使用if ... else(或類似方法)。
fun main(args: Array<String>) { val number = 4 val result: Long result = factorial(number) println("$number 階乘 = $result") } fun factorial(n: Int): Long { return if (n == 1) n.toLong() else n*factorial(n-1) }
運行該程序時,輸出為:
4 階乘 = 24
factorial()下圖說明了該函數(shù)的遞歸調(diào)用:
涉及的步驟如下:
factorial(4) // 第1次函數(shù)調(diào)用,參數(shù): 4 4*factorial(3) // 第2次函數(shù)調(diào)用,參數(shù): 3 4*(3*factorial(2)) // 第3次函數(shù)調(diào)用,參數(shù): 2 4*(3*(2*factorial(1))) // 第4次函數(shù)調(diào)用,參數(shù): 1 4*(3*(2*1)) 24
尾遞歸不是Kotlin語言的特征,而是一個通用概念。 包括 Kotlin 在內(nèi)的一些編程語言使用它來優(yōu)化遞歸調(diào)用,而其他語言(例如 Python )不支持它們。
在普通遞歸中,首先執(zhí)行所有遞歸調(diào)用,最后根據(jù)返回值計算結(jié)果(如上面的示例所示)。所以,在進行所有遞歸調(diào)用之前,您不會得到結(jié)果。
在尾遞歸中,首先執(zhí)行計算,然后執(zhí)行遞歸調(diào)用(遞歸調(diào)用將當(dāng)前步驟的結(jié)果傳遞給下一個遞歸調(diào)用)。 這使得遞歸調(diào)用等同于循環(huán),并避免了堆棧溢出的風(fēng)險。
如果對自身的函數(shù)調(diào)用是它執(zhí)行的最后一個操作,則該遞歸函數(shù)可以進行尾部遞歸。例如,
示例1:不符合尾部遞歸的條件,因為對其自身的函數(shù)調(diào)用 n*factorial(n-1) 不是最后一個操作。
fun factorial(n: Int): Long { if (n == 1) { return n.toLong() } else { return n*factorial(n - 1) } }
示例2:符合尾遞歸條件,因為對自身的函數(shù)調(diào)用fibonacci(n-1, a+b, a)是最后的操作。
fun fibonacci(n: Int, a: Long, b: Long): Long { return if (n == 0) b else fibonacci(n-1, a+b, a) }
要告訴編譯器在Kotlin中執(zhí)行尾部遞歸,您需要使用tailrec修飾符標記該函數(shù)。
import java.math.BigInteger fun main(args: Array<String>) { val n = 100 val first = BigInteger("0") val second = BigInteger("1") println(fibonacci(n, first, second)) } tailrec fun fibonacci(n: Int, a: BigInteger, b: BigInteger): BigInteger { return if (n == 0) a else fibonacci(n-1, b, a+b) }
運行該程序時,輸出為:
354224848179261915075
這個程序計算斐波納契級數(shù)的第100項。 因為輸出可以是非常大的整數(shù),所以我們從Java標準庫中導(dǎo)入了BigInteger類。
在這里,函數(shù)fibonacci()用trarec修飾符標記,該函數(shù)有資格進行尾遞歸調(diào)用。 因此,編譯器在這種情況下優(yōu)化了遞歸。
如果您試圖在不使用尾遞歸的情況下查找斐波納契數(shù)列的第20000項(或任何其他大整數(shù)),編譯器將拋出 java.lang.StackOverflowError 異常。
但是,我們上面的程序運行得很好。 這是因為我們使用了尾遞歸,它使用了高效的基于循環(huán)的版本,而不是傳統(tǒng)的遞歸。
上述示例(第一個示例)中用于計算數(shù)字階乘的示例無法針對尾遞歸進行優(yōu)化。 這是執(zhí)行相同任務(wù)的另一個程序。
fun main(args: Array<String>) { val number = 5 println("$number 階乘 = ${factorial(number)}") } tailrec fun factorial(n: Int, run: Int = 1): Long { return if (n == 1) run.toLong() else factorial(n-1, run*n) }
運行該程序時,輸出為:
5 階乘= 120
編譯器可以在該程序中優(yōu)化遞歸,因為遞歸函數(shù)可以進行尾遞歸,并且我們使用了tailrec修飾符,告訴編譯器優(yōu)化遞歸。