PyCourse/递归.ipynb

474 lines
16 KiB
Plaintext
Raw Normal View History

2024-06-30 12:03:17 +08:00
{
"cells": [
{
"cell_type": "markdown",
"metadata": {},
"source": [
"# 递归函数"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## 递归Recursion"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"在函数中有个理解门槛比较高的概念:**递归函数**Recursive Functions—— 那些**在自身内部调用自身的函数**。说起来都比较拗口。"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"在所有的项目中,都是非常常用的,这样子写,虽然运算复杂了、逻辑性要求更高,但是那是计算机考虑的事情,对人而言,写的代码少了,预计的结果出来了!"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"先看一个例子,我们想要有个能够计算 `n` 的*阶乘*factorial`n!` 的函数,`f()`,规则如下:\n",
"\n",
"> - `n! = n × (n-1) × (n-2)... × 1`\n",
"> - 即,`n! = n × (n-1)!`\n",
"> - 且,`n >= 1`\n",
">\n",
"> **注意**:以上是数学表达,不是程序,所以,`=` 在这一小段中是 “*等于*” 的意思,**不是程序语言中的赋值符号**。\n",
"\n",
"于是,计算 `f(n)` 的 Python 程序如下:\n"
]
},
{
"cell_type": "code",
"execution_count": 1,
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"120\n"
]
}
],
"source": [
"def f(n):\n",
" if n == 1:\n",
" return 1\n",
" else:\n",
" return n * f(n-1)\n",
" \n",
"print(f(5))"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## 递归函数的执行过程"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"以 factorial(5) 为例,让我们看看程序的流程:\n",
"\n",
"![](images/recursive-function-call.png)\n",
"\n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"当 f(5) 被调用之后,函数开始运行……\n",
"* 因为 `5 > 1`,所以,在计算 `n * f(n-1)` 的时候要再次调用自己 `f(4)`;所以必须等待 `f(4)` 的值返回;\n",
"* 因为 `4 > 1`,所以,在计算 `n * f(n-1)` 的时候要再次调用自己 `f(3)`;所以必须等待 `f(3)` 的值返回;\n",
"* 因为 `3 > 1`,所以,在计算 `n * f(n-1)` 的时候要再次调用自己 `f(2)`;所以必须等待 `f(2)` 的值返回;\n",
"* 因为 `2 > 1`,所以,在计算 `n * f(n-1)` 的时候要再次调用自己 `f(1)`;所以必须等待 `f(1)` 的值返回;\n",
"* 因为 `1 == 1`,所以,这时候不会再次调用 `f()` 了,于是递归结束,开始返回,这次返回的是 `1`\n",
"* 下一步返回的是 `2 * 1`\n",
"* 下一步返回的是 `3 * 2`\n",
"* 下一步返回的是 `4 * 6`\n",
"* 下一步返回的是 `5 * 24` —— 至此,外部调用 `f(5)` 的最终返回值是 `120`……\n",
"\n",
"加上一些输出语句之后,能更清楚地看到大概的执行流程:"
]
},
{
"cell_type": "code",
"execution_count": 1,
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"Call f(5)...\n",
"\tn = 5\n",
"\tn = 4\n",
"\tn = 3\n",
"\tn = 2\n",
"\tn = 1\n",
"Returning...\n",
"\tn = 1 return: 1\n",
"\tn = 2 return: 2\n",
"\tn = 3 return: 6\n",
"\tn = 4 return: 24\n",
"\tn = 5 return: 120\n",
"Get out of f(n), and f(5) = 120\n"
]
}
],
"source": [
"def f(n):\n",
" print('\\tn =', n)\n",
" if n == 1:\n",
" print('Returning...')\n",
" print('\\tn =', n, 'return:', 1)\n",
" return 1\n",
" else:\n",
" r = n * f(n-1)\n",
" print('\\tn =', n, 'return:', r)\n",
" return r\n",
" \n",
"print('Call f(5)...')\n",
"print('Get out of f(n), and f(5) =', f(5))"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## 递归的终点"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"递归函数在内部必须有一个能够让自己停止调用自己的方式,否则永远循环下去了……\n",
"\n",
"其实,我们所有人很小就见过递归应用,只不过,那时候不知道那就是递归而已。听过那个无聊的故事罢?\n",
"\n",
"> 山上有座庙,庙里有个和尚,和尚讲故事,说……\n",
"> > 山上有座庙,庙里有个和尚,和尚讲故事,说……\n",
"> > > 山上有座庙,庙里有个和尚,和尚讲故事,说……\n",
"\n",
"写成 Python 程序大概是这样:\n",
"\n",
"```python\n",
"def a_monk_telling_story():\n",
" print('山上有座庙,庙里有个和尚,和尚讲故事,他说…… ')\n",
" return a_monk_telling_story()\n",
"\n",
"a_monk_telling_story()\n",
"```\n",
"\n",
"这是个*无限循环*的递归,因为这个函数里*没有设置中止自我调用的条件*。无限循环还有个不好听的名字,叫做 “死循环”。"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"在著名的电影**盗梦空间**_2010_从整体结构上来看“入梦” 也是个 “递归函数”。只不过,这个函数和 `a_monk_telling_story()` 不一样,它并不是死循环 —— 因为它设定了*中止自我调用的条件*\n",
"\n",
"> 在电影里,醒过来的条件有两个\n",
">> * 一个是在梦里死掉;\n",
">> * 一个是在梦里被 kicked 到……\n",
">\n",
"> 如果这两个条件一直不被满足,那就进入 limbo 状态 —— 其实就跟死循环一样,出不来了……\n",
"\n",
"为了演示,我把故事情节改变成这样:\n",
"> * 入梦,`in_dream()`,是个递归函数;\n",
"> * 入梦之后醒过来的条件有两个:\n",
">> * 一个是在梦里死掉,`dead is True`\n",
">> * 一个是在梦里被 kicked`kicked is True`……\n",
">>\n",
">> 以上两个条件中任意一个被满足,就苏醒……\n",
"\n",
"至于为什么会死掉,如何被 kick我偷懒了一下管它怎样管它如何反正每个条件被满足的概率是 1/10……也只有这样我才能写出一个简短的能够运行的 “*盗梦空间程序*”。)\n",
"\n",
"把这个很抽象的故事写成 Python 程序,看看一次入梦之后能睡多少天,大概是这样:"
]
},
{
"cell_type": "code",
"execution_count": 9,
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"dead: False kicked: False\n",
"dead: False kicked: False\n",
"dead: False kicked: False\n",
"dead: True kicked: False\n",
"I slept 4 days, and was dead to wake up...\n",
"The in_dream() function returns: 4\n"
]
}
],
"source": [
"import random\n",
"\n",
"def in_dream(day=0, dead=False, kicked=False):\n",
" dead = not random.randrange(0,10) # 1/10 probability to be dead\n",
" kicked = not random.randrange(0,10) # 1/10 probability to be kicked\n",
" day += 1\n",
" print('dead:', dead, 'kicked:', kicked)\n",
" \n",
" if dead:\n",
" print((f\"I slept {day} days, and was dead to wake up...\"))\n",
" return day\n",
" elif kicked:\n",
" print(f\"I slept {day} days, and was kicked to wake up...\")\n",
" return day\n",
" \n",
" return in_dream(day)\n",
" \n",
"print('The in_dream() function returns:', in_dream())"
]
},
{
"cell_type": "code",
"execution_count": 10,
"metadata": {},
"outputs": [
{
"ename": "RecursionError",
"evalue": "maximum recursion depth exceeded",
"output_type": "error",
"traceback": [
"\u001b[0;31m---------------------------------------------------------------------------\u001b[0m",
"\u001b[0;31mRecursionError\u001b[0m Traceback (most recent call last)",
"Cell \u001b[0;32mIn[10], line 3\u001b[0m\n\u001b[1;32m 1\u001b[0m \u001b[38;5;28;01mdef\u001b[39;00m \u001b[38;5;21mx\u001b[39m(n):\n\u001b[1;32m 2\u001b[0m \u001b[38;5;28;01mreturn\u001b[39;00m n \u001b[38;5;241m*\u001b[39m x(n\u001b[38;5;241m-\u001b[39m\u001b[38;5;241m1\u001b[39m)\n\u001b[0;32m----> 3\u001b[0m \u001b[43mx\u001b[49m\u001b[43m(\u001b[49m\u001b[38;5;241;43m5\u001b[39;49m\u001b[43m)\u001b[49m\n",
"Cell \u001b[0;32mIn[10], line 2\u001b[0m, in \u001b[0;36mx\u001b[0;34m(n)\u001b[0m\n\u001b[1;32m 1\u001b[0m \u001b[38;5;28;01mdef\u001b[39;00m \u001b[38;5;21mx\u001b[39m(n):\n\u001b[0;32m----> 2\u001b[0m \u001b[38;5;28;01mreturn\u001b[39;00m n \u001b[38;5;241m*\u001b[39m \u001b[43mx\u001b[49m\u001b[43m(\u001b[49m\u001b[43mn\u001b[49m\u001b[38;5;241;43m-\u001b[39;49m\u001b[38;5;241;43m1\u001b[39;49m\u001b[43m)\u001b[49m\n",
"Cell \u001b[0;32mIn[10], line 2\u001b[0m, in \u001b[0;36mx\u001b[0;34m(n)\u001b[0m\n\u001b[1;32m 1\u001b[0m \u001b[38;5;28;01mdef\u001b[39;00m \u001b[38;5;21mx\u001b[39m(n):\n\u001b[0;32m----> 2\u001b[0m \u001b[38;5;28;01mreturn\u001b[39;00m n \u001b[38;5;241m*\u001b[39m \u001b[43mx\u001b[49m\u001b[43m(\u001b[49m\u001b[43mn\u001b[49m\u001b[38;5;241;43m-\u001b[39;49m\u001b[38;5;241;43m1\u001b[39;49m\u001b[43m)\u001b[49m\n",
" \u001b[0;31m[... skipping similar frames: x at line 2 (2969 times)]\u001b[0m\n",
"Cell \u001b[0;32mIn[10], line 2\u001b[0m, in \u001b[0;36mx\u001b[0;34m(n)\u001b[0m\n\u001b[1;32m 1\u001b[0m \u001b[38;5;28;01mdef\u001b[39;00m \u001b[38;5;21mx\u001b[39m(n):\n\u001b[0;32m----> 2\u001b[0m \u001b[38;5;28;01mreturn\u001b[39;00m n \u001b[38;5;241m*\u001b[39m \u001b[43mx\u001b[49m\u001b[43m(\u001b[49m\u001b[43mn\u001b[49m\u001b[38;5;241;43m-\u001b[39;49m\u001b[38;5;241;43m1\u001b[39;49m\u001b[43m)\u001b[49m\n",
"\u001b[0;31mRecursionError\u001b[0m: maximum recursion depth exceeded"
]
}
],
"source": [
"def x(n):\n",
" return n * x(n-1)\n",
"x(5)"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"不用深究上面盗梦空间这个程序的其它细节,不过,通过以上三个递归程序 —— 两个很扯淡的例子,一个正经例子 —— 你已经看到了递归函数的共同特征:\n",
"\n",
"> 1. 在 `return` 语句中返回的是*自身的调用*(或者是*含有自身的表达式*\n",
"> 2. 为了避免死循环,*一定要有至少一个条件*下返回的不再是自身调用……"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## 变量的作用域"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"再回来看计算阶乘的程序 —— 这是正经程序。这次我们把程序名写完整,`factorial()`:"
]
},
{
"cell_type": "code",
"execution_count": 6,
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"120\n"
]
}
],
"source": [
"def factorial(n):\n",
" if n == 1:\n",
" return 1\n",
" else:\n",
" return n * factorial(n-1)\n",
" \n",
"print(factorial(5))"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"最初的时候,这个函数的执行流程之所以令人迷惑,是因为初学者对*变量*的**作用域**把握得不够充分。\n",
"\n",
"变量根据作用域可以分为两种全局变量Global Variables和局部变量Local Variables。\n",
"\n",
"可以这样简化理解:\n",
"\n",
"> * 在函数内部被赋值而后使用的,都是*局部变量*,它们的作用域是*局部*,无法被函数外的代码调用;\n",
"> * 在所有函数之外被赋值而后开始使用的,是*全局变量*,它们的作用域是*全局*,在函数内外都可以被调用。\n",
"\n",
"定义如此,但通常程序员们会严格地遵守一条原则:\n",
"\n",
"> 在函数内部绝对不调用全局变量。即便是必须改变全局变量,也只能通过函数的返回值在函数外改变全局变量。\n",
"\n",
"你也必须遵守同样的原则。而这个原则同样可以在日常的工作生活中 “调用”:\n",
"\n",
"> 做事的原则:自己的事自己做,别人的事,最多通过自己的产出让他们自己去搞……"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"再仔细观察一下以下代码。当一个变量被当做参数传递给一个函数的时候,这个变量本身并不会被函数所改变。比如,`a = 5`,而后,再把 `a` 当作参数传递给 `f(a)` 的时候,这个函数当然应该返回它内部任务完成之后应该传递回来的值,但 `a` 本身不会被改变。"
]
},
{
"cell_type": "code",
"execution_count": 7,
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"5 120\n",
"120 120\n"
]
}
],
"source": [
"def factorial(n):\n",
" if n == 1:\n",
" return 1\n",
" else:\n",
" return n * factorial(n-1)\n",
" \n",
"a = 5\n",
"b = factorial(a) # a 并不会因此改变;\n",
"print(a, b)\n",
"a = factorial(a) # 这是你主动为 a 再一次赋值……\n",
"print(a, b)"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"理解了这一点之后,再看 `factorial()` 这个递归函数的递归执行过程,你就能明白这个事实:\n",
"\n",
"> 在每一次 factorial(n) 被调用的时候,它都会形成一个作用域,`n` 这个变量作为参数把它的值传递给了函数,*但是*`n` 这个变量本身并不会被改变。\n",
"\n",
"我们再修改一下上面的代码:"
]
},
{
"cell_type": "code",
"execution_count": 8,
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"5 120\n"
]
}
],
"source": [
"def factorial(n):\n",
" if n == 1:\n",
" return 1\n",
" else:\n",
" return n * factorial(n-1)\n",
" \n",
"n = 5 # 这一次,这个变量名称是 n\n",
"m = factorial(n) # n 并不会因此改变;\n",
"print(n, m)"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"在 `m = factorial(n)` 这一句中,`n` 被 `factorial()` 当做参数调用了,但无论函数内部如何操作,并不会改变变量 `n` 的值。\n",
"\n",
"关键的地方在这里:在函数内部出现的变量 `n`,和函数外部的变量 `n` 不是一回事 —— **它们只是名称恰好相同而已**,函数参数定义的时候,用别的名称也没什么区别:"
]
},
{
"cell_type": "code",
"execution_count": 1,
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"5 120\n"
]
}
],
"source": [
"def factorial(x): # 在这个语句块中出现的变量,都是局部变量\n",
" if x == 1:\n",
" return 1\n",
" else:\n",
" return x * factorial(x-1)\n",
" \n",
"n = 5 # 这一次,这个变量名称是 n\n",
"m = factorial(n) # n 并不会因此改变;\n",
"print(n, m)\n",
"# 这个例子和之前再之前的示例代码有什么区别吗?\n",
"# 本质上没区别,就是变量名称换了而已……"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"函数开始执行的时候,`x` 的值,是由外部代码(即,函数被调用的那一句)传递进来的。即便函数内部的变量名称与外部的变量名称相同,它们也不是同一个变量。"
]
}
],
"metadata": {
"kernelspec": {
"display_name": "Python 3",
"language": "python",
"name": "python3"
},
"language_info": {
"codemirror_mode": {
"name": "ipython",
"version": 3
},
"file_extension": ".py",
"mimetype": "text/x-python",
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
"version": "3.8.2"
}
},
"nbformat": 4,
"nbformat_minor": 2
}