AI开发 - 算法基础 递归 的概念和入门(三)递归的进阶学习

前面我们通过2篇文章,一起了解了 递归,以及使用递归来解决汉诺塔问题。今天我们在这个基础上,进一步地熟悉和学习递归。 

这篇学习笔记将涵盖递归的基本概念、应用、优化技巧、陷阱及与迭代的对比,并通过具体的 Python 代码示例和大家一起来深入理解递归的使用。

一、 巩固基础

1. 递归的概念

递归,简单来说就是函数自己调用自己。听起来有点绕,但其实就像俄罗斯套娃,一层套一层,直到遇到最小的那个娃娃(基线条件)才停止。

递归是指一个函数直接或间接地调用自身。它通常由两个关键要素构成:

  • 基线条件(Base Case):递归结束的条件,防止无限递归。
  • 递归条件(Recursive Case):将问题分解成更小的子问题,并递归调用自身来解决这些子问题。
2. 递归的调用栈

每次递归调用都会把当前的局部变量、返回地址等压入调用栈,直到达到基线条件并开始回溯。理解调用栈有助于调试递归程序。

3. 示例:阶乘与斐波那契数列
  • 阶乘

阶乘是一个经典的递归例子,定义为 n! = n * (n-1)!,直到 1! = 1

def factorial(n):if n == 0:  # 基线条件return 1else:return n * factorial(n - 1)  # 递归条件

调用 factorial(5) 会通过递归逐步调用直到 factorial(0),然后逐步返回结果。

想象一下,计算5的阶乘:

  1. factorial(5) 调用 factorial(4)

  2. factorial(4) 调用 factorial(3)

  3. factorial(3) 调用 factorial(2)

  4. factorial(2) 调用 factorial(1)

  5. factorial(1) 调用 factorial(0)

  6. factorial(0) 遇到基线条件,返回1

  7. 然后逐层返回,最终得到 5 * 4 * 3 * 2 * 1 * 1 = 120

  • 斐波那契数列

斐波那契数列的递归定义为 F(n) = F(n-1) + F(n-2),直到 F(0) = 0F(1) = 1

def fibonacci(n):if n <= 1:return nelse:return fibonacci(n - 1) + fibonacci(n - 2)
4. 使用调试工具可视化递归调用栈

可以通过调试工具(例如 PyCharm 或 VS Code 的调试器)逐步观察递归调用栈。每进入一次递归,都会看到栈中增加一个新的调用帧,直到基线条件触发。

也使用调试工具例如Python的pdb,可以一步步跟踪递归函数的执行过程,观察每次调用时变量的变化,帮助你更直观地理解递归。

二、 递归的应用场景

递归在算法设计中就像一把瑞士军刀,可以解决各种问题。递归在很多经典算法中都有广泛应用:

1. 分治法

分治法的精髓在于“分而治之”,把大问题拆解成小问题,分别解决后再合并结果。

  • 归并排序

归并排序是一种典型的分治算法,通过递归地将数组分成两半进行排序,然后合并已排序的两部分。

def merge_sort(arr):if len(arr) <= 1:return arrmid = len(arr) // 2left = merge_sort(arr[:mid])right = merge_sort(arr[mid:])return merge(left, right)def merge(left, right):result = []while left and right:if left[0] < right[0]:result.append(left.pop(0))else:result.append(right.pop(0))result.extend(left)result.extend(right)return result
  • 快速排序

快速排序通过递归地选择一个基准元素,将数组分为比基准小和比基准大的两部分,然后对这两部分分别进行排序。

def quick_sort(arr):if len(arr) <= 1:return arrpivot = arr[len(arr) // 2]left = [x for x in arr if x < pivot]middle = [x for x in arr if x == pivot]right = [x for x in arr if x > pivot]return quick_sort(left) + middle + quick_sort(right)
2. 回溯法

回溯法是通过递归尝试所有可能的解,并在遇到错误时回退并继续尝试其他解。

  • 八皇后问题

通过递归摆放皇后,并判断每一层是否满足规则。如果满足规则就进入下一层,否则回退。

八皇后问题是一个经典的回溯算法问题,目标是在一个8x8的棋盘上放置8个皇后,使得它们互不攻击(即任意两个皇后不能在同一行、同一列或同一对角线上)。

def solve_n_queens(n):# 初始化棋盘,-1表示未放置皇后board = [-1] * n# 存储所有合法的棋盘布局solutions = []# 回溯函数def backtrack(row):# 如果已经放置了n个皇后,保存当前棋盘布局if row == n:solutions.append(board[:])return# 尝试在当前行的每一列放置皇后for col in range(n):# 检查当前位置是否合法if is_valid(board, row, col):# 放置皇后board[row] = col# 递归处理下一行backtrack(row + 1)# 回溯:撤销当前行的皇后放置board[row] = -1# 检查在第row行第col列放置皇后是否合法def is_valid(board, row, col):# 遍历之前的所有行for i in range(row):# 检查是否在同一列或同一对角线上if board[i] == col or abs(board[i] - col) == row - i:return Falsereturn True# 从第0行开始回溯backtrack(0)# 返回所有合法的棋盘布局return solutions

代码解析

2.1. 数据结构

  • board: 一个长度为n的列表,表示棋盘。board[i] = j表示在第i行第j列放置了一个皇后。

  • solutions: 存储所有合法的棋盘布局。

2.2. 核心函数

  • backtrack(row): 递归回溯函数,尝试在第row行放置皇后。

    • 如果row == n,说明已经成功放置了n个皇后,将当前棋盘布局保存到solutions中。

    • 否则,遍历当前行的每一列,尝试放置皇后:

      • 如果当前位置合法(通过is_valid检查),则放置皇后,并递归处理下一行。

      • 递归结束后,撤销当前行的皇后放置(回溯),尝试其他可能性。

  • is_valid(board, row, col): 检查在第row行第col列放置皇后是否合法。

    • 遍历之前的所有行,检查是否有皇后在同一列或同一对角线上。

2.3. 运行流程

  1. 初始化一个大小为n的棋盘board,所有值初始化为-1(表示未放置皇后)。

  2. 调用backtrack(0),从第0行开始尝试放置皇后。

  3. backtrack函数中:

    • 如果当前行row == n,说明找到一种合法布局,将其保存到solutions中。

    • 否则,遍历当前行的每一列,尝试放置皇后:

      • 如果当前位置合法,则放置皇后,并递归处理下一行。

      • 递归结束后,撤销当前行的皇后放置,尝试其他列。

  4. 最终返回所有合法的棋盘布局solutions

3. 树的遍历

树形结构本身就是递归的天然应用。二叉树的前序、中序、后序遍历都可以通过递归实现。

class TreeNode:def __init__(self, val=0, left=None, right=None):self.val = valself.left = leftself.right = rightdef preorder(root):if root:print(root.val)preorder(root.left)preorder(root.right)

三、 递归的优化

1. 记忆化搜索

记忆化搜索是一种优化递归的技术,目的是避免重复计算相同的子问题。常用于斐波那契数列等问题。

def fibonacci_memo(n, memo={}):if n in memo:return memo[n]if n <= 1:return nmemo[n] = fibonacci_memo(n - 1, memo) + fibonacci_memo(n - 2, memo)return memo[n]
2. 尾递归优化

尾递归是指递归调用出现在函数的最后一步,可以将递归转化为循环,从而避免栈溢出问题。

def factorial_tail(n, acc=1):if n == 0:return accreturn factorial_tail(n - 1, n * acc)

各位需要注意的是:Python 并不支持尾递归优化,因此对于深度较大的递归,仍然要小心栈溢出。

四、 递归的陷阱

1. 栈溢出

递归深度过大时,调用栈会导致栈溢出。可以通过增加基线条件或者将递归转化为迭代来避免。

2. 重复计算

递归可能会多次计算相同的子问题,造成效率低下。例如,斐波那契数列的递归实现会计算许多重复的子问题。

3. 效率低下

递归相较于循环通常会带来额外的函数调用开销,导致效率较低。

五、 递归与迭代

递归和迭代各有优缺点:

  • 递归在处理分治问题、树的遍历等复杂问题时非常直观。
  • 迭代相对效率更高,特别是对于简单问题,如阶乘、斐波那契数列。

有些递归问题可以转化为迭代解决,特别是尾递归问题。

六、 实践项目

递归在实际问题中有广泛应用。以下是几个经典递归问题:

  • 全排列:给定一组数字,输出所有可能的排列。
  • 组合问题:从一个集合中选取若干元素的所有组合。
  • 子集问题:从一个集合中生成所有的子集。
# 组合问题示例
def combine(n, k):res = []def backtrack(start, path):if len(path) == k:res.append(path)returnfor i in range(start, n + 1):backtrack(i + 1, path + [i])backtrack(1, [])return res

继续扩展递归应用,我们将通过实际问题来进一步理解递归的强大功能。

以下是我们一起来使用递归解决的三个实际问题:文件目录遍历JSON 数据解析HTML 文档解析

6.1. 文件目录遍历

文件目录遍历是一个常见的递归问题,因为目录可以包含文件和子目录,而子目录又可能包含更多的文件和子目录。因此,我们可以通过递归来遍历整个文件树。

代码示例:递归遍历文件夹

import osdef traverse_directory(path):# 获取路径下的所有文件和子目录for item in os.listdir(path):full_path = os.path.join(path, item)if os.path.isdir(full_path):  # 如果是目录,递归遍历print(f"Directory: {full_path}")traverse_directory(full_path)  # 递归调用else:  # 如果是文件,打印文件路径print(f"File: {full_path}")# 示例调用
traverse_directory('/path/to/directory')

解释:

  • os.listdir(path) 返回指定目录下的所有文件和子目录的名称。
  • os.path.isdir(full_path) 判断路径是否是一个目录。
  • 如果是目录,则递归调用 traverse_directory,遍历该目录。
  • 如果是文件,则直接打印文件路径。

通过这种方式,可以遍历整个文件树,不管目录有多深,这个非常实用!

6.2. JSON 数据解析

递归常常用于处理具有嵌套结构的数据,像 JSON 这样的格式通常包含字典、列表等复杂嵌套结构。使用递归可以帮助我们解析和提取其中的数据。

代码示例:递归解析 JSON 数据

假设我们有一个复杂的 JSON 数据,其中包含嵌套的字典和列表。

import json# 假设的 JSON 数据
data = '''
{"name": "Alice","age": 25,"address": {"city": "Wonderland","zipcode": "12345"},"hobbies": ["reading", "painting", {"name": "sports", "types": ["soccer", "basketball"]}]
}
'''def parse_json(obj):if isinstance(obj, dict):for key, value in obj.items():print(f"{key}:")parse_json(value)  # 递归调用,处理字典的每个值elif isinstance(obj, list):for item in obj:parse_json(item)  # 递归调用,处理列表中的每个元素else:print(f"Value: {obj}")  # 打印最终的值# 解析 JSON 数据
parse_json(json.loads(data))

解释:

  • parse_json 函数检查对象类型:
    • 如果是字典,就递归地遍历字典的键值对。
    • 如果是列表,就递归地遍历列表中的元素。
    • 如果既不是字典也不是列表,那么就是基本数据类型,直接打印值。
  • 使用 json.loads 将字符串转换为 Python 对象后传递给 parse_json 进行解析。

这种递归解析方法适用于处理结构复杂的 JSON 数据,能够处理不确定的深度和层级。

6.3. HTML 文档解析

HTML 文档通常是由标签嵌套构成的树形结构,因此解析 HTML 时递归是非常自然的选择。我们可以使用递归遍历 HTML 元素及其子元素。

代码示例:递归解析 HTML 文档

假设我们需要解析一个简单的 HTML 文件,提取其中的所有 a 标签(超链接)。

from html.parser import HTMLParserclass MyHTMLParser(HTMLParser):def handle_starttag(self, tag, attrs):if tag == 'a':  # 如果是 <a> 标签for attr in attrs:if attr[0] == 'href':print(f"Link: {attr[1]}")  # 打印链接地址# 示例 HTML 文档
html_data = '''
<html><head><title>Test Page</title></head><body><h1>Welcome to My Webpage</h1><p>Here are some links:</p><a href="https://example.com">Example</a><a href="https://another-example.com">Another Example</a></body>
</html>
'''# 创建并使用 HTMLParser 对象
parser = MyHTMLParser()
parser.feed(html_data)

解释:

  • HTMLParser 是 Python 的标准库,用于解析 HTML 内容。
  • handle_starttag 方法在每次遇到开始标签时被调用。当标签是 a 时,我们从其属性中提取 href,即超链接地址。
  • feed 方法将 HTML 字符串传递给解析器,递归解析 HTML 内容并提取超链接。

在实际的 HTML 解析中,递归处理各个标签及其子标签是非常常见的,尤其是当文档结构复杂时。

七、 拓展学习

拓展学习可以让我们在理解和掌握递归的基础上,进行融汇贯通,将递归运用在实际需要的地方。

一点建议:

  • 函数式编程:学习函数式编程语言,如 Haskell 或 Lisp,有助于深入理解递归的思想。
  • 数学归纳法:递归和数学归纳法密切相关,理解递归关系式和数学归纳法的原理有助于深入理解递归的本质。
  • 学习递归相关的数学知识,例如:

    • 递归关系式

    • 数学归纳法

  • 学习资源:

  • 书籍:

    • 《算法导论》

    • 《编程珠玑》

  • 网站:

    • LeetCode

    • LintCode

  • 视频:

    • MIT OpenCourseware: Introduction to Algorithms

  • 从简单的例子开始,逐步深入理解递归。

  • 多动手实践,尝试用递归解决不同的问题。

  • 不要害怕犯错,调试是学习递归的重要部分。

通过以上内容,我们已经将递归的基础知识和应用场景进行了充分阐述,后续我们就可以通过实际练习不断提升递归的能力。祝你好运!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/diannao/66934.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

计算机视觉算法实战——车道线检测

✨个人主页欢迎您的访问 ✨期待您的三连 ✨ ✨个人主页欢迎您的访问 ✨期待您的三连 ✨ ✨个人主页欢迎您的访问 ✨期待您的三连✨ ​​​​​​ ​​​​​​​​​​​​ ​​​​​ 车道线检测是计算机视觉领域的一个重要研究方向&#xff0c;尤其在自动驾驶和高级驾驶辅助…

【微服务】面试 3、 服务监控 SkyWalking

微服务监控的原因 问题定位&#xff1a;在微服务架构中&#xff0c;客户端&#xff08;如 PC 端、APP 端、小程序等&#xff09;请求后台服务需经过网关再路由到各个微服务&#xff0c;服务间可能存在多链路调用。当某一微服务挂掉时&#xff0c;在复杂的调用链路中难以迅速确定…

【MySQL数据库】基础总结

目录 前言 一、概述 二、 SQL 1. SQL通用语法 2. SQL分类 3. DDL 3.1 数据库操作 3.2 表操作 4. DML 5. DQL 5.1 基础查询 5.2 条件查询 5.3 聚合函数 5.4 分组查询 5.5 排序查询 5.6 分页查询 6. DCL 6.1 管理用户 6.2 权限控制 三、数据类型 1. 数值类…

aws(学习笔记第二十三课) step functions进行开发(lambda函数调用)

aws(学习笔记第二十三课) 开发step functions状态机的应用程序 学习内容&#xff1a; step functions状态机的概念开发简单的step functions状态机 1. step functions状态机概念 官方说明文档和实例程序 AWS的官方给出了学习的链接和实例程序。使用SAM创建step functions 借…

【Docker】入门教程

目录 一、Docker的安装 二、Docker的命令 Docker命令实验 1.下载镜像 2.启动容器 3.修改页面 4.保存镜像 5.分享社区 三、Docker存储 1.目录挂载 2.卷映射 四、Docker网络 1.容器间相互访问 2.Redis主从同步集群 3.启动MySQL 五、Docker Compose 1.命令式安装 …

算法练习7——拦截导弹的系统数量求解

题目描述 某国为了防御敌国的导弹袭击&#xff0c;发展出一种导弹拦截系统。但是这种导弹拦截系统有一个缺陷&#xff1a;虽然它的第一发炮弹能够到达任意的高度&#xff0c;但是以后每一发炮弹都不能高于前一发的高度。 假设某天雷达捕捉到敌国的导弹来袭。由于该系统还在试用…

如何使用高性能内存数据库Redis

一、详细介绍 1.1、Redis概述 Redis&#xff08;Remote Dictionary Server&#xff09;是一个开源的、内存中的数据结构存储系统&#xff0c;它可以用作数据库、缓存和消息中间件。Redis支持多种类型的数据结构&#xff0c;如字符串&#xff08;strings&#xff09;、哈希&am…

【Linux系列】`find / -name cacert.pem` 文件搜索

&#x1f49d;&#x1f49d;&#x1f49d;欢迎来到我的博客&#xff0c;很高兴能够在这里和您见面&#xff01;希望您在这里可以感受到一份轻松愉快的氛围&#xff0c;不仅可以获得有趣的内容和知识&#xff0c;也可以畅所欲言、分享您的想法和见解。 推荐:kwan 的首页,持续学…

UE材质Fab Megascans

2025年Bridge里已经不能直接导入资产了&#xff0c;显示GET IT ON FAB 只能在Fab中导入资产&#xff0c; 纹理打包技术从RMA改成了ORM O&#xff1a;AO 环境光遮蔽 R&#xff1a;Roughness 粗糙度 M&#xff1a;Metallic 金属度 在Fab中找到材质&#xff0c;点击Add to P…

【NP-hard问题】NP与NP-hard问题通俗解释

最近在研究NP-hard问题&#xff0c;讲一下自己的对于NP与NP-hard问题的通俗解释 一、NP-Hard 问题是什么意思&#xff1f; 什么是 NP&#xff1f; NP 问题可以理解为「检查答案很容易&#xff0c;但找到答案很难」。 举个例子&#xff1a; 假设你在一个迷宫里&#xff0c;…

ollama教程(window系统)

前言 在《本地大模型工具哪家强&#xff1f;对比Ollama、LocalLLM、LM Studio》一文中对比了三个常用的大模型聚合工具优缺点&#xff0c;本文将详细介绍在window操作系统下ollama的安装和使用。要在 Windows 上安装并使用 Ollama&#xff0c;需要依赖 NVIDIA 显卡&#xff0c…

[论文阅读] (35)TIFS24 MEGR-APT:基于攻击表示学习的高效内存APT猎杀系统

《娜璋带你读论文》系列主要是督促自己阅读优秀论文及听取学术讲座&#xff0c;并分享给大家&#xff0c;希望您喜欢。由于作者的英文水平和学术能力不高&#xff0c;需要不断提升&#xff0c;所以还请大家批评指正&#xff0c;非常欢迎大家给我留言评论&#xff0c;学术路上期…

半导体数据分析: 玩转WM-811K Wafermap 数据集(三) AI 机器学习

前面我们已经通过两篇文章&#xff0c;一起熟悉了WM-811K Wafermap 数据集&#xff0c;并对其中的一些数据进行了调用&#xff0c;生成了一些统计信息和图片。今天我们接着继续往前走。 半导体数据分析&#xff1a; 玩转WM-811K Wafermap 数据集&#xff08;二&#xff09; AI…

BGP 泄露

大家读完觉得有帮助记得关注和点赞&#xff01;&#xff01;&#xff01; 目录 1. BGP 是什么&#xff1f; 2. 什么是 BGP 泄露&#xff1f; 3. 今天发生了什么&#xff1f; 4. 正常和被劫持状态下的路由示意图 5. 受影响区域 6. 责任在谁&#xff1f; 7. 有办法避免这…

wireshark排除私接小路由

1.wireshark打开&#xff0c;发现了可疑地址&#xff0c;合法的地址段DHCP是192.168.100.0段的&#xff0c;打开后查看发现可疑地址段&#xff0c;分别是&#xff0c;192.168.0.1 192.168.1.174 192.168.1.1。查找到它对应的MAC地址。 ip.src192.168.1.1 2.通过show fdb p…

使用 CompletableFuture 实现异步编程

在现代 Java 开发中&#xff0c;异步编程是一项重要技能。而 CompletableFuture 是从 Java 8 开始提供的一个功能强大的工具&#xff0c;用于简化异步任务的编写和组合。本文将详细介绍 CompletableFuture 的基本使用和一些常见的应用场景。 1. 为什么选择 CompletableFuture&…

AWS云计算概览(自用留存,整理中)

目录 一、云概念概览 &#xff08;1&#xff09;云计算简介 &#xff08;2&#xff09;云计算6大优势 &#xff08;3&#xff09;web服务 &#xff08;4&#xff09;AWS云采用框架&#xff08;AWS CAF&#xff09; 二、云经济学 & 账单 &#xff08;1&#xff09;定…

【江协STM32】10-4/5 I2C通信外设、硬件I2C读写MPU6050

1. I2C外设简介 STM32内部集成了硬件I2C收发电路&#xff0c;可以由硬件自动执行时钟生成、起始终止条件生成、应答位收发、数据收发等功能&#xff0c;减轻CPU的负担支持多主机模型支持7位/10位地址模式支持不同的通讯速度&#xff0c;标准速度(高达100 kHz)&#xff0c;快速…

Web开发中页面出现乱码的解决(Java Web学习笔记:需在编译时用 -encoding utf-8)

目录 1 引言2 乱码表现、原因分析及解决2.1 乱码表现2.2 原因分析2.3 解决 3 总结 1 引言 Web开发的页面出现了乱码&#xff0c;一直不愿写出来&#xff0c;因为网上的解决方案太多了。但本文的所说的页面乱码问题&#xff0c;则是与网上的大多数解决方案不一样&#xff0c;使…

分类模型为什么使用交叉熵作为损失函数

推导过程 让推理更有体感&#xff0c;进行下面假设&#xff1a; 假设要对猫、狗进行图片识别分类假设模型输出 y y y&#xff0c;是一个几率&#xff0c;表示是猫的概率 训练资料如下&#xff1a; x n x^n xn类别 y ^ n \widehat{y}^n y ​n x 1 x^1 x1猫1 x 2 x^2 x2猫1 x …