又回来念一念递归的经,编程之旅上有笔直的大路,也有盘一盘的递归? 今天探讨递归是引人入胜的思想 — 递归也许是上帝做工的方式。函数调用自己则体现了内在统一的秩序,
递归究竟是什么?
每年我们都会长一岁,试试定义一个函数 deltaYear
只做一件事情,就是在输入的age上的年龄上 +1 (增加 1)
def deltaYear(age):
if age == 996:return
#age = 997 就报错!
print('age',age)
return deltaYear(age+1)
print(deltaYear(age=0))
deltaYear(age)
一直不断地调用自己,age变量也随之而变化!
这也是大喵能想到的最简洁的递归栗子,直观地看到 age = 997 时,程序出现错误:
RecursionError: maximum recursion depth
exceeded while calling a Python object
请参见最后几步的输出:
...
age 990
age 991
age 992
age 993
age 994
age 995
Traceback (most recent call last):
[Previous line repeated 993 more times]
File "C:\Users\29358\PycharmProjects\Python_workspace-main\4 ddm_course\02_Python inner function\RecursionABC.py", line 7, in deltaYear
print('age',age)
RecursionError: maximum recursion depth exceeded while calling a Python object
爆栈的次数发生在什么时候?
好了,让我们戴上探险家的帽子。想象一下,你身处一个神奇的森林。是的,编程可以是神奇的!
你偶然发现了一棵会说话的树,它要求你数一下它的树枝上的所有叶子。
但有一个小插曲 — 这棵树的树枝分成了更小的树枝,而那些树枝又分成更小的树枝。简而言之,你需要数在嵌套的叶子中的叶子。
编程中的递归有点像这个情景 — 一个函数调用自身来解决问题,问题会被分成越来越小的部分。
递归函数的要点
那么,你听说过“递归函数”这个术语,它听起来有点神秘,对吧?好吧,让我们来揭开它的神秘面纱。
递归函数就像一个解谜伙伴。它将一个复杂的问题分解为相同问题的简化版本,直到达到基本情况 — 题目中最小的可解部分。
让我们来看一个经典的例子 — 计算阶乘。假设你想找出一个数 n 的阶乘。
你知道 n! 就等于 n 乘以 (n-1)!,而 (n-1)! 又等于 (n-1) 乘以 (n-2)!,以此类推,直到我们得到 1!,它就是 1。
那么,这在 Python 中是这样的:
def factorial(n):
if n == 1:
return 1
else:
return n * factorial(n - 1)
看到了吗?我们使用相同的函数将问题分解成更小的部分,最终达到递归停止的基本情况。
信任之舞:基本情况和递归情况
好了,想象一下,你正在举办一个派对,邀请了一些朋友。你告诉你的朋友邀请他们的朋友,而他们的朋友又邀请更多的朋友。
但是,有个问题 — 你至少需要一个朋友来开始这个连锁。在递归中,这个“一个朋友”就是你的基本情况。如果没有它,你就会陷入无限循环的派对策划中!
在我们的阶乘例子中,基本情况是当 n 等于 1 时。这就是开始派对的朋友。剩下的链条,我们在其中将 n 与 (n-1)! 相乘,就是递归情况。就像每个朋友都邀请他们自己的一组朋友一样。
解开魔法:调用栈
好的,让我们看一看幕后情况。当你在 Python 中调用一个函数时,计算机会创建一个调用栈 — 这是一个函数调用的堆栈的花哨说法。
在我们的递归旅程中,每次调用阶乘函数时,一个新的函数实例都会被添加到堆栈中。但请记住,堆栈的空间是有限的!
所以,当我们深入递归时,堆栈会变得更高。但当我们达到那个神奇的基本情况时,堆栈开始缩小,因为每个函数调用都会完成并返回其值。这就像解开礼物一样 — 你一层一层地剥开,直到达到里面的礼物!
小心:无限陷阱
现在,我的朋友们,在触发递归末日之前,请记住那个至关重要的基本情况。如果你忘了它,你将发现自己陷入无限的函数调用坑中 — 那可不是一个有趣的派对!
总结递归的乐趣
所以,这就是 Python 中递归的世界,一点也不可怕。我们已经解决了基础知识 — 理解递归的概念,区分基本情况和递归情况,以及瞥见调用栈的神奇之处。
递归就像一个解谜巫师,将问题分解成一口口大小的块。只要有正确的基本情况和对如何组合这些块的清晰理解,你将能够创建递归函数,令即使是最智慧的编程树也会赞叹不已。
汉诺塔
MIT的CS课堂对递归的理解并没有用上述的栗子。递归不仅是函数调用自己本身,还有什么精妙之处被以上的栗子忽略?
经典的做法是用汉诺塔理解递归。汉诺塔之所以经典,是因为不同于前面谈到的两个栗子,只包含递归的一个要素:函数自己调用自己。
![alt](https://img-blog.csdnimg.cn/img_convert/3f9bde3960076983b321ff173ce1e8e2.png)
图示有 8 个Disk,A, B, C三个柱。任务是将A柱的disk全部移动到C 柱即移动到最右侧的柱,还原为A 柱的叠放状态:保持较小的总是在较大的上面。
搬动过程中要满足两个条件,搬动过程中:每次只能搬动一片;每一次搬动保证尺寸大的Disk不能放在小的Disk上面。
试一试看?
自上而下将 8 个disk分别叫1、2 .... 7、8号盘,任务目标是搬动 8 个盘到 C 柱。
那么,任务等价于最先将 8 号盘放在 C的最下面,再将1-7号盘放在 8 之上。
注意我的表述方式,任务分为两个子任务。
def tower_of_hanoi(n, source, auxiliary, target):
if n == 1:
print(f"Move disk 1 from {source} to {target}")
return
tower_of_hanoi(n-1, source, target, auxiliary)
print(f"Move disk {n} from {source} to {target}")
tower_of_hanoi(n-1, auxiliary, source, target)
# 示例用法
tower_of_hanoi(4, 'A', 'B', 'C')
输出结果:
Move disk 1 from A to B
Move disk 2 from A to C
Move disk 1 from B to C
Move disk 3 from A to B
Move disk 1 from C to A
Move disk 2 from C to B
Move disk 1 from A to B
Move disk 4 from A to C
Move disk 1 from B to C
Move disk 2 from B to A
Move disk 1 from C to A
Move disk 3 from B to C
Move disk 1 from A to B
Move disk 2 from A to C
Move disk 1 from B to C
C++:
cpp
#include <iostream>
void tower_of_hanoi(int n, char source, char auxiliary, char target) {
if (n == 1) {
std::cout << "Move disk 1 from " << source << " to " << target << std::endl;
return;
}
tower_of_hanoi(n-1, source, target, auxiliary);
std::cout << "Move disk " << n << " from " << source << " to " << target << std::endl;
tower_of_hanoi(n-1, auxiliary, source, target);
}
int main() {
int n = 3; // 汉诺塔的盘子数
tower_of_hanoi(n, 'A', 'B', 'C');
return 0;
}
这两个示例代码都实现了汉诺塔问题的解决方案。在这个问题中,有三个杆(通常称为A、B和C),以及一些不同大小的圆盘,开始时都放在一个杆上。
目标是将所有圆盘从起始杆移动到目标杆,遵守以下规则:
一次只能移动一个盘子。
任何时候,大盘子不能放在小盘子的上面。
这两个示例都使用递归来解决问题,因为汉诺塔问题本身就是递归性质的经典问题。这些代码将打印出移动每个盘子的步骤,以完成整个汉诺塔谜题。
当你解决汉诺塔问题时,搬动的总次数可以通过数学公式来计算,而不需要实际模拟每一步的搬动。下面是一个计算搬动次数的Python程序:
def hanoi_moves(n):
if n == 1:
return 1
else:
return 2 * hanoi_moves(n - 1) + 1
#示例用法
num_disks = 3 # 汉诺塔的盘子数
total_moves = hanoi_moves(num_disks)
print(f"移动的总次数:{num_disks} disks: {total_moves}")
这个程序使用递归方式计算汉诺塔问题所需的总搬动次数。
公式为:Total moves = 2**n - 1
![alt](https://img-blog.csdnimg.cn/img_convert/b21b0458e5ea4c8c49b858ebf4c822b5.png)
其中,n 是汉诺塔的盘子数。
在示例用法中,我们计算了有3个盘子的汉诺塔问题所需的总搬动次数。你可以根据需要更改 num_disks 的值来计算其他盘子数量的搬动次数。
本文由 mdnice 多平台发布