📅  最后修改于: 2023-12-03 14:48:50.357000             🧑  作者: Mango
递归是一种常见的编程技巧,在递归函数中,函数调用自身来解决问题。递归在解决一些问题时可以让代码更加简洁清晰,但同时也需要我们注意一些递归中可能会遇到的问题,如递归栈溢出、重复计算等。本文将介绍不同类型的递归关系及其解决方案。
线性递归是最简单的递归形式,即在递归函数中只调用了一次函数本身。典型的例子是计算阶乘:
def factorial(n):
if n == 1:
return 1
else:
return n * factorial(n-1)
这个函数中只有一次函数调用 factorial(n-1)
,时间复杂度为 O(n)。但是由于递归调用栈的存在,空间复杂度为 O(n),有可能会造成栈溢出的问题。为了避免这种情况,我们可以使用尾递归优化。
尾递归是指递归函数的最后一步是直接调用自身,不做其他任何操作。在尾递归函数中,调用栈不需要保存本次调用的任何信息,因此可以大大减小调用栈的空间使用,避免栈溢出的问题。对于需要使用递归解决的线性问题,我们可以使用尾递归优化来提高代码性能。下面是计算阶乘的尾递归版本:
def factorial(n, acc=1):
if n == 1:
return acc
else:
return factorial(n-1, n*acc)
这个递归函数中使用了一个额外的参数 acc
来保存计算结果,将递归关系转化为了一个循环,使用时只需要传入一个初始值即可。尾递归优化的时间和空间复杂度都为 O(n),避免了栈溢出问题。
二叉树递归是指在二叉树中进行递归操作。二叉树递归通常出现在二叉树的遍历、查找、插入、删除等操作中。通常的模板如下:
class TreeNode:
def __init__(self, val=0, left=None, right=None):
self.val = val
self.left = left
self.right = right
def traverse(root):
if not root:
return
# Do something with root
traverse(root.left)
traverse(root.right)
这个函数首先判断当前节点是否为空,如果是则直接返回;否则依次递归遍历左右子树,并在遍历过程中进行一些其他操作。这种递归关系的时间复杂度为 O(n),但与线性递归不同的是,二叉树递归的空间复杂度通常是 O(logn) 的,因为每次递归只会占用一层栈空间。
在二叉树的遍历、查找、插入、删除等操作中,往往需要在遍历过程中记录一些信息,并回溯到之前的节点进行操作。因此,我们可以通过递归+回溯的方式实现对二叉树的操作。
下面是一个在二叉树中查找符合条件的节点的例子:
def find_node(root, target):
def helper(node):
if not node:
return None
if node.val == target:
return node
left = helper(node.left)
if left:
return left
right = helper(node.right)
if right:
return right
return None
return helper(root)
在这个函数中,我们定义了一个 helper
函数进行递归操作,并在每个节点中判断是否满足条件。如果当前节点符合条件,则直接返回;否则继续递归搜索左右子树,如果左右子树中存在符合条件的节点,则返回该节点,否则返回 None。
在二叉树插入和删除操作中,由于需要对二叉树结构进行修改,因此需要注意回溯的处理。
对于一些复杂的递归问题,比如分治、动态规划等,我们需要更加深入地理解递归的本质及其时间和空间复杂度,才能更好地解决问题。
分治递归是指将一个问题分成若干个子问题进行求解,再将子问题的解合并成原问题的解。分治递归通常需要递归地调用自身两次,将问题划分为两个子问题进行求解。下面是一个求解最大子序列和的分治算法:
def max_subarray(nums):
def helper(left, right):
if left == right:
return nums[left]
mid = (left + right) // 2
left_sum = helper(left, mid)
right_sum = helper(mid+1, right)
cross_sum = cross_helper(left, right, mid)
return max(left_sum, right_sum, cross_sum)
def cross_helper(left, right, mid):
left_sum = float('-inf')
curr_sum = 0
for i in range(mid, left-1, -1):
curr_sum += nums[i]
left_sum = max(left_sum, curr_sum)
right_sum = float('-inf')
curr_sum = 0
for i in range(mid+1, right+1):
curr_sum += nums[i]
right_sum = max(right_sum, curr_sum)
return left_sum + right_sum
return helper(0, len(nums)-1)
这个算法首先将问题划分成两个子问题:求解左边子数组的最大子序列和、求解右边子数组的最大子序列和、求解跨越中心点的最大子序列和。然后递归地对这两个子问题进行求解,再将结果进行合并。时间复杂度为 O(nlogn),空间复杂度为 O(logn)。
动态规划是指在求解一个问题时,将其分成若干个子问题进行求解,并将子问题的解存储下来,避免重复计算。通常采用递归+备忘录的方式进行实现。下面是一个求解斐波那契数列的例子:
def fibonacci(n):
memo = [None] * (n+1)
def helper(n):
if n == 0 or n == 1:
return n
if memo[n]:
return memo[n]
memo[n] = helper(n-1) + helper(n-2)
return memo[n]
return helper(n)
这个函数中使用了备忘录 memo
来保存已经计算过的值,避免重复计算。在递归过程中,首先判断是否存在已经计算过的值,如果是则直接返回;否则递归进行计算,并将计算结果存入备忘录中。时间复杂度为 O(n),空间复杂度为 O(n)。
递归作为编程技巧的一种重要形式,在程序设计中应用广泛。本文介绍了线性递归、二叉树递归、复杂递归的相关知识,以及解决这些递归问题所需要的技巧。对于一些复杂的递归问题,我们需要更加深入地理解递归的本质及其时间和空间复杂度,才能更好地解决问题。