仅用作个人学习记录
Reference:
https://inst.eecs.berkeley.edu/~cs61a/sp21/
函数的哲学
Syntactic specification(Signature)规定了调用函数的语法,包括需要几个参数。
Semantic specification 即函数注释部分,规定了:
- Precondition 先决条件是对函数调用者的要求;
- Postcondition 后置条件是函数设计者的承诺。
在理想条件下,调用者仅凭语法规范和语义规范就可使用函数的功能,而不需要了解函数的主体部分。
调用者(客户)关心的是提供怎样的参数和使用结果,但不关心结果是如何计算的。函数实现者关心的是结果如何计算,而不是参数从哪里来。从客户的角度来看,sqrt是计算函数平方根的各种方法的抽象,叫做功能抽象。
编程在很大程度上是关于选择抽象概念,从而设计清晰、快速和可维护的程序。在设计大型程序时,就是在不断抽象,分而治之,从而将一个复杂问题分解为较小的简单问题。
简单的线性递归
def sum squares(N):
"""Return The sum of K**2 for K from 1 to N (inclusive)."""
if N < 1:
return 0
else: # Use the comment!!
return sum squares(N - 1) + N**2
这是一个简单的线性递归,每个函数实例化一次递归调用。
sum squares(3) => sum squares(2) + 3**2
=> sum squares(1) + 2**2 + 3**2
=> sum squares(0) + 1**2 + 2**2 + 3**2
=> 0 + 1**2 + 2**2 + 3**2 => 14
尾部递归
在第三讲中,我们看到了一种特殊的递归,它与迭代密切相关。下面是这个例子的一个变体和一个相应的迭代版本,并标明了对应关系(见方框)。
右边的版本是一个尾部递归函数,意味着递归调用要么是返回值,要么是最后执行的动作。上述例子就是将返回值分解为部分和,返回部分和,最终得到返回值。
尾递归函数的特点是在回归过程中不用做任何操作,这个特性很重要,因为大多数现代的智能编译器会利用这种特点自动生成优化的代码,即将递归优化为循环,一般来说递归操作会不断压栈,占用大量空间。但 Python 不会这么做(将尾递归优化为循环)。
尾部递归是线性递归的一种。递归和循环一般来说可以相互转换。
递归思想
按照函数的哲学,我们仅凭函数的文档注释就可以使用这个函数,而不关心函数是这怎样实现的。因此在设计函数时,同样也可以使用该函数,即在函数实现内部调用自身。
在上述的sum squares 函数的实现中,可以观察到:
- 基准条件,即递归终止的情况,如果没有要加和的数时,结果是 0 (N < 1)
- 否则,结果就是 N**2 加上 1 到 N-1 的平方和
- 我们有一个函数(本身)可以计算平方和,调用这个函数
- 当 N >= 1时,就返回 N**2 + sum squares(N − 1)
只要我们能保证我们会达到基准条件(避免无限递归),这种“递归的信仰之跃”就会奏效。
避免无限递归
-
存在基准条件,即存在结束递归的条件。
-
确保每次递归时,递归的问题规模在减小。
-
在有限的步骤内可以达到基准条件。
递归与归纳法
在数学中,我们有各种各样的归纳法来证明一个属性P(x)对某个集合中的所有x都成立。
如对整数进行多米诺骨牌归纳的方法:
-
证明「 第一张骨牌会倒。」
-
证明 「只要任意一张骨牌倒了,其下一张骨牌也会因為前面的骨牌倒而跟著倒。」
-
则可下结论:所有的骨牌都会倒下。
即如果P(b) (the base case)成立,且对于所有的 k>b 有 P(k-1)成立可推出 P(k) 成立,就有任意k>b,P(k)成立。
进一步推广:
- 如果 P 是具有≺关系的集合上的属性,该属性要在有限步数内返回
- 如果你可以证明当 P(y) 对所有 y≺x 为真时,P(x) 也为真,
- 那么 P(x) 对所有x为真。
递归思维是一种归纳性思维。我们要证明的属性是,我们的函数对任何大小的输入都有效。
归纳法告诉我们,如果想要函数对所有的输入都有效,必须有:
- 每当我们的函数对所有比给定输入小的参数起作用时,它也一定对该输入起作用。
- 任何输入的序列中,每一个都比它的前一个小,最终一定满足结束条件。
相互递归
当一个递归过程被划分为两个相互调用的函数时,被称为相互递归。考虑以下非负整数的偶数和奇数的定义:
- 如果一个数比奇数大一,则这个数是偶数
- 如果一个数比偶数大一,则这个数是奇数
- 0 是偶数
利用这个定义,我们可以实现相互递归的函数来确定一个数字是偶数还是奇数。
def is_even(n):
if n == 0:
return True
else:
return is_odd(n-1)
def is_odd(n):
if n == 0:
return False
else:
return is_even(n-1)
result = is_even(4)
通过打破两个函数之间的抽象边界,可以将相互递归的函数变成一个单一的递归函数。在这个例子中,is_odd 的主体可以并入 is_even 的主体,确保在 is_odd 的主体中用 n-1 替换 n,以反映传递到它的参数。
def is_even(n):
if n == 0:
return True
else:
if (n-1) == 0:
return False
else:
return is_even((n-1)-1)
相互递归并不比简单递归更神秘、更强大,它提供了一种在复杂递归程序中保持抽象的机制。