前言:递归函数是一种强大而又充满魅力的编程技巧。它就像是一面神奇的镜子,函数在其中能够调用自身的倒影,从而以一种简洁而优雅的方式解决许多复杂的问题。
目录
一、递归函数是啥玩意儿
递归函数说白了,就是函数自己调用自己。不过,咱可不能让函数这么一直调用下去没完没了,不然程序就 “跑偏” 了。所以得给它设个 “停止信号”,也就是终止条件。一到这个条件,函数就老实停下来,不再调自己啦,然后把之前算出来的结果一层一层地返回去。
举个超简单的例子,算阶乘就特别适合用递归函数。阶乘就是把一个正整数 n 和所有比它小的正整数都乘起来,像 5! = 5 × 4 × 3 × 2 × 1。咱用递归的思路想,其实就是 n! = n × (n - 1)! ,那咱就让函数自己调自己,把问题越变越小,直到变成 1! = 1,这时候就让函数停住返结果。
package main
import "fmt"
func factorial(n int) int {
if n == 1 { // 咱的终止条件就这儿,要是 n 是 1,就返 1
return 1
}
return n * factorial(n-1) // 这里函数就开始调自己啦,看不看懂?
}
func main() {
fmt.Println(factorial(5)) // 试一试,看看会不会返 120 呢
}
看看,这代码是不是超简洁?就像把问题一层层剥开,找到最里面的核心,然后再一层层包回去,把结果算出来。
二、递归函数的优缺点
优点
代码简洁 :递归函数能把复杂问题拆成小问题,代码看着就清爽多了。比如算斐波那契数列(斐波那契数列就是从 0 和 1 开始,后面的数都是前两个数的和),递归写法就很简单:
package main
import "fmt"
func fibonacci(n int) int {
if n <= 1 {
return n
}
return fibonacci(n-1) + fibonacci(n-2) // 自己调自己,把问题拆小
}
func main() {
fmt.Println(fibonacci(10)) // 算一算第 10 个斐波那契数
}
缺点
性能问题 :递归调用自己会搞出好多新的栈帧(栈帧就是函数调用时在内存里占的小窝),要是调用太多次,就会把栈给搞溢出,程序就直接 “扑街” 了。而且像斐波那契数列的简单递归写法,会算好多重复的东西,效率很低。
调试困难 :递归函数调来调去,调试的时候得跟着好多层函数调用,搞起来有点麻烦。
三、递归函数能干啥
树或图的遍历 :比如遍历文件系统里的目录,递归地把每个文件夹和文件都瞅一遍:
package main
import (
"fmt"
"path/filepath"
)
func traverseDir(path string) {
entries, err := filepath.Glob(filepath.Join(path, "*"))
if err != nil {
fmt.Println("哎呀,出错了:", err)
return
}
for _, entry := range entries {
if info, err := filepath.Stat(entry); err == nil && info.IsDir() {
fmt.Println("哇,这是个文件夹:", entry)
traverseDir(entry) // 递归遍历子文件夹
} else {
fmt.Println("嘿,这是个文件:", entry)
}
}
}
func main() {
traverseDir("./testdir") // 假设当前目录下有个叫 testdir 的文件夹
}
深度优先搜索(DFS) :在迷宫求解、八皇后这种问题里都能用上,像是在迷宫里一直往深处走,走不通了再回来换条路。
四、递归函数咋优化
记忆化 :对于斐波那契数列,可以用记忆化避免重复算,搞个 “小本本” 把算过的结果记下来:
package main
import "fmt"
func fibonacci(n int, memo map[int]int) int {
if val, exists := memo[n]; exists { // 先瞅瞅算过没
return val
}
if n <= 1 {
memo[n] = n
} else {
memo[n] = fibonacci(n-1, memo) + fibonacci(n-2, memo)
}
return memo[n]
}
func main() {
memo := make(map[int]int)
fmt.Println(fibonacci(10, memo)) // 算一算第 10 个斐波那契数
}
尾递归优化 :把递归调用放在函数的最后一步,有些编译器会把它优化成循环,避免把栈搞崩。比如算阶乘的尾递归写法:
package main
import "fmt"
func factorial(n int, acc int) int {
if n == 0 {
return acc
}
return factorial(n-1, n*acc) // 尾递归调用
}
func main() {
fmt.Println(factorial(5, 1)) // 算一算 5 的阶乘
}
五、递归函数经典案例 - 汉诺塔问题
汉诺塔是个超经典的递归问题。有三根柱子 A、B、C,A 上面有 n 个盘子,按从大到小叠着。要求把所有盘子都移到 C 上,每次只能移一个,还不能把大的盘子放在小的上面。
package main
import "fmt"
func hanoi(n int, from, aux, to string) {
if n == 1 {
fmt.Printf("把盘子 1 从 %s 移到 %s\n", from, to)
return
}
hanoi(n-1, from, to, aux) // 把 n-1 个盘子从 from 移到 aux
fmt.Printf("把盘子 %d 从 %s 移到 %s\n", n, from, to) // 移最后一个盘子
hanoi(n-1, aux, from, to) // 把 n-1 个盘子从 aux 移到 to
}
func main() {
hanoi(3, "A", "B", "C") // 算一算 3 个盘子咋移
}
这段代码把问题拆成把 n-1 个盘子移来移去的子问题,最后就解决了整个汉诺塔问题。
总结
递归函数就是这么个既厉害又有点小脾气的工具。宝子们要是刚入门别怕,多写写多练练,理解了它的终止条件和调用过程,就能轻松用它搞定好多复杂问题。不过写的时候得注意性能问题,要是函数调用太多,栈就不够用了,必要时就用优化技巧。相信宝子们只要肯花时间,很快就能掌握递归函数的精髓,让它变成自己编程路上的好帮手。