LeetCode 剑指Offer 数据结构之 递归 总结 Part1
递归算法主要应用于树结构的搜索与遍历。其他方面也有一些经典的问题,比如斐波那契数列,跳台阶等,很多问题中动态规划都能替代递归算法。本章将对剑指Offer题库中几道递归算法考题进行总结,记录一些对于递归算法的入门理解,同时简单的与动态规划解法进行一些对比。关于动态规划的部分将在新的文章中进行总结。
递归算法核心在于函数中重复调用本函数,明确终止条件+下一次的函数调用输入(递推公式产生)即可
剑指 Offer 07:
重建二叉树
输入某二叉树的前序遍历和中序遍历的结果,请重建该二叉树。假设输入的前序遍历和中序遍历的结果中都不含重复的数字。
例如,给出
前序遍历 preorder = [3,9,20,15,7]
中序遍历 inorder = [9,3,15,20,7]
返回如下的二叉树:
3
/ \
9 20
/ \
15 7
经典的递归问题,二叉树中有提到过。
先序遍历每科子树的第一位都是根节点,不断从中序遍历中找根节点的索引,对左右子树分别进行递归
终止条件是子树的先序遍历节点都放入root就停止
def buildTree(self, preorder, inorder):
if not preorder or not inorder:
return None
root = TreeNode(preorder.pop(0))
indexroot = inorder.index(root.val)
root.left = self.buildTree(preorder, inorder[:indexroot])
root.right = self.buildTree(preorder, inorder[indexroot+1:])
return root
总结:生成树是自上而下的
剑指 Offer 10- I:
斐波那契数列
写一个函数,输入 n ,求斐波那契(Fibonacci)数列的第 n 项。斐波那契数列的定义如下:
F(0) = 0, F(1) = 1
F(N) = F(N - 1) + F(N - 2), 其中 N > 1.
斐波那契数列由 0 和 1 开始,之后的斐波那契数就是由之前的两数相加而得出。
答案需要取模 1e9+7(1000000007),如计算初始结果为:1000000008,请返回 1
经典的递归,动态规划题
可以得出递归与动态规划的一些对比,以及带备忘录的递归优化方式:
解法1:
def fib(self, n: int) -> int:
# method1 recursive
if n == 0:
return 0
if n == 1:
return 1
return self.fib(n-1) + self.fib(n-2)
数列第n项,经典的递归公式:
a
n
+
2
=
a
n
+
1
+
a
n
a_{n+2} = a_{n+1} + a_{n}
an+2=an+1+an
终止条件,两个递归函数n减少到0,或者1
解法2: 带dp空间的动态规划
def fib(self, n: int) -> int:
# method1 recursive
if n == 0:
return 0
if n == 1:
return 1
return self.fib(n-1) + self.fib(n-2)
解法3:带备忘录的递归
把已经得到的中间项用字典保存起来,在下次需要计算的时候先查找一下
主要是n-2,n-1不重复计算了
my_dic = {0:0, 1:1}
def my_fib(n):
if n in my_dic:
return my_dic[n]
if n-2 not in my_dic:
my_dic[n-2] = my_fib(n-2)
if n-1 not in my_dic:
my_dic[n-1] = my_fib(n-1)
my_dic[n] = (my_dic[n-2] + my_dic[n-1])%1000000007
return my_dic[n]
return my_fib(n)
剑指 Offer 10- II:
青蛙跳台阶问题
一只青蛙一次可以跳上1级台阶,也可以跳上2级台阶。求该青蛙跳上一个 n 级的台阶总共有多少种跳法。
答案需要取模 1e9+7(1000000007),如计算初始结果为:1000000008,请返回 1。
考虑到最后一次跳跃,可以跳2,可以跳1所以所有的方式是以n-2最后一次跳2 + n-1最后一次跳1组成
递推公式:
a
n
=
a
n
−
1
+
a
n
−
2
a_{n} = a_{n-1} + a_{n-2}
an=an−1+an−2
本题同样可以用动态规划 与 递归分别求解
def numWays(self, n):
'''
递归
'''
if n==0:
return 1
if n==1:
return 1
if n==2:
return 2
records = [-1 for i in range(n+1)]
if records[n] == -1: # 表明这个值没有算过
records[n] = (self.numWays(n-1) +self.numWays(n-2))%1000000007
return records[n]
def numWays(self,n):
'''
动态规划
'''
a, b = 1, 1
for _ in range(n):
a, b = b, a + b# a代表dp[n],b代表dp[n-1],一次转移 b代表dp[n] ,a+b代表dp[n-1],dp[n-2]
return a % 1000000007
显然动态规划空间复杂度很低
剑指 Offer 16:
数值的整数次方
实现函数double Power(double base, int exponent),求base的exponent次方。不得使用库函数,同时不需要考虑大数问题。
示例 1:
输入: 2.00000, 10
输出: 1024.00000
示例 2:
输入: 2.10000, 3
输出: 9.26100
示例 3:
输入: 2.00000, -2
输出: 0.25000
解释: 2-2 = 1/22 = 1/4 = 0.25
快速幂,优化的根据是当幂为偶数时,可以先对底数平方,幂除以2节约计算
终止条件是幂不断除2取整之后等于0停止
def myPow1(self, x, n):
if n == 0:# 终止条件
return 1
if n < 0:
return self.myPow1(1 / x, -1 * n)
if n % 2 == 0:
return self.myPow1(x * x, n // 2)
else:
return x * self.myPow1(x * x, n // 2)
其他解法,二进制法(详细解释将在位运算总结中再进行归纳)
def myPow(self, x, n):
if x == 0:
return 0
res = 1
if n < 0:
x, n = 1 / x, -n
while n:
if n & 1:
res *= x
x *= x
n >>= 1
return res
总结:递归主要依赖于递推公式+终止条件,往往是从下到上(n项从大到小,最后终止)的感觉。 有明确递推公式的问题都可以用动态规划进行替代,而且动态规划还可以通过优化dp空间,状态转移方式等获得更优的时间,空间复杂度指标。
下面将对动态规划算法的考题进行总结。