心灵有其自身的逻辑,是理性所不能理解的。
⚙️ 递归
递归(recursion)是一种函数调用自身来解决问题的编程技巧。它特别适合处理可以分解为同类型子问题的场景,例如飞行包线边界计算、递归轨迹规划、飞行系统组件层级遍历等。掌握递归能以更优雅的方式解决许多复杂问题,但同时要注意 Python 中的递归深度限制和性能问题。
📌 本节要点
- 递归三要素:基线条件、递归调用、问题规模缩小
sys.getrecursionlimit()查看递归深度限制,@lru_cache记忆化避免重复计算- 树形结构遍历、分治算法、回溯问题是递归的经典应用场景
- 递归转迭代:用显式栈替代系统调用栈,突破深度限制
- 尾递归在 Python 中不被优化,了解其原理但不必强求
递归思想
递归的核心思想是"分而治之":把一个大问题分解为规模更小的同类型问题,直到问题小到可以直接求解。
每个递归函数必须包含两个部分:
- 基线条件(base case):不再递归,直接返回结果的终止条件
- 递归条件(recursive case):调用自身处理规模更小的子问题
递归调用栈(以 factorial(4) 为例):
import numpy as np
def countdown_altitude(altitude: float, step: float = 1000.0) -> None:
"""递归倒计时:从当前高度逐步下降到地面"""
# 基线条件
if altitude <= 0:
print("已到达地面!")
return
# 递归条件
print(f"当前高度:{altitude:.0f} m")
countdown_altitude(altitude - step)
countdown_altitude(5000)
# 输出:
# 当前高度:5000 m
# 当前高度:4000 m
# 当前高度:3000 m
# 当前高度:2000 m
# 当前高度:1000 m
# 已到达地面!
没有基线条件的递归会无限调用自己,最终触发 RecursionError:
def infinite_check():
return infinite_check() # 没有终止条件
infinite_check() # RecursionError: maximum recursion depth exceeded
基线条件
基线条件的设计是递归的关键。一个良好的基线条件应该满足:
- 一定能被达到(避免无限递归)
- 能直接给出答案,不需要再递归
import numpy as np
# 反例:基线条件设计不当
def bad_envelope_calc(altitude: float) -> float:
"""计算某高度下的最大速度包线"""
if altitude == 0.0: # 浮点数精确比较不可靠
return 0.0
return 340.0 + bad_envelope_calc(altitude - 0.1)
# bad_envelope_calc(1.0) # 可能无限递归!浮点精度问题
# 正例:基线条件更稳健
def max_speed_at_altitude(altitude: float) -> float:
"""计算某高度下的最大允许速度(简化包线模型)"""
if altitude <= 0.0: # 处理负数和 0
return 0.0
# 简化模型:高度每升高 1000m,最大速度降低 10 m/s
return 340.0 - (altitude / 1000.0) * 10.0
print(max_speed_at_altitude(5000)) # 输出:290.0
print(max_speed_at_altitude(-100)) # 输出:0.0
递归深度限制
Python 默认递归深度上限是 1000,超过会抛出 RecursionError:
def recursive_altitude_check(n: int) -> str:
if n > 0:
return recursive_altitude_check(n - 1)
return "done"
# print(recursive_altitude_check(2000)) # RecursionError
import sys
print(f"默认递归深度上限:{sys.getrecursionlimit()}") # 通常输出:1000
修改递归深度限制
使用 sys.setrecursionlimit 可以调整:
import sys
sys.setrecursionlimit(5000) # 调高上限
print(recursive_altitude_check(2000)) # 输出:done
Python 没有真正的尾调用优化(TCO),每次递归调用都会在调用栈上保留帧。调得过高可能导致:
- 栈溢出(segfault,解释器崩溃)
- 内存暴涨
- 性能下降
如果递归深度真的需要很大,应该改用迭代。
典型应用
1. 飞行包线边界计算
飞行包线(flight envelope)定义了飞行器的安全运行边界。递归可用于逐层计算不同高度下的速度、过载等限制。
import numpy as np
def compute_max_speed(altitude: float, v0: float = 340.0, decay: float = 0.01) -> float:
"""
递归计算给定高度下的最大允许速度。
简化模型:速度随高度指数衰减。
"""
if altitude <= 0.0: # 基线条件:海平面
return v0
# 递归条件:每 100m 高度,速度衰减
return compute_max_speed(altitude - 100.0, v0 * (1 - decay))
# 计算不同高度的最大速度
altitudes = np.array([0, 2000, 5000, 8000, 10000])
max_speeds = np.array([compute_max_speed(a) for a in altitudes])
for alt, spd in zip(altitudes, max_speeds):
print(f"高度 {alt:5d} m → 最大速度 {spd:.1f} m/s")
# 输出:
# 高度 0 m → 最大速度 340.0 m/s
# 高度 2000 m → 最大速度 276.3 m/s
# 高度 5000 m → 最大速度 207.9 m/s
# 高度 8000 m → 最大速度 155.4 m/s
# 高度 10000 m → 最大速度 124.4 m/s
2. 递归轨迹规划
递归可用于生成飞行轨迹的航路点序列,每一层递归计算下一个航路点的位置。
import numpy as np
def plan_trajectory(
position: np.ndarray,
velocity: np.ndarray,
steps: int,
dt: float = 1.0,
) -> list[np.ndarray]:
"""
递归规划直线轨迹航路点。
position: 当前位置 [x, y, z]
velocity: 速度向量 [vx, vy, vz]
steps: 剩余规划步数
"""
if steps <= 0: # 基线条件:规划完成
return [position]
next_pos = position + velocity * dt
# 递归条件:规划剩余步骤
return [position] + plan_trajectory(next_pos, velocity, steps - 1, dt)
# 生成 5 个航路点
start = np.array([0.0, 0.0, 1000.0])
vel = np.array([100.0, 50.0, -10.0]) # 向前、向右、缓慢下降
waypoints = plan_trajectory(start, vel, steps=5)
for i, wp in enumerate(waypoints):
print(f"WP{i}: ({wp[0]:.0f}, {wp[1]:.0f}, {wp[2]:.0f})")
# 输出:
# WP0: (0, 0, 1000)
# WP1: (100, 50, 990)
# WP2: (200, 100, 980)
# WP3: (300, 150, 970)
# WP4: (400, 200, 960)
# WP5: (500, 250, 950)
3. 飞行任务序列(汉诺塔变体)
经典汉诺塔问题可以映射为飞行任务调度:将 n 个飞行任务从"待执行"队列转移到"已完成"队列,中间需要借助"暂存"队列。
def flight_task_schedule(
n: int, source: str, target: str, auxiliary: str
) -> None:
"""
递归调度 n 个飞行任务。
约束:任务必须按编号从小到大执行。
"""
if n == 1:
print(f" 执行任务 {n}:{source} → {target}")
return
# 把上面 n-1 个任务移到暂存队列
flight_task_schedule(n - 1, source, auxiliary, target)
# 执行最底部的任务
print(f" 执行任务 {n}:{source} → {target}")
# 把 n-1 个任务从暂存队列移到目标队列
flight_task_schedule(n - 1, auxiliary, target, source)
print("3 个飞行任务的调度:")
flight_task_schedule(3, "待执行", "已完成", "暂存")
输出:
3 个飞行任务的调度:
执行任务 1:待执行 → 已完成
执行任务 2:待执行 → 暂存
执行任务 1:已完成 → 暂存
执行任务 3:待执行 → 已完成
执行任务 1:暂存 → 待执行
执行任务 2:暂存 → 已完成
执行任务 1:待执行 → 已完成
汉诺塔的递推关系:T(n) = 2*T(n-1) + 1,移动次数为 2^n - 1。
4. 飞行系统组件层级遍历
递归遍历飞行器系统组件树是天然适合递归的场景:
from dataclasses import dataclass
@dataclass
class FlightComponent:
name: str
component_type: str # "system", "subsystem", "component"
children: list["FlightComponent"]
def print_component_tree(node: FlightComponent, indent: int = 0) -> None:
"""递归打印飞行器系统组件树。"""
prefix = " " * indent
icon = "🛩️" if node.component_type == "system" else (
"⚙️" if node.component_type == "subsystem" else "🔩"
)
print(f"{prefix}{icon} {node.name}")
for child in node.children:
print_component_tree(child, indent + 1)
# 构造飞行器系统组件树
avionics = FlightComponent("航电系统", "system", [
FlightComponent("飞行控制计算机", "subsystem", [
FlightComponent("传感器模块", "component", []),
FlightComponent("执行机构接口", "component", []),
]),
FlightComponent("导航系统", "subsystem", [
FlightComponent("GPS 接收机", "component", []),
FlightComponent("惯性测量单元", "component", []),
]),
FlightComponent("通信系统", "subsystem", [
FlightComponent("甚高频电台", "component", []),
FlightComponent("数据链", "component", []),
]),
])
print_component_tree(avionics)
过滤特定类型的组件:
def find_components_by_type(node: FlightComponent, comp_type: str) -> list[FlightComponent]:
"""递归查找指定类型的所有组件。"""
result: list[FlightComponent] = []
if node.component_type == comp_type:
result.append(node)
for child in node.children:
result.extend(find_components_by_type(child, comp_type))
return result
components = find_components_by_type(avionics, "component")
print(f"找到 {len(components)} 个底层组件")
for c in components:
print(f" - {c.name}")
5. 飞行决策树
递归遍历飞行决策树,根据当前飞行状态(高度、速度、过载)逐层检查是否满足安全条件。
from dataclasses import dataclass
import numpy as np
@dataclass
class DecisionNode:
"""飞行决策树节点"""
check_name: str
threshold: float
unit: str
left: "DecisionNode | None" = None # 满足条件的分支
right: "DecisionNode | None" = None # 不满足条件的分支
is_leaf: bool = False
result: str = ""
def build_flight_decision_tree() -> DecisionNode:
"""构建飞行安全检查决策树"""
# 叶子节点
safe = DecisionNode("安全", 0, "", is_leaf=True, result="✅ 通过所有检查")
warn = DecisionNode("警告", 0, "", is_leaf=True, result="⚠️ 需要关注")
danger = DecisionNode("危险", 0, "", is_leaf=True, result="❌ 禁止飞行")
# 第一层:高度检查
altitude_check = DecisionNode("高度", 500, "m", left=safe, right=danger)
# 第二层:速度检查(高度合格时)
speed_check = DecisionNode("速度", 250, "m/s", left=safe, right=warn)
# 第三层:过载检查(速度合格时)
load_check = DecisionNode("过载", 2.5, "g", left=safe, right=danger)
# 组装树
speed_check.left = load_check
altitude_check.left = speed_check
return altitude_check
def check_flight_safety(
node: DecisionNode,
altitude: float,
speed: float,
load_factor: float,
) -> str:
"""
递归遍历决策树,检查飞行安全状态。
"""
if node.is_leaf: # 基线条件:到达叶子节点
return node.result
# 获取当前检查的参数值
if node.check_name == "高度":
value = altitude
elif node.check_name == "速度":
value = speed
elif node.check_name == "过载":
value = load_factor
else:
return "❌ 未知检查项"
# 递归条件:根据检查结果选择分支
if value >= node.threshold:
return check_flight_safety(node.left, altitude, speed, load_factor)
else:
return check_flight_safety(node.right, altitude, speed, load_factor)
# 测试不同飞行状态
tree = build_flight_decision_tree()
test_cases = [
(8000, 280, 2.0, "正常巡航"),
(300, 200, 1.5, "低空慢速"),
(8000, 300, 3.0, "高速高过载"),
(200, 150, 1.2, "超低空"),
]
for alt, spd, load, desc in test_cases:
result = check_flight_safety(tree, alt, spd, load)
print(f"{desc:8s} → {result}")
# 输出:
# 正常巡航 → ✅ 通过所有检查
# 低空慢速 → ⚠️ 需要关注
# 高速高过载 → ❌ 禁止飞行
# 超低空 → ❌ 禁止飞行
尾递归
如果一个函数的递归调用是函数体中最后一步操作(返回值就是递归调用的返回值),称为尾递归。
import numpy as np
# 不是尾递归:递归调用后还要做乘法
def compute_max_speed_non_tail(altitude: float, v0: float = 340.0, decay: float = 0.01) -> float:
if altitude <= 0.0:
return v0
return compute_max_speed_non_tail(altitude - 100.0, v0 * (1 - decay)) # ← 还要乘
# 改写为尾递归:用累加器传递中间结果
def compute_max_speed_tail(
altitude: float, acc: float = 340.0, decay: float = 0.01
) -> float:
if altitude <= 0.0:
return acc
return compute_max_speed_tail(altitude - 100.0, acc * (1 - decay), decay) # ← 直接返回
print(compute_max_speed_tail(5000)) # 输出:约 207.9
理论上尾递归可以转化为循环,不增加调用栈深度。但 CPython 解释器不做尾调用优化,因此尾递归在 Python 中并不能避免 RecursionError。
如果递归深度可能很大,应该手动改写为循环:
import numpy as np
def compute_max_speed_iter(altitude: float, v0: float = 340.0, decay: float = 0.01) -> float:
speed = v0
h = altitude
while h > 0.0:
speed *= (1 - decay)
h -= 100.0
return speed
递归 vs 迭代
任何递归都可以改写为迭代(用栈或循环模拟),反之亦然。两者各有优劣:
| 特性 | 递归 | 迭代 |
|---|---|---|
| 代码可读性 | 通常更简洁、贴近问题定义 | 有时较冗长 |
| 调用栈开销 | 每次调用都占栈帧 | 无栈帧开销 |
| 深度限制 | 受 sys.getrecursionlimit() 限制 | 无限制 |
| 性能 | 函数调用开销较大 | 通常更快 |
| 调试 | 调用栈清晰 | 状态变量较多 |
import numpy as np
# 递归版:计算一组飞行数据的累计能量
def sum_energy_recursive(energies: list[float]) -> float:
if not energies:
return 0.0
return energies[0] + sum_energy_recursive(energies[1:])
# 迭代版
def sum_energy_iterative(energies: list[float]) -> float:
total = 0.0
for e in energies:
total += e
return total
energy_data = [100.5, 200.3, 150.8, 300.1, 250.7]
print(f"递归:{sum_energy_recursive(energy_data):.1f}") # 输出:1001.4
print(f"迭代:{sum_energy_iterative(energy_data):.1f}") # 输出:1001.4
- 天然递归结构(树、图、分治、回溯):用递归更自然
- 简单循环(求和、累加、计数):用迭代更高效
- 递归深度大(> 几百层):必须用迭代
- 递归+缓存(记忆化搜索):可以解决很多动态规划问题
实战:飞行数据快速排序
快速排序是经典的分治算法,可用于对飞行数据进行排序分析:
def quicksort(arr: list[int]) -> list[int]:
"""快速排序(返回新列表,原地版本略复杂)。"""
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 quicksort(left) + middle + quicksort(right)
# 对飞行高度数据排序
altitudes = [3200, 1500, 4800, 2100, 3200, 900, 5600]
sorted_alt = quicksort(altitudes)
print(f"排序后高度:{sorted_alt}")
# 输出:[900, 1500, 2100, 3200, 3200, 4800, 5600]
# 原地版本(性能更好)
def quicksort_inplace(arr: list[int], low: int = 0, high: int | None = None) -> None:
"""原地快速排序。"""
if high is None:
high = len(arr) - 1
if low >= high: # 基线条件
return
pivot = arr[high]
i = low - 1
for j in range(low, high):
if arr[j] <= pivot:
i += 1
arr[i], arr[j] = arr[j], arr[i]
arr[i + 1], arr[high] = arr[high], arr[i + 1]
quicksort_inplace(arr, low, i)
quicksort_inplace(arr, i + 2, high)
data = [3200, 1500, 4800, 2100, 3200, 900, 5600]
quicksort_inplace(data)
print(f"原地排序:{data}")
# 输出:[900, 1500, 2100, 3200, 3200, 4800, 5600]
快速排序、归并排序、二分查找都是典型的分治算法,思路都是:
- 分:把问题分解为子问题
- 治:递归求解子问题
- 合:合并子问题的解
# 归并排序:另一个分治经典
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(a, b):
result = []
i = j = 0
while i < len(a) and j < len(b):
if a[i] <= b[j]:
result.append(a[i]); i += 1
else:
result.append(b[j]); j += 1
result.extend(a[i:])
result.extend(b[j:])
return result
print(merge_sort([3200, 1500, 4800, 2100, 3200, 900, 5600]))
# 输出:[900, 1500, 2100, 3200, 3200, 4800, 5600]
🎯 动手练习
- 包线边界计算:分别用递归、尾递归、迭代三种方式计算飞行包线边界,对比性能
- 轨迹规划优化:使用
@functools.cache优化递归轨迹规划,对比计算 100 步轨迹的时间差异 - 组件层级遍历:递归遍历飞行器系统组件树,统计所有底层组件的数量和类型分布
- 飞行数据排序:实现快速排序的递归版本和原地版本,对比飞行高度数据的排序性能
📚 延伸阅读
- 递归文档 - Python 递归限制说明
- functools.cache 文档 - 缓存装饰器
- sys.setrecursionlimit - 修改递归深度限制
- 分治算法 - 分治策略详解
基线条件递归终止条件if altitude <= 0: return v0递归条件调用自身处理子问题return compute_max_speed(altitude - 100, ...)递归深度限制默认 1000 层sys.getrecursionlimit()记忆化搜索缓存已计算结果@functools.cache尾递归最后一步是递归调用Python 不做 TCO 优化迭代替代用循环替代递归while 循环 + 累加器分治算法分→治→合快速排序、归并排序决策树遍历天然适合递归飞行安全检查、包线验证汉诺塔变体经典递归问题飞行任务调度 T(n) = 2*T(n-1) + 1延迟绑定循环中闭包陷阱lambda i=i: i 修复✅ 本节总结
- 递归函数必须包含基线条件(终止)和递归条件(调用自身)
- Python 默认递归深度上限 1000,可通过
sys.setrecursionlimit()调整,但不要盲目调高 - 典型应用:飞行包线边界计算、递归轨迹规划、飞行任务调度、系统组件层级遍历、飞行决策树
- 朴素递归效率可能很低(O(2^n)),用
@functools.cache记忆化搜索可优化到 O(n) - Python 不做尾调用优化,需要大深度时改用迭代
- 递归更贴近问题定义、可读性好;迭代性能更好、无深度限制
- 快速排序、归并排序是分治思想的典型应用