前面我们通过2篇文章,一起了解了 递归,以及使用递归来解决汉诺塔问题。今天我们在这个基础上,进一步地熟悉和学习递归。
这篇学习笔记将涵盖递归的基本概念、应用、优化技巧、陷阱及与迭代的对比,并通过具体的 Python 代码示例和大家一起来深入理解递归的使用。
递归,简单来说就是函数自己调用自己。听起来有点绕,但其实就像俄罗斯套娃,一层套一层,直到遇到最小的那个娃娃(基线条件)才停止。
递归是指一个函数直接或间接地调用自身。它通常由两个关键要素构成:
每次递归调用都会把当前的局部变量、返回地址等压入调用栈,直到达到基线条件并开始回溯。理解调用栈有助于调试递归程序。
阶乘是一个经典的递归例子,定义为 n! = n * (n-1)!
,直到 1! = 1
。
def factorial(n):
if n == 0: # 基线条件
return 1
else:
return n * factorial(n - 1) # 递归条件
调用 factorial(5)
会通过递归逐步调用直到 factorial(0)
,然后逐步返回结果。
想象一下,计算5的阶乘:
factorial(5)
调用 factorial(4)
factorial(4)
调用 factorial(3)
factorial(3)
调用 factorial(2)
factorial(2)
调用 factorial(1)
factorial(1)
调用 factorial(0)
factorial(0)
遇到基线条件,返回1
然后逐层返回,最终得到 5 * 4 * 3 * 2 * 1 * 1 = 120
斐波那契数列的递归定义为 F(n) = F(n-1) + F(n-2)
,直到 F(0) = 0
和 F(1) = 1
。
def fibonacci(n):
if n <= 1:
return n
else:
return fibonacci(n - 1) + fibonacci(n - 2)
可以通过调试工具(例如 PyCharm 或 VS Code 的调试器)逐步观察递归调用栈。每进入一次递归,都会看到栈中增加一个新的调用帧,直到基线条件触发。
也使用调试工具例如Python的pdb,可以一步步跟踪递归函数的执行过程,观察每次调用时变量的变化,帮助你更直观地理解递归。
递归在算法设计中就像一把瑞士军刀,可以解决各种问题。递归在很多经典算法中都有广泛应用:
分治法的精髓在于“分而治之”,把大问题拆解成小问题,分别解决后再合并结果。
归并排序是一种典型的分治算法,通过递归地将数组分成两半进行排序,然后合并已排序的两部分。
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 = []
while left and right:
if left[0] < right[0]:
result.append(left.pop(0))
else:
result.append(right.pop(0))
result.extend(left)
result.extend(right)
return result
快速排序通过递归地选择一个基准元素,将数组分为比基准小和比基准大的两部分,然后对这两部分分别进行排序。
def quick_sort(arr):
if len(arr) <= 1:
return arr
pivot = arr[len(arr) // 2]
left = [x for x in arr if x < pivot]
middle = [x for x in arr if x == pivot]
right = [x for x in arr if x > pivot]
return quick_sort(left) + middle + quick_sort(right)
回溯法是通过递归尝试所有可能的解,并在遇到错误时回退并继续尝试其他解。
通过递归摆放皇后,并判断每一层是否满足规则。如果满足规则就进入下一层,否则回退。
八皇后问题是一个经典的回溯算法问题,目标是在一个8x8的棋盘上放置8个皇后,使得它们互不攻击(即任意两个皇后不能在同一行、同一列或同一对角线上)。
def solve_n_queens(n):
# 初始化棋盘,-1表示未放置皇后
board = [-1] * n
# 存储所有合法的棋盘布局
solutions = []
# 回溯函数
def backtrack(row):
# 如果已经放置了n个皇后,保存当前棋盘布局
if row == n:
solutions.append(board[:])
return
# 尝试在当前行的每一列放置皇后
for col in range(n):
# 检查当前位置是否合法
if is_valid(board, row, col):
# 放置皇后
board[row] = col
# 递归处理下一行
backtrack(row + 1)
# 回溯:撤销当前行的皇后放置
board[row] = -1
# 检查在第row行第col列放置皇后是否合法
def is_valid(board, row, col):
# 遍历之前的所有行
for i in range(row):
# 检查是否在同一列或同一对角线上
if board[i] == col or abs(board[i] - col) == row - i:
return False
return True
# 从第0行开始回溯
backtrack(0)
# 返回所有合法的棋盘布局
return solutions
代码解析
2.1. 数据结构
board
: 一个长度为n
的列表,表示棋盘。board[i] = j
表示在第i
行第j
列放置了一个皇后。
solutions
: 存储所有合法的棋盘布局。
2.2. 核心函数
backtrack(row)
: 递归回溯函数,尝试在第row
行放置皇后。
如果row == n
,说明已经成功放置了n
个皇后,将当前棋盘布局保存到solutions
中。
否则,遍历当前行的每一列,尝试放置皇后:
如果当前位置合法(通过is_valid
检查),则放置皇后,并递归处理下一行。
递归结束后,撤销当前行的皇后放置(回溯),尝试其他可能性。
is_valid(board, row, col)
: 检查在第row
行第col
列放置皇后是否合法。
遍历之前的所有行,检查是否有皇后在同一列或同一对角线上。
2.3. 运行流程
初始化一个大小为n
的棋盘board
,所有值初始化为-1
(表示未放置皇后)。
调用backtrack(0)
,从第0行开始尝试放置皇后。
在backtrack
函数中:
如果当前行row == n
,说明找到一种合法布局,将其保存到solutions
中。
否则,遍历当前行的每一列,尝试放置皇后:
如果当前位置合法,则放置皇后,并递归处理下一行。
递归结束后,撤销当前行的皇后放置,尝试其他列。
最终返回所有合法的棋盘布局solutions
。
树形结构本身就是递归的天然应用。二叉树的前序、中序、后序遍历都可以通过递归实现。
class TreeNode:
def __init__(self, val=0, left=None, right=None):
self.val = val
self.left = left
self.right = right
def preorder(root):
if root:
print(root.val)
preorder(root.left)
preorder(root.right)
记忆化搜索是一种优化递归的技术,目的是避免重复计算相同的子问题。常用于斐波那契数列等问题。
def fibonacci_memo(n, memo={}):
if n in memo:
return memo[n]
if n <= 1:
return n
memo[n] = fibonacci_memo(n - 1, memo) + fibonacci_memo(n - 2, memo)
return memo[n]
尾递归是指递归调用出现在函数的最后一步,可以将递归转化为循环,从而避免栈溢出问题。
def factorial_tail(n, acc=1):
if n == 0:
return acc
return factorial_tail(n - 1, n * acc)
各位需要注意的是:Python 并不支持尾递归优化,因此对于深度较大的递归,仍然要小心栈溢出。
递归深度过大时,调用栈会导致栈溢出。可以通过增加基线条件或者将递归转化为迭代来避免。
递归可能会多次计算相同的子问题,造成效率低下。例如,斐波那契数列的递归实现会计算许多重复的子问题。
递归相较于循环通常会带来额外的函数调用开销,导致效率较低。
递归和迭代各有优缺点:
有些递归问题可以转化为迭代解决,特别是尾递归问题。
递归在实际问题中有广泛应用。以下是几个经典递归问题:
# 组合问题示例
def combine(n, k):
res = []
def backtrack(start, path):
if len(path) == k:
res.append(path)
return
for i in range(start, n + 1):
backtrack(i + 1, path + [i])
backtrack(1, [])
return res
继续扩展递归应用,我们将通过实际问题来进一步理解递归的强大功能。
以下是我们一起来使用递归解决的三个实际问题:文件目录遍历、JSON 数据解析 和 HTML 文档解析。
文件目录遍历是一个常见的递归问题,因为目录可以包含文件和子目录,而子目录又可能包含更多的文件和子目录。因此,我们可以通过递归来遍历整个文件树。
代码示例:递归遍历文件夹
import os
def traverse_directory(path):
# 获取路径下的所有文件和子目录
for item in os.listdir(path):
full_path = os.path.join(path, item)
if os.path.isdir(full_path): # 如果是目录,递归遍历
print(f"Directory: {full_path}")
traverse_directory(full_path) # 递归调用
else: # 如果是文件,打印文件路径
print(f"File: {full_path}")
# 示例调用
traverse_directory('/path/to/directory')
解释:
os.listdir(path)
返回指定目录下的所有文件和子目录的名称。os.path.isdir(full_path)
判断路径是否是一个目录。traverse_directory
,遍历该目录。通过这种方式,可以遍历整个文件树,不管目录有多深,这个非常实用!
递归常常用于处理具有嵌套结构的数据,像 JSON 这样的格式通常包含字典、列表等复杂嵌套结构。使用递归可以帮助我们解析和提取其中的数据。
代码示例:递归解析 JSON 数据
假设我们有一个复杂的 JSON 数据,其中包含嵌套的字典和列表。
import json
# 假设的 JSON 数据
data = '''
{
"name": "Alice",
"age": 25,
"address": {
"city": "Wonderland",
"zipcode": "12345"
},
"hobbies": ["reading", "painting", {"name": "sports", "types": ["soccer", "basketball"]}]
}
'''
def parse_json(obj):
if isinstance(obj, dict):
for key, value in obj.items():
print(f"{key}:")
parse_json(value) # 递归调用,处理字典的每个值
elif isinstance(obj, list):
for item in obj:
parse_json(item) # 递归调用,处理列表中的每个元素
else:
print(f"Value: {obj}") # 打印最终的值
# 解析 JSON 数据
parse_json(json.loads(data))
解释:
parse_json
函数检查对象类型:
json.loads
将字符串转换为 Python 对象后传递给 parse_json
进行解析。这种递归解析方法适用于处理结构复杂的 JSON 数据,能够处理不确定的深度和层级。
HTML 文档通常是由标签嵌套构成的树形结构,因此解析 HTML 时递归是非常自然的选择。我们可以使用递归遍历 HTML 元素及其子元素。
代码示例:递归解析 HTML 文档
假设我们需要解析一个简单的 HTML 文件,提取其中的所有 a
标签(超链接)。
from html.parser import HTMLParser
class MyHTMLParser(HTMLParser):
def handle_starttag(self, tag, attrs):
if tag == 'a': # 如果是 标签
for attr in attrs:
if attr[0] == 'href':
print(f"Link: {attr[1]}") # 打印链接地址
# 示例 HTML 文档
html_data = '''
Test Page
Welcome to My Webpage
Here are some links:
Example
Another Example
'''
# 创建并使用 HTMLParser 对象
parser = MyHTMLParser()
parser.feed(html_data)
解释:
HTMLParser
是 Python 的标准库,用于解析 HTML 内容。handle_starttag
方法在每次遇到开始标签时被调用。当标签是 a
时,我们从其属性中提取 href
,即超链接地址。feed
方法将 HTML 字符串传递给解析器,递归解析 HTML 内容并提取超链接。在实际的 HTML 解析中,递归处理各个标签及其子标签是非常常见的,尤其是当文档结构复杂时。
拓展学习可以让我们在理解和掌握递归的基础上,进行融汇贯通,将递归运用在实际需要的地方。
一点建议:
学习递归相关的数学知识,例如:
递归关系式
数学归纳法
学习资源:
书籍:
《算法导论》
《编程珠玑》
网站:
LeetCode
LintCode
视频:
MIT OpenCourseware: Introduction to Algorithms
从简单的例子开始,逐步深入理解递归。
多动手实践,尝试用递归解决不同的问题。
不要害怕犯错,调试是学习递归的重要部分。
通过以上内容,我们已经将递归的基础知识和应用场景进行了充分阐述,后续我们就可以通过实际练习不断提升递归的能力。祝你好运!