递归(Recursion) 是一种解决问题的方法,其中函数直接或间接地调用自身来解决问题的子问题。简单来说,递归是函数调用自身的过程。
递归思想的本质是将复杂问题分解成相似但规模更小的子问题,直到达到易于直接解决的基本情况。
每个递归函数都需要包含两个关键部分:
def recursive_function(parameters):
# 基本情况 - 递归终止条件
if base_condition:
return base_result
# 递归情况 - 调用自身处理子问题
else:
# 可能涉及一些计算
result = ... recursive_function(reduced_parameters) ...
return result
为了理解递归的工作原理,我们需要了解函数调用栈:
阶乘是递归的经典应用:n! = n × (n-1)!
def factorial(n):
# 基本情况
if n == 0 or n == 1:
return 1
# 递归情况
else:
return n * factorial(n-1)
# 测试
print(factorial(5)) # 输出: 120 (5 × 4 × 3 × 2 × 1)
执行过程分析:
factorial(5)
└── 5 × factorial(4)
└── 5 × (4 × factorial(3))
└── 5 × (4 × (3 × factorial(2)))
└── 5 × (4 × (3 × (2 × factorial(1))))
└── 5 × (4 × (3 × (2 × 1)))
└── 5 × (4 × (3 × 2))
└── 5 × (4 × 6)
└── 5 × 24
└── 120
斐波那契数列定义为:F(n) = F(n-1) + F(n-2),其中 F(0) = 0, F(1) = 1
def fibonacci(n):
# 基本情况
if n == 0:
return 0
elif n == 1:
return 1
# 递归情况
else:
return fibonacci(n-1) + fibonacci(n-2)
# 测试
print(fibonacci(6)) # 输出: 8
执行过程分析:
fibonacci(6)
├── fibonacci(5) + fibonacci(4)
│ ├── [fibonacci(4) + fibonacci(3)] + [fibonacci(3) + fibonacci(2)]
│ └── ...(展开后有多次重复计算)
└── 8
汉诺塔是一个著名的数学问题:将n个大小不同的圆盘从一根柱子移动到另一根,每次只能移动一个圆盘,且大圆盘不能放在小圆盘上。
def hanoi(n, source, auxiliary, target):
# 基本情况 - 只有一个圆盘
if n == 1:
print(f"将圆盘 1 从 {source} 移动到 {target}")
return
# 递归情况 - 多个圆盘
# 步骤1: 将n-1个圆盘从source移到auxiliary
hanoi(n-1, source, target, auxiliary)
# 步骤2: 将最大的圆盘从source移到target
print(f"将圆盘 {n} 从 {source} 移动到 {target}")
# 步骤3: 将n-1个圆盘从auxiliary移到target
hanoi(n-1, auxiliary, source, target)
# 测试
hanoi(3, 'A', 'B', 'C')
执行输出:
将圆盘 1 从 A 移动到 C
将圆盘 2 从 A 移动到 B
将圆盘 1 从 C 移动到 B
将圆盘 3 从 A 移动到 C
将圆盘 1 从 B 移动到 A
将圆盘 2 从 B 移动到 C
将圆盘 1 从 A 移动到 C
递归在很多领域都有广泛应用:
# 二叉树的前序遍历
def preorder(node):
if node is None:
return
print(node.value) # 处理当前节点
preorder(node.left) # 递归处理左子树
preorder(node.right) # 递归处理右子树
# 归并排序
def merge_sort(arr):
# 基本情况
if len(arr) <= 1:
return arr
# 分解问题
mid = len(arr) // 2
left = merge_sort(arr[:mid])
right = merge_sort(arr[mid:])
# 合并结果
return merge(left, right)
def merge(left, right):
result = []
i = j = 0
while i < len(left) and j < len(right):
if left[i] < right[j]:
result.append(left[i])
i += 1
else:
result.append(right[j])
j += 1
result.extend(left[i:])
result.extend(right[j:])
return result
动态规划问题
数学计算
# 最大公约数的递归实现
def gcd(a, b):
if b == 0:
return a
return gcd(b, a % b)
尽管递归可以使代码简洁易懂,但它面临一些严重的效率问题:
每次递归调用都会在调用栈上创建新的栈帧,当递归深度过大时,会导致栈溢出错误。
# 可能导致栈溢出的代码
def deep_recursion(n):
if n == 0:
return
deep_recursion(n - 1)
deep_recursion(10000) # 在多数Python实现中会导致RecursionError
Python默认的递归深度限制是1000:
import sys
print(sys.getrecursionlimit()) # 输出: 1000
可以修改限制(不推荐):
sys.setrecursionlimit(2000) # 增加递归深度限制至2000
某些递归实现(如简单的斐波那契数列)会导致大量重复计算:
计算fibonacci(5)时的函数调用:
fib(5)
/ \
fib(4) fib(3)
/ \ / \
fib(3) fib(2) fib(2) fib(1)
/ \ / \ / \
fib(2) fib(1) fib(1) fib(0) fib(1) fib(0)
/ \
fib(1) fib(0)
注意fib(3)、fib(2)、fib(1)和fib(0)被重复计算了多次!
尾递归是一种特殊形式的递归,其中递归调用是函数执行的最后一个操作,没有其他计算。
尾递归可以被编译器优化,防止栈溢出(不过Python不默认支持尾递归优化)。
# 传统递归阶乘
def factorial(n):
if n <= 1:
return 1
return n * factorial(n - 1) # 递归调用后还有乘法运算
# 尾递归阶乘
def factorial_tail(n, accumulator=1):
if n <= 1:
return accumulator
return factorial_tail(n - 1, n * accumulator) # 递归调用是最后一个操作
使用记忆化(缓存已计算的结果)可以避免重复计算:
# 使用字典手动实现记忆化
def fibonacci_memo(n, memo={}):
if n in memo:
return memo[n]
if n <= 1:
result = n
else:
result = fibonacci_memo(n-1, memo) + fibonacci_memo(n-2, memo)
memo[n] = result
return result
# 使用Python的内置装饰器
from functools import lru_cache
@lru_cache(maxsize=None)
def fibonacci_cached(n):
if n <= 1:
return n
return fibonacci_cached(n-1) + fibonacci_cached(n-2)
# 性能比较
import time
start = time.time()
fibonacci(35) # 纯递归,非常慢
print(f"纯递归耗时: {time.time() - start:.2f}秒")
start = time.time()
fibonacci_cached(35) # 缓存递归,极快
print(f"缓存递归耗时: {time.time() - start:.2f}秒")
很多递归算法可以改写成迭代形式,避免递归开销:
# 递归斐波那契
def fibonacci_recursive(n):
if n <= 1:
return n
return fibonacci_recursive(n-1) + fibonacci_recursive(n-2)
# 迭代斐波那契
def fibonacci_iterative(n):
if n <= 1:
return n
a, b = 0, 1
for _ in range(2, n + 1):
a, b = b, a + b
return b
特性 | 递归 | 迭代 |
---|---|---|
代码清晰度 | 通常更清晰易懂 | 对于复杂问题可能更难理解 |
内存使用 | 较高(调用栈开销) | 较低 |
执行速度 | 通常较慢 | 通常较快 |
无限序列处理 | 有限制 | 可以处理无限序列 |
实现复杂度 | 对某些问题简单 | 对某些问题复杂 |
调试难度 | 相对困难 | 相对简单 |
明确基本情况:确保你的递归函数有明确的终止条件
防止栈溢出:
避免重复计算:
调试递归:
性能考虑:
可读性与效率平衡:
递归能够简洁地表达某些算法的核心思想。然而,递归也带来了效率问题,包括调用栈溢出和重复计算。通过使用优化技术如记忆化缓存和尾递归优化,以及在适当情况下转换为迭代实现,可以在保持代码清晰度的同时提高程序效率。