前面我们通过《AI开发 - 算法基础 递归 的概念和入门(一) 递归算法的常见应用 PYTHON-CSDN博客》学习了递归的基础概念和常规的应用场景,其中有一个汉诺塔问题,有一些可玩的乐趣。今天我们在这里进一步通过这个问题来了解递归 和递归的使用需要注意什么。
汉诺塔问题是一个经典的递归问题,非常适合用来学习递归的概念。我们从基础开始,一步一步讲解。
1. 汉诺塔问题简介
汉诺塔问题是一个著名的数学谜题,问题的描述如下:
- 有三个柱子,分别叫做 A、B、C。
- 某个柱子(比如A柱子)上有若干个圆盘,圆盘的大小是不同的,并且最小的圆盘在最上面,最大的圆盘在最下面。
- 任务是将这些圆盘从柱子 A 移到柱子 C,移动时必须遵循以下规则:
- 每次只能移动一个圆盘。
- 每次只能将一个圆盘从上面拿走,且只能放在空柱子或者比它大的圆盘上。
- 要求在移动过程中,柱子 B 作为辅助柱子,可以帮助转移圆盘。
2. 递归思路解析
递归的关键是将大问题分解成更小的子问题。对于汉诺塔问题来说,假设我们要移动 n
个圆盘,从柱子 A 移到柱子 C,我们可以采取以下步骤:
- 第 1 步 :先将上面
n-1
个圆盘从柱子 A 移到柱子 B,借助柱子 C。 - 第 2 步 :将第
n
个圆盘(即最大的那个)从柱子 A 移到柱子 C。 - 第 3 步 :再将
n-1
个圆盘从柱子 B 移到柱子 C,借助柱子 A。
这个过程会一直递归下去,直到 n=1
,即只剩一个圆盘时,它就可以直接从 A 移到 C。
3. 编写递归函数
递归的核心是要有一个停止条件。对于汉诺塔问题,停止条件是当 n = 1
时,直接移动这个唯一的圆盘。
我们用 Python 来实现这个递归过程:
# 递归函数,移动 n 个圆盘
def hanoi(n, source, target, auxiliary):
# 递归停止条件:只有一个圆盘时,直接移动
if n == 1:
print(f"Move disk 1 from {source} to {target}")
return
# 第 1 步:将 n-1 个圆盘从 source 移到 auxiliary,使用 target 作为辅助
hanoi(n-1, source, auxiliary, target)
# 第 2 步:将第 n 个圆盘从 source 移到 target
print(f"Move disk {n} from {source} to {target}")
# 第 3 步:将 n-1 个圆盘从 auxiliary 移到 target,使用 source 作为辅助
hanoi(n-1, auxiliary, target, source)
# 测试函数:假设有 3 个圆盘
hanoi(3, 'A', 'C', 'B')
4. 解释代码
-
hanoi(n, source, target, auxiliary)
是递归函数,参数的含义是:n
:要移动的圆盘数。source
:源柱子(即要移出圆盘的柱子)。target
:目标柱子(即圆盘要移到的柱子)。auxiliary
:辅助柱子(即第三个柱子,用来暂时放置圆盘)。
-
当
n == 1
时,表示只有一个圆盘,这时直接将圆盘从source
移到target
。 -
当
n > 1
时,先递归地将上面n-1
个圆盘从source
移到auxiliary
,然后再将最大的那个圆盘(第n
个圆盘)从source
移到target
,最后递归地将n-1
个圆盘从auxiliary
移到target
。
5. 运行结果
假设有 3 个圆盘,运行结果如下:
Move disk 1 from A to C
Move disk 2 from A to B
Move disk 1 from C to B
Move disk 3 from A to C
Move disk 1 from B to A
Move disk 2 from B to C
Move disk 1 from A to C
这表示:
- 将第一个圆盘从 A 移到 C。
- 将第二个圆盘从 A 移到 B。
- 将第一个圆盘从 C 移到 B。
- 将第三个圆盘从 A 移到 C。
- 将第一个圆盘从 B 移到 A。
- 将第二个圆盘从 B 移到 C。
- 将第一个圆盘从 A 移到 C。
6. 递归思维要点
- 每次递归会将问题规模减小,直到问题足够简单(只有一个圆盘)。
- 递归调用的栈会自动处理好每一层的操作,并返回到上一层继续操作。
- 递归的本质是"分而治之",每一层都在解决小规模的子问题。
7. 递归的优缺点分析
汉诺塔问题的递归算法具有非常清晰和直观的结构,但在实际应用中也存在一些优缺点。我们来逐一分析它的优缺点。
- 1. 优点
-
代码简洁易懂: 递归算法是将问题分解成更小的子问题,非常符合汉诺塔问题的结构。代码简洁,逻辑清晰,易于理解。只需要遵循"移动 n-1 个圆盘"、"移动最大圆盘"、"再移动 n-1 个圆盘"的思路,就可以完美解决问题。
-
自然表达问题: 汉诺塔问题本身就是一个递归问题,因此递归解法非常自然地契合了问题的结构。没有复杂的迭代和状态管理,递归本身就能很好地解决问题。
-
理论优雅: 递归的思想与数学和理论计算机科学中的分治法相符合,能够很好的展示递归的基本思想:通过将大问题拆解为小问题来解决。这种方法在很多计算问题中都有广泛应用。
-
节省代码量: 相较于使用迭代方法,递归算法通常能写得更简洁。避免了手动管理循环变量和栈操作,递归通过系统的调用栈来管理这些状态。
- 2. 缺点
-
性能问题(时间复杂度高):
- 递归算法的时间复杂度是指数级别的,具体来说是
O(2^n)
。这是因为每次递归都会处理两个子问题,每个子问题的规模是原问题的一半,直到递归到最底层(n = 1
)。 - 这意味着对于大量的圆盘,算法的运行时间会非常长,随着
n
的增加,运行时间呈指数增长。
- 递归算法的时间复杂度是指数级别的,具体来说是
-
空间复杂度高:
- 递归算法会消耗较多的内存空间,因为每一次递归调用都会将当前的函数状态(如参数、局部变量等)压入系统的调用栈中。当递归的层数很深时,可能会导致栈溢出(
StackOverflowError
)。 - 空间复杂度是
O(n)
,即递归的最大深度与n
成正比。
示例:
- 如果
n
非常大(比如 1000 或更多),系统的栈可能就会溢出。
这就是在笔记本上跑50个盘,崩溃了!
实际上我跑这个case是通过vscode的jupyter notebook 远程连接到服务器上运行的,服务器很强大,占用内存如下:(不要轻易长似乎增加圆盘数哦!)
- 递归算法会消耗较多的内存空间,因为每一次递归调用都会将当前的函数状态(如参数、局部变量等)压入系统的调用栈中。当递归的层数很深时,可能会导致栈溢出(
-
不适合处理非常大的问题规模:
- 当问题规模增大时,递归算法的效率会急剧下降,特别是在
n
很大的时候。比如,n = 64
的情况下,递归算法将需要执行2^64
次操作,几乎无法在合理的时间内完成。 - 对于大规模问题,通常需要使用动态规划或迭代等其他方法来优化。
这是通过服务器运行,20个圆盘的递归运行时间达到了4s!
- 当问题规模增大时,递归算法的效率会急剧下降,特别是在
-
重复计算:
- 在递归过程中,很多子问题会被重复计算。例如,对于
hanoi(n-1, source, auxiliary, target)
和hanoi(n-1, auxiliary, target, source)
,这些递归会被多次计算,导致冗余计算。虽然汉诺塔本身是一个非常基础的递归问题,但对于更复杂的问题,递归算法可能会带来大量不必要的重复计算。
- 在递归过程中,很多子问题会被重复计算。例如,对于
-
理解和调试困难(对于初学者):
- 虽然递归算法在理论上很简单,但在实际调试和执行时,理解递归的执行过程、栈的展开与回收,可能会对初学者造成一定的困扰。特别是对递归的执行顺序和递归返回时的状态恢复需要足够的理解和经验。
- 对于递归的调用栈的管理也比较抽象,调试时可能会比较麻烦。
- 3. 何时选择递归方法
递归方法特别适用于以下情况:
- 问题本身就是递归的:当问题具有自然的递归结构时,递归算法可以非常直观地表达问题。
- 问题规模适中:如果问题规模不大,递归算法可以快速并且简单地解决问题,且能够清晰表达解法。
- 对时间复杂度要求不高:当问题的规模不会过大,或者对于小规模问题,递归方法是快速且简洁的选择。
- 4. 何时避免递归方法
避免递归算法的情形包括:
- 问题规模较大 :对于需要处理大量数据的情况(如
n
非常大时),递归的时间和空间复杂度可能会导致性能瓶颈。 - 内存消耗过高:当递归的调用栈较深时,可能会导致栈溢出,特别是在嵌套递归调用非常多的情况下。
- 需要高效计算:递归方法可能需要大量的重复计算,特别是在没有优化(如动态规划、记忆化递归)的情况下。
8. 对初学者的建议
理解递归对于初学者来说可能有些挑战,以下是我对学习递归的一些简短建议:
-
理解递归的基本概念 :递归就是函数调用自己。递归问题通常有两个部分:基本情况 (停止条件)和递归步骤(将问题分解为更小的子问题)。
-
从简单问题开始:先从简单的递归问题做起,比如阶乘、斐波那契数列和汉诺塔。
-
分解问题:将问题分解为更小的子问题,并确保每一步递归都在向基本情况靠近。
-
掌握递归的终止条件:递归函数必须有一个明确的停止条件,否则会导致无限递归。
-
多做练习:通过练习理解递归的执行过程,逐渐熟悉如何写递归代码。
-
画出递归树:用图形化的方式帮助理解递归的调用过程,尤其是递归的分支和返回过程。
-
调试递归:递归代码有时容易出错,可以使用打印调试或工具跟踪递归调用栈。
9. 写在最后
递归算法在解决汉诺塔问题时是非常自然且简洁的,它能够很好地展示递归的思想和分治法的应用。然而,递归也有其局限性,主要体现在性能上的不足,尤其是当问题规模较大时,时间复杂度和空间复杂度的限制会导致程序效率低下。
因此,在实际使用中,递归适用于规模较小或对效率要求不高的问题,而对于大规模问题,可以考虑其他优化方法或非递归算法(例如迭代、动态规划、尾递归优化等)。