文章目录
简介
分而治之的算法设计思想
递归与分治
递归函数的设计思想:分而治之(减而治之)
自顶向下地解决问题
为什么需要使用栈?
这一步在我看剑指offer的时候,也理解到了。
拆分的时候「先走出去」,合并的时候「再走回来」
总结
自顶向下与自底向上
使用「递归」与「循环」实现的求阶乘函数对比
我们实现一个函数,输入 n ,输出 n 的阶乘。为了简化描述,我们不考虑输入为负整数,且输出发生整型溢出的情况。也就是说,我们假设输入是合法的,并且计算阶乘得到的结果的整数在 32 位整型范围之内。
public int factorial(int n) {
if (n == 1) {
return 1;
}
return n * factorial(n - 1);
}
注意:在递归方法调用返回以后,还能够做一些事情。就以上面的代码为例:factorial(n - 1) 返回以后,我们将返回的结果值和 n 相乘。以上的代码等价于下面这段代码:
public int factorial(int n) {
if (n == 1) {
return 1;
}
int res = factorial(n - 1);
// 在这里还能够执行一次操作,实现「分治思想」里「合并」的逻辑
return n * res;
}
这种「能够在递归调用返回的时候做一些事情」对应于「分治思想」的第 3 步「合并」的过程。「在上一层「递归」调用结束以后,我们可以实现一些逻辑」这一点是我们在学习递归的过程当中容易被忽略的,要重视这个细节的理解,才能够更好地理解,并且应用递归。
/**
* @param n
* @param res 递归函数上一层的结果,由于求的是阶乘,一开始需要传入 1
* @return
*/
public int factorial(int n, int res) {
if (n == 1) {
return res;
}
return factorial(n - 1, n * res);
}
使用循环计算 5!
如果我们知道了一个问题最开始的样子,就可以通过递推的方式一步一步求解,直到得到了我们想要的问题的解,相对于递归而言,这样的思考方向是「自底向上」的,计算 5!5! 我们还可以使用循环实现。代码如下:
public int factorial(int n) {
int res = 1;
for (int i = 2; i <= n; i++) {
res *= i;
}
return res;
}
友情提示:如果大家学习过「动态规划」的朋友就会知道,动态规划有两个思考的方向:一个是记忆化递归,另一个是递推。记忆化递归对应了「自顶向下」的解决问题的方向,递推对应了「自底向上」的逐步求解问题的方向。
很明显:「自底向上」思考问题的方向直接从一个问题的「源头」开始,逐步求解。相比较于「自顶向下」而言:
- 少了一层一层拆分问题的步骤;
- 也不需要借助一个数据结构(栈)记录拆分过程中的每一个子问题。
递推与递归
总结与练习
「自顶向下」与「自底向上」分别对应了我们解决问题的两种思考路径:
自顶向下:直接面对问题,直接解决问题;
自底向上:从这个问题最开始的样子出发,一点一点「逐步演化」成我们最终想要解决的问题的样子。
50. Pow(x, n)
我的代码,没有用到递归啊。。。。不过我因为昨天看了剑指offer,现在终于开始注意边界问题和代码鲁棒性了,现在看自己的代码,比之前还是进步不少的:
def myPow(self, x: float, n: int) -> float:
negative_flag = False
if x == 0: return 0
elif n == 0: return 1
elif n < 0: negative_flag = True
res = 1
for _ in range(abs(n)):
res *= x
if negative_flag: return 1 / res
else: return res
大数那里过不去,时间超了:
写了递归,也是大数那里过不去:
def myPow(self, x: float, n: int) -> float:
negative_flag = False
if x == 0: return 0
elif n == 0: return 1
elif n < 0: negative_flag = True
if n == 1: return x
if negative_flag: return 1 / (x * self.myPow(x, abs(n)-1))
else: return x * self.myPow(x, n-1)
下面是答案:
根据答案的思路,我写的快速幂➕递归,还是过不了大数:
def myPow(self, x: float, n: int) -> float:
neg_flag = False
if x == 0: return 0
elif n == 0: return 1
elif n < 0: neg_flag = True
if n % 2 != 0:
if neg_flag: return 1 / (self.myPow(x, abs(n)//2)**2 * x)
else: return self.myPow(x, n//2)**2 * x
else:
if neg_flag: return 1 / (self.myPow(x, abs(n)/2)**2)
else: return self.myPow(x, n/2)**2
注意这里用y*y代替了y**2,就可以过大数,不知道为什么
class Solution:
def myPow(self, x: float, n: int) -> float:
def quickMul(N):
if N == 0:
return 1.0
y = quickMul(N // 2)
return y * y if N % 2 == 0 else y * y * x
return quickMul(n) if n >= 0 else 1.0 / quickMul(-n)
剑指 Offer 65. 不用加减乘除做加法
之前做过一遍,复习过一遍,知道用位运算,但还是不会写。
直接看答案吧,这说明我的做题效率不高,虽然刷了题,但是刷完了,复习了一遍,还是写不出来,我可能是猪。只有自己大脑加工过的东西,才会记住,别人的思路,直接抄过来,永远都不会。
下面是根据大佬思路,尝试写的代码:
def add(self, a: int, b: int) -> int:
n = a ^ b
y = a & b << 1
return n + y
下面是大佬的代码:
def add(self, a: int, b: int) -> int:
x = 0xffffffff
a, b = a & x, b & x
while b != 0:
a, b = (a ^ b), (a & b) << 1 & x
return a if a <= 0x7fffffff else ~(a ^ x)
递归:
class Solution:
def add(self, a: int, b: int) -> int:
if b == 0: return a
return add(a ^ b, (a & b) << 1 )
面试题 08.05. 递归乘法
递归,看完《编程之美》后,发现自己的思路开始变了,开始尝试自己去思考答案了,而且要有信心自己可以写出来答案,如果自己现在囫囵吞枣,那么笔试或者面试的时候,那些题一定也会用同样的方法对待我们。要像稻盛和夫所言:敬畏的做每一道题,把每一道题的思路想通,再做。而且开始注意边界条件和鲁棒性了。
def multiply(self, A: int, B: int) -> int:
if A <= 0 or B <= 0: return -1
if B == 1: return A
return A + self.multiply(A, B-1)
可以看书,递归的调用,是函数的调用,每次函数调用,计算机会给函数和参数开辟额外的内存空间,所以占用的空间会比较大。相当于栈:后进先出。