当前位置: 首页 > news >正文

【算法竞赛学习笔记】基础算法篇:递归再探

前言

本文为个人学习的算法学习笔记,学习笔记,学习笔记不是经验分享与教学,不是经验分享与教学,不是经验分享与教学,若有错误各位大佬轻喷(T^T)。主要使用编程语言为Python3,各类资料题目源于网络,主要自学途径为蓝桥云课,侵权即删。

一、学习目标

  1. 理解递归的核心现象(“递” 与 “归” 的双向过程)
  2. 掌握递归在数学和计算机科学中的严格定义
  3. 熟练运用递归代码的通用模板,具备独立编写 Python 递归代码的能力
  4. 明确递归过程中的关键注意事项(避免重复计算、栈溢出等)

二、递归现象:从生活例子理解 “递” 与 “归”

递归的本质是 “先递后归”—— 先将问题逐层拆解为更小的同类问题(递的过程),直到触及可直接解决的最小问题,再将结果逐层回溯合并(归的过程)。

文档中经典生活案例:某计姓家族子孙询问 “18 代祖的名字”,过程如下:

  • 递的过程:子孙问父亲→父亲问祖父→祖父问曾祖父→……→直到 18 代祖(最小问题:18 代祖知道自己的名字)
  • 归的过程:18 代祖告诉儿子 “我叫你猜”→儿子告诉孙子→……→最终子孙得到答案 “你猜”

通过该案例可直观理解:递归必须包含 “拆解(递)” 和 “回溯(归)” 两个环节,缺一不可。

三、递归的定义

在数学和计算机科学中,递归(Recursion)是指在函数定义中直接或间接调用函数自身的方法,其核心是 “用同类小规模问题的解构建原问题的解”。

1. 数学定义

若一个问题的解可表示为更小规模同类问题的解,且存在最小解(终止条件),则可通过递归定义。示例:函数 \(f(n) = f(n-1) + 1\)(其中 \(f(1) = 1\))

  • 原问题:求 \(f(3)\)
  • 拆解(递):\(f(3) = f(2) + 1\) → \(f(2) = f(1) + 1\)
  • 终止(最小解):\(f(1) = 1\)(无需再拆解)
  • 回溯(归):\(f(2) = 1 + 1 = 2\) → \(f(3) = 2 + 1 = 3\)

2. 计算机科学定义(Python 实现)

在代码中,递归表现为 “函数调用自身”,需满足 “可重复子问题” 和 “递归出口” 两大条件。示例(对应上述数学函数):

def f(n):# 递归出口(最小子问题的解)if n == 1:return 1# 调用自身(拆解为小规模问题)return f(n - 1) + 1# 测试:计算f(3)
print(f(3))  # 输出:3

四、递归的两大核心要点

递归能正确运行的前提是满足以下两个条件,缺一不可:

1. 可拆解为 “可重复子问题”

原问题的解可拆分为多个子问题的解,且子问题与原问题的求解思路完全一致,仅数据规模不同

  • 关键特征:子问题的 “逻辑结构” 和 “求解步骤” 与原问题相同。
  • 示例(计算 n 的阶乘):
    • 原问题:\(n! = n \times (n-1)!\)
    • 子问题:\((n-1)! = (n-1) \times (n-2)!\)
    • 规律:所有子问题均遵循 “当前数 × 前一个数的阶乘”,仅 “当前数” 的规模不同,符合 “可重复子问题” 要求。

若子问题与原问题思路不同(如 “计算 n! 时突然需要计算 n 的平方”),则无法用递归解决。

2. 必须有 “递归出口”(终止条件)

存在一个 “最小子问题”,其解可直接给出(无需再拆解调用自身),否则会导致函数无限递归,最终引发栈溢出。

  • 示例 1:阶乘的递归出口是 \(n=1\) 时返回 1(\(1! = 1\) 是已知结论)。
  • 示例 2:斐波那契数列的递归出口是 \(F(0)=0\)、\(F(1)=1\)(最小子问题的解明确)。

反例:若删除阶乘函数的终止条件(if n == 1: return 1),则函数会无限调用 \(f(n-1)\)、\(f(n-2)\)…… 直到栈溢出。

五、递归代码通用模板(Python 优先)

递归代码的结构具有高度规律性,可总结为以下两类模板(根据问题复杂度选择):

1. 基础模板(适用于简单递归问题)

适用于仅需 “拆解 + 回溯”,无需中间层处理的场景(如阶乘、简单求和),对应文档中的基础模板逻辑。

def recursive_func(parameters):# 1. 递归出口:处理最小子问题,直接返回结果if 终止条件:return 最小子问题的解# 2. 递的过程:调用自身,缩小问题规模(参数需调整)return recursive_func(缩小后的参数)

示例(计算 n 的阶乘)

def factorial(n):# 递归出口:0!和1!均为1if n in (0, 1):return 1# 拆解为n × (n-1)!return n * factorial(n - 1)# 测试:计算5!
print(factorial(5))  # 输出:120

2. 完整模板(适用于复杂递归问题)

适用于需要 “处理当前层逻辑” 或 “回溯后善后” 的场景(如二叉树遍历、链表反转),对应文档中包含四层结构的模板。

def recursive_complete(level, other_params):# Part 1:递归出口(终止条件,超过最大层级则终止)if level > 最大层级:return 终止时的结果  # 或直接return(无返回值场景)# Part 2:处理当前层逻辑(根据问题需求编写,如修改数据、打印信息)process(level, other_params)# Part 3:递的过程(进入下一层,缩小问题规模,层级+1)recursive_complete(level + 1, other_params)# Part 4:归的过程(回溯后善后,如恢复状态、释放资源,按需编写)# post_process(level, other_params)return 当前层的结果  # 按需返回(无返回值则省略)

示例(二叉树前序遍历)

# 定义二叉树节点类
class TreeNode:def __init__(self, val=0, left=None, right=None):self.val = valself.left = leftself.right = rightdef pre_order_traversal(node, level=1):# Part 1:递归出口(节点为空,无需处理)if node is None:return# Part 2:当前层逻辑(打印当前节点值和层级)print(f"层级{level}:节点值{node.val}")# Part 3:进入下一层(先遍历左子树,再遍历右子树,层级+1)pre_order_traversal(node.left, level + 1)pre_order_traversal(node.right, level + 1)# 构建测试二叉树:    1
#                  /   \
#                 2     3
root = TreeNode(1, TreeNode(2), TreeNode(3))
# 测试前序遍历
pre_order_traversal(root)
# 输出:
# 层级1:节点值1
# 层级2:节点值2
# 层级2:节点值3

六、模拟演练:实现 “神秘函数” S (x)

1. 问题描述(来自文档)

神秘函数 \(S(x)\) 定义如下:

  • 当 \(x = 0\) 时,\(S(0) = 1\)(递归出口);
  • 当 x 为偶数时,\(S(x) = S(x/2)\)(拆解为更小问题);
  • 当 x 为奇数时,\(S(x) = S(x-1) + 1\)(拆解为更小问题)。

输入:正整数 x(\(1 ≤ x ≤ 10^6\)),输出 \(S(x)\) 的值。

2. 样例分析(输入 x=7)

  • 递的过程:\(S(7) → S(6)+1 → S(3)+1 → S(2)+1 → S(1)+1 → S(0)+1\)
  • 归的过程:\(S(0)=1 → S(1)=1+1=2 → S(2)=2 → S(3)=2+1=3 → S(6)=3 → S(7)=3+1=4\)
  • 最终输出:4

3. Python 代码实现

def calculate_mystery_S(x):# 递归出口:x=0时返回1if x == 0:return 1# x为偶数:调用S(x//2)(整数除法,避免浮点数)if x % 2 == 0:return calculate_mystery_S(x // 2)# x为奇数:调用S(x-1) + 1else:return calculate_mystery_S(x - 1) + 1# 测试样例(输入x=7)
x = 7
print(calculate_mystery_S(x))  # 输出:4# 额外测试:x=4(偶数)
print(calculate_mystery_S(4))  # 输出:1(S(4)=S(2)=S(1)=S(0)+1=2?注:需按定义重新计算:S(4)=S(2)=S(1)=S(0)+1=2,实际运行验证)

七、编写递归代码的核心技巧

文档中强调:“明白函数作用并相信它能完成这个任务,千万不要跳进这个函数里面企图探究更多细节,关注当前层的逻辑就好”,这是避免 “人肉递归” 的关键。

1. 技巧核心:“函数作用先行”

编写递归代码时,先明确 “当前函数的核心功能”,然后直接用该功能(调用自身)解决子问题,无需关心子问题内部如何执行。

示例 1:反转链表(基于文档逻辑)

  • 函数定义:reverse_linked_list(head),功能是 “反转以 head 为头节点的链表,并返回反转后的新头节点”。
  • Python 实现:
class ListNode:def __init__(self, val=0, next=None):self.val = valself.next = nextdef reverse_linked_list(head):# 递归出口:链表为空或只有一个节点,直接返回(无需反转)if head is None or head.next is None:return head# 子问题:反转head.next之后的链表,相信它能返回新头节点new_headnew_head = reverse_linked_list(head.next)# 当前层逻辑:调整head与head.next的指向(完成当前节点的反转)head.next.next = head  # 让原head的下一个节点指向自己head.next = None       # 断开原指向,避免循环# 返回反转后的新头节点return new_head# 构建测试链表:1 → 2 → 3 → 4 → 5
head = ListNode(1, ListNode(2, ListNode(3, ListNode(4, ListNode(5)))))
# 反转链表
new_head = reverse_linked_list(head)
# 遍历反转后的链表(验证结果)
current = new_head
while current:print(current.val, end=" → ")current = current.next
# 输出:5 → 4 → 3 → 2 → 1 → 

示例 2:快速幂(递归实现)快速幂的功能是 “在 O (log n) 时间内计算 \(a^b\)”,核心是拆解为 “偶数次幂平方、奇数次幂平方再乘 a”,文档中提供了 Python 实现思路。

def quick_pow(a, b):# 递归出口:任何数的0次幂为1if b == 0:return 1# 子问题:计算a^(b//2),相信它能返回结果resres = quick_pow(a, b // 2)# 当前层逻辑:根据b的奇偶性合并结果if b % 2 == 1:return res * res * a  # 奇数:多乘一次aelse:return res * res      # 偶数:直接平方# 测试:计算2^5(预期32)、3^4(预期81)
print(quick_pow(2, 5))  # 输出:32
print(quick_pow(3, 4))  # 输出:81

八、递归的注意事项(避坑指南)

递归虽简洁,但易出现重复计算栈溢出两大问题,需针对性解决。

1. 问题 1:重复计算(重叠子问题)

当多个子问题的解被反复计算时,会导致时间复杂度急剧上升(如未优化的斐波那契数列)。

(1)案例:未优化的斐波那契数列

斐波那契定义:\(F(n) = F(n-1) + F(n-2)\)(\(F(0)=0, F(1)=1\))调用树(以 F (6) 为例):\(F(6) = F(5) + F(4)\),\(F(5) = F(4) + F(3)\)…… 其中 F (4)、F (3)、F (2) 均被多次计算,时间复杂度为 O (2ⁿ)。

(2)解决办法(Python 实现)
  • 方法 1:记忆化(缓存中间结果)用列表或字典存储已计算的 F (n),下次需要时直接读取,避免重复计算。优化后时间复杂度为 O (n)。

    # 用字典缓存中间结果(键:n,值:F(n))
    fib_cache = {}def fib_memo(n):if n == 0:return 0if n == 1:return 1# 若已计算过,直接返回缓存值if n in fib_cache:return fib_cache[n]# 未计算则递归,并缓存结果fib_cache[n] = fib_memo(n-1) + fib_memo(n-2)return fib_cache[n]# 测试:计算F(10)(预期55)
    print(fib_memo(10))  # 输出:55
    
  • 方法 2:改递归为非递归(动态规划思想)用迭代方式从最小子问题(F (0)、F (1))逐步计算到 F (n),完全避免递归调用。

    def fib_iterative(n):if n == 0:return 0if n == 1:return 1# 用变量存储前两个结果(F(i-2)和F(i-1))prev_prev = 0  # F(0)prev = 1       # F(1)for i in range(2, n+1):current = prev_prev + prev  # F(i) = F(i-2) + F(i-1)prev_prev = prev            # 更新F(i-2)为F(i-1)prev = current              # 更新F(i-1)为F(i)return prev# 测试:计算F(10)(预期55)
    print(fib_iterative(10))  # 输出:55
    

2. 问题 2:栈溢出

递归依赖 “函数调用栈” 实现,每次调用函数会在栈中添加一个 “栈帧”。若递归层数过多(如 n=10000),栈会超出最大容量,引发栈溢出错误。

(1)案例:深层递归导致栈溢出
# 当n=1000时,可能引发栈溢出(Python默认递归深度约1000)
def deep_recursion(n):if n == 1:return 1return deep_recursion(n - 1) + 1# 测试:n=1000(可能报错:RecursionError: maximum recursion depth exceeded)
# print(deep_recursion(1000))
(2)解决办法
  • 方法 1:手动调整递归深度(谨慎使用)通过sys.setrecursionlimit()扩大递归深度,但不推荐(可能导致内存问题):

    import sys
    # 调整递归深度为2000(仅临时测试用)
    sys.setrecursionlimit(2000)
    def deep_recursion(n):if n == 1:return 1return deep_recursion(n - 1) + 1
    # 测试:n=1500(大概率可运行)
    print(deep_recursion(1500))  # 输出:1500
    
  • 方法 2:改递归为非递归(推荐)用循环模拟递归过程,彻底避免栈溢出。例如将上述 “深层递归求和” 改为迭代:

    def iterative_sum(n):result = 0for i in range(1, n+1):result += 1return result# 测试:n=10000(无栈溢出风险)
    print(iterative_sum(10000))  # 输出:10000
    

3. 拓展:递归的复杂度分析

  • 时间复杂度:取决于 “递归调用次数” 和 “每一层的操作复杂度”。例如优化后的斐波那契(记忆化),调用次数 O (n),每一层操作 O (1),总时间 O (n)。
  • 空间复杂度:取决于 “递归深度” 和 “每一层的空间开销”。例如递归实现的斐波那契,深度 O (n),每一层无额外空间,总空间 O (n);非递归实现空间 O (1)。

九、递归的优势

  1. 代码结构清晰,可读性强相比复杂的迭代逻辑,递归能直接映射问题的数学定义,代码更简洁。例如斐波那契的递归代码(3 行核心逻辑)vs 迭代代码(循环 + 变量维护),递归更易理解。

  2. 培养问题拆解能力递归要求开发者将原问题拆解为同类子问题,强制锻炼 “抽象思维” 和 “分治思想”,为后续学习分治、动态规划等算法奠定基础。

十、递归核心小结(基于文档)

  1. 本质:函数调用自身,通过 “递(拆解子问题)” 和 “归(回溯结果)” 解决问题,需包含 “可重复子问题” 和 “递归出口”。
  2. 代码模板:基础模板(出口 + 调用自身)、完整模板(出口 + 当前层处理 + 下一层 + 善后),优先用 Python 实现。
  3. 编写技巧:明确函数作用,不人肉递归,仅关注当前层逻辑。
  4. 避坑重点:用记忆化避免重复计算,用非递归避免栈溢出。
  5. 优势:代码简洁、可读性强,助力培养问题拆解能力。
http://www.dtcms.com/a/432246.html

相关文章:

  • 杭州门户网站建设工信部网站备案怎么查询
  • 多线程环境下虚函数性能评估与优化指南
  • 高端网站设计欣赏视频门户网站建设服务器
  • 实用的LoRaWAN 应用层协议规范
  • 阿里云 建网站攻略做网站一屏一屏的
  • 沈阳营销型网站开发网站的流量怎么赚钱
  • 网站开发属于什么费用wordpress 文章循环
  • 音视频编解码全流程之用Extractor后Decodec
  • 03.动画眼睛跟随鼠标光标 | JavaScript 鼠标移动事件
  • 瑞安学校网站建设哈尔滨网站优化技术
  • 南和邢台网站制作色彩设计网站
  • tauri2使用fs的watch报错fs:watch “Command watch not found“
  • 国外优秀vi设计网站eclipse网站建设
  • 扬中网站优化哪家好服务器2003怎么做网站
  • 深圳建站公司服务宁乡网页设计
  • 营销型网站模板广告代理商是什么意思
  • 网站建设需要几步让网站降权
  • 如何优化企业网站游戏网站创建
  • 单页网站seo怎么做想学设计没有基础怎么办
  • 泛解析对网站的影响网站建设的培训心得
  • 做企业网站的缺点英文外贸网站制作
  • 付网站建设服务费的会计分录深圳做推广哪家比较好
  • 莆系医疗网站建设做网站小代码大全
  • 23.CSS 图片悬停效果
  • 潍坊有哪些网站知名网站建设托管
  • 普通企业网站营销内链好的网站
  • 网站建设最新外文翻译网页传奇哪个最火
  • 外贸型网站建设方法企业信用报告如何获取
  • 网站建设小结wordpress 多地址插件
  • 报名网站建设费用报价做网站和做app哪个简单