PyCourse/递归.ipynb

474 lines
16 KiB
Plaintext
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

{
"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
}