11# 递归和动态规划
22
3- 动态规划可以理解为是查表的递归 。那么什么是递归?
3+ 动态规划可以理解为是 ** 查表的递归(记忆化) ** 。那么什么是递归?什么是查表(记忆化) ?
44
55## 递归
66
7- 定义: 递归算法是一种直接或者间接调用自身函数或者方法的算法 。
7+ 定义: 递归是指在函数的定义中使用函数自身的方法 。
88
9- 算法中使用递归可以很简单地完成一些用循环实现的功能,比如二叉树的左中右序遍历。递归在算法中有非常广泛的使用,
10- 包括现在日趋流行的函数式编程。
9+ 算法中使用递归可以很简单地完成一些用循环实现的功能,比如二叉树的左中右序遍历。递归在算法中有非常广泛的使用,包括现在日趋流行的函数式编程。
1110
1211> 纯粹的函数式编程中没有循环,只有递归。
1312
14- 接下来我们来讲解一下递归。通俗来说,递归算法的实质是把问题分解成规模缩小的同类问题的子问题,然后递归调用方法来表示问题的解
13+ 通俗来说,递归算法会把问题分解成规模缩小的同类子问题,当子问题缩写到寻常的时候,我们可以知道它的解。然后我们建立递归函数之间的联系即可解决原问题。
14+
15+ 接下来我们来讲解一下递归。
1516
1617### 递归的三个要素
1718
18- 1 . 一个问题的解可以分解为几个子问题的解
19- 2 . 子问题的求解思路除了规模之外,没有任何区别
20- 3 . 有递归终止条件
19+ 一个问题要使用递归来解决有递归终止条件(算法的有穷性)。并且通常情况下,递归会逐步缩小规模到寻常。
20+
21+ 虽然以下代码也是递归,但由于其无法结束,因此不是一个有效的算法:
2122
22- 我这里列举了几道算法题目,这几道算法题目都可以用递归轻松写出来:
23+ ``` py
24+ def f ():
25+ return f()
26+ ```
27+
28+ 更多的情况应该是:
29+
30+ ``` py
31+ def f (n ):
32+ return n + f(n - 1 )
33+ ```
34+
35+ 使用递归函数的优点是逻辑简单清晰,缺点是过深的调用会导致栈溢出。这里我列举了几道算法题目,这几道算法题目都可以用递归轻松写出来:
2336
2437- 递归实现 sum
2538
3548
3649一个简单练习递归的方式是将你写的迭代全部改成递归形式。比如你写了一个程序,功能是“将一个字符串逆序输出”,那么使用迭代将其写出来会非常容易,那么你是否可以使用递归写出来呢?通过这样的练习,可以让你逐步适应使用递归来写程序。
3750
51+ 如果你已经对递归比较熟悉了,那么我们继续往下看。
52+
3853### 递归中的重复计算
3954
40- 递归中存在这么多的重复计算, 一种简单的方式就是记忆化递归。即一边递归一边使用“记录表”记录我们已经计算过的情况,这样就避免了重复计算。而动态规划中 DP 数组其实和“记录表”一样 。
55+ 递归中可能存在这么多的重复计算,为了消除这种重复计算, 一种简单的方式就是记忆化递归。即一边递归一边使用“记录表”(比如哈希表) 记录我们已经计算过的情况,当下次我们计算的时候,如果之前已经计算了,那么直接返回即可, 这样就避免了重复计算。而 ** 动态规划中 DP 数组其实和“记录表”的作用是一样的 ** 。
4156
4257你可以尝试使记忆化更加通用和非侵入性,即应用记忆化技术而不改变原来的功能。 (提示:可以参考一种被称作 decorator 的设计模式)。
4358
4762
4863## 动态规划
4964
50- ` 如果说递归是从问题的结果倒推,直到问题的规模缩小到寻常。 那么动态规划就是从寻常入手, 逐步扩大规模到最优子结构。 ` 这句话需要一定的时间来消化,
51- 如果不理解,可以过一段时间再来看。
65+ ` 如果说递归是从问题的结果倒推,直到问题的规模缩小到寻常。 那么动态规划就是从寻常入手, 逐步扩大规模到最优子结构。 ` 这句话需要一定的时间来消化,如果不理解,可以过一段时间再来看。
66+
67+ 递归的解决问题非常符合人的直觉,代码写起来比较简单。我们可以通过分析(可以尝试画一个递归树),可以看出递归在缩小问题规模的同时** 是否可能会重复计算** 。 [ 279.perfect-squares] ( ../problems/279.perfect-squares.md ) 中 我通过递归的方式来解决这个问题,同时内部维护了一个缓存来存储计算过的运算,那么我们可以减少很多运算。 这其实和动态规划有着异曲同工的地方。
5268
53- 递归的解决问题非常符合人的直觉,代码写起来比较简单。但是我们通过分析(可以尝试画一个递归树),可以看出递归在缩小问题规模的同时可能会
54- 重复计算。 [ 279.perfect-squares] ( ../problems/279.perfect-squares.md ) 中 我通过递归的方式来解决这个问题,同时内部维护了一个缓存
55- 来存储计算过的运算,那么我们可以减少很多运算。 这其实和动态规划有着异曲同工的地方。
69+ > 小提示:如果你发现并没有重复计算,那么就没有必要用记忆化递归或者动态规划了。
5670
5771我们结合求和问题来讲解一下,题目是给定一个数组,求出数组中所有项的和,要求使用递归实现。
5872
@@ -71,9 +85,7 @@ function sum(nums) {
7185
7286![ dynamic-programming-1] ( ../assets/thinkings/dynamic-programming-1.png )
7387
74- 这种做法本身没有问题,但是每次执行一个函数都有一定的开销,拿 JS 引擎执行 JS 来说,
75- 每次函数执行都会进行入栈操作,并进行预处理和执行过程,所以对于内存来说是一个挑战。
76- 很容易造成爆栈。
88+ 这种做法本身没有问题,但是每次执行一个函数都有一定的开销,拿 JS 引擎执行 JS 来说,每次函数执行都会进行入栈操作,并进行预处理和执行过程,所以内存很容易造成爆栈。
7789
7890> 浏览器中的 JS 引擎对于代码执行栈的长度是有限制的,超过会爆栈,抛出异常。
7991
0 commit comments