数据结构与算法之递归

127 阅读5分钟

开启掘金成长之旅!这是我参与「掘金日新计划 · 2 月更文挑战」的第 7 天,点击查看活动详情

作者: 千石
支持:点赞、收藏、评论
欢迎各位在评论区交流

前言

本文内容来自我平时学习的一些积累,如有错误,还请指正

在题目实战部分,我将代码实现和代码解释设置在了解题思路的下方,方便各位作为参考刷题

本文大纲

递归.png

一些话

本文内容来自我平时学习的一些积累,如有错误,还请指正

在题目实战部分,我将代码实现和代码解释设置在了解题思路的下方,方便各位作为参考刷题

题目练习步骤:

  1. 给自己10分钟,读题并思考解题思路
  2. 有了思路以后开始写代码,如果在上一步骤中没有思路则停止思考并且看该题题解
  3. 在看懂题解(暂时没看懂也没关系)的思路后,背诵默写题解,直至能熟练写出来
  4. 隔一段时间,再次尝试写这道题目

前置知识

  • 实现:递归需要满足两个条件:

    1. 一个终止条件:防止递归无限循环
    2. 一个调用自身的语句:递归处理问题的子问题
  • 特性:

    1. 易于实现和理解:对于解决的问题的某些类型,递归可以比迭代更易于理解和实现。
    2. 易于维护:递归代码通常比迭代代码更容易维护。
    3. 效率低:递归代码比迭代代码执行得慢得多,因为它需要记录递归调用的状态。
  • 思维要点:

    1. 清晰的定义终止条件:避免无限递归。
    2. 确定如何将问题分解为子问题:确定如何递归地处理问题。
    3. 正确处理递归状态:在每次递归调用之间正确地存储和恢复状态。

题目

70. 爬楼梯

image.png

思路1:递归

这道题递归思路很明显,就是计算爬到第n阶的方案数,实际上就是前n-1阶和n-2阶的方案数之和。所以,我们可以递归地求解这两个子问题,最后相加得到答案。

复杂度分析

递归的时间复杂度为O(2^n),因为每次递归会产生两个子问题,总的问题数是指数级增长的。但是可以通过使用缓存来优化,以避免重复计算。

代码实现

class Solution:
    @cache
    def climbStairs(self, n: int) -> int:
        # 定义递归终止条件:当n<=2时,直接返回n,代表有n种方法
        if n <= 2:
            return n
        # 否则,递归地求解问题的两个子问题,即爬1阶和2阶的方案数,最后相加
        else:
            return self.climbStairs(n-1) + self.climbStairs(n-2)
image.png

优化:记忆化递归

使用递归来模拟爬楼梯的过程,从 n 开始递归,每次可以爬 1 阶或 2 阶,直到爬到楼顶。 使用记忆化数组 memo 来保存已经计算过的状态的结果,避免重复计算,优化时间复杂度。 复杂度分析:

复杂度分析

时间复杂度:O(n),因为每个状态最多被计算一次,所以总的计算次数不超过 n。 空间复杂度:O(n),因为需要使用长度为 n 的数组来保存状态的结果。

代码实现

class Solution:
    def climbStairs(self, n: int) -> int:
        memo = [-1 for i in range(n + 1)] # 记忆化数组,初始化为-1,表示该状态没有被计算过
        return self.helper(n, memo)
    
    def helper(self, n, memo):
        if n < 0: # 如果n小于0,说明该状态不合法,直接返回0
            return 0
        if n == 0: # 如果n为0,说明到达了楼顶,直接返回1
            return 1
        if memo[n] != -1: # 如果该状态已经被计算过了,直接返回记忆的结果
            return memo[n]
        memo[n] = self.helper(n - 1, memo) + self.helper(n - 2, memo) # 记忆该状态的结果
        return memo[n]
image.png

22. 括号生成

image.png
rubyCopy code
class Solution:
    def generateParenthesis(self, n: int) -> List[str]:
        res = []
        self.helper(n, n, "", res) # n代表左括号个数,n代表右括号个数
        return res
    
    def helper(self, left, right, path, res):
        if left == 0 and right == 0: # 如果左右括号都用完了,说明生成了一个合法的括号组合,将其加入结果列表
            res.append(path)
            return
        if left > 0: # 如果还有左括号未用,可以放一个左括号
            self.helper(left - 1, right, path + "(", res)
        if right > left: # 如果还有右括号未用且剩余的右括号数大于左括号数,可以放一个右括号
            self.helper(left, right - 1, path + ")", res)

思路:

  • 使用递归来模拟生成括号的过程,从 n 和 n 开始递归,分别代表剩余的左括号个数和右括号个数。
  • 每次递归中,可以选择放一个左括号或者右括号,并将其加入生成的括号组合字符串 path 中。
  • 当左右括号都用完了,说明生成了一个合法的括号组合,将其加入结果列表。

复杂度分析:

  • 时间复杂度:O(2(nn)/n)O(2^(n*n)/√n),每个括号有两种选择,共有 2(nn)2^(n*n) 个选择,但是大部分选择是不合法的,因此实际时间复杂度接近 O(4n/n)O(4^n/√n),每次递归都需要生成两个结果,共有 4n4^n 个递归,但是因为结果的单调性,右括号的个数最多只能比左括号多一个,因此实际复杂度接近 O(4n/n)O(4^n/√n)

  • 空间复杂度:O(n)O(n),递归栈的深度最多为 n。

代码实现

class Solution:
    def generateParenthesis(self, n: int) -> List[str]:
        res = []
        self.helper(n, n, "", res) # n代表左括号个数,n代表右括号个数
        return res
    
    def helper(self, left, right, path, res):
        if left == 0 and right == 0: # 如果左右括号都用完了,说明生成了一个合法的括号组合,将其加入结果列表
            res.append(path)
            return
        if left > 0: # 如果还有左括号未用,可以放一个左括号
            self.helper(left - 1, right, path + "(", res)
        if right > left: # 如果还有右括号未用且剩余的右括号数大于左括号数,可以放一个右括号
            self.helper(left, right - 1, path + ")", res)

image.png

总结

本文介绍了递归的实现、特性和思维要点,并且讲解了两道题目,希望对大家有所帮助