数据结构:递归的种类(Types of Recursion)

article/2025/6/13 23:05:45

目录

尾递归(Tail Recursion)

什么是 Loop(循环)?

 复杂度分析

 头递归(Head Recursion)

 树形递归(Tree Recursion)

线性递归(Linear Recursion)

递归调用树(fun(3))

栈帧变化

复杂度分析

 间接递归(Indirect Recursion)

复杂度分析 

嵌套递归(Nested Recursion)

递归调用树 


尾递归(Tail Recursion)

尾递归是递归函数的一种特殊形式:

一个函数在递归调用之后,不再做任何额外的操作,而是“直接返回”这个递归调用的结果。

也就是说,递归调用是函数中的最后一步,没有其他计算或操作跟在它后面。

void fun(int x)
{if(x > 0){printf("%d", x);fun(x - 1);}
}

分析:

  • printf("%d", x); 是当前函数的最后一个非递归操作。

  • fun(x - 1); 是函数体中最后一个执行的动作,它后面没有其它操作。

  • 调用的返回值也没有被用在任何表达式中,比如赋值、加法等。

 ✅所以这是尾递归。

在尾递归中,当前函数的调用栈帧不需要保存,因为没有额外操作依赖返回值,编译器甚至可以优化为循环(loop),提高效率。 

void fun(int n)
{if(n > 0){printf("%d", n);fun(n - 1) + n;}
}

分析:

  • 这里虽然 fun(n - 1) 是递归调用,但它不是函数体中的最后操作。

  • 还有一个 + n —— 表示你需要获取 fun(n - 1) 的返回值后,再与 n 相加。

  • 这意味着 当前栈帧必须保留,等待子调用返回后进行加法操作。

❌ 所以这不是尾递归。

什么是 Loop(循环)?

Loop(循环) 是程序中一种重复执行代码的结构。
常见的 C++ 循环有:

  • for 循环:已知循环次数

  • while 循环:满足条件就循环

  • do...while:至少执行一次

将递归转化为 循环

void fun_loop(int x)
{while(x > 0){printf("%d", x);x--;}
}

转化步骤说明:

  1. x 看作循环变量;

  2. fun(x - 1);x--;

  3. if (x > 0) → 变成 while(x > 0);

  4. 保持 printf 逻辑不变;

🎯 核心联系:

  • 所有尾递归都可以被转换为循环(Loop);

  • 但非尾递归不一定能直接转换为 Loop(可能需要用栈模拟)。

 复杂度分析

时间复杂度分析

无论是递归版本还是循环版本,printf 一次就是常数时间 O(1),每次都打印一个数字。

递归版本:

  • x = n 时,会调用 fun(n), fun(n-1), ..., fun(1),共 n 次。

  • 每次调用只做一次 printf,没有重复计算。

✅ 时间复杂度:O(n)

 循环版本:

同样执行 nprintf,逻辑完全等价于递归。

✅ 时间复杂度:O(n)

空间复杂度分析

递归版本:

  • 每一次函数调用,都会占用一层调用栈帧。

  • 所以 fun(5) 会占用 5 层栈空间。

  • 栈大小是受语言/平台限制的,过多递归可能导致 栈溢出(stack overflow)。

❌ 空间复杂度:O(n)(用于递归栈)

循环版本:

  • 不存在函数嵌套调用,变量 x 是一个普通变量。

  • 不会占用额外栈帧。

✅ 空间复杂度:O(1)(常量空间)


 头递归(Head Recursion)

头递归(Head Recursion) 是指:

函数中 先调用自己,再执行其他操作 的递归形式。

💡 特点:

  • 函数体中,递归调用在最前面。

  • 所有的动作都发生在“返回之后”。

  • 结构上与“尾递归”相反,先深入到底,再回溯处理”。

void fun(int n)
{if(n > 0){fun(n - 1);printf("%d", n);}
}

调用流程:

fun(3)
└── fun(2)└── fun(1)└── fun(0) → 终止

然后开始回溯:

  • 尾递归:打印从 3 → 2 → 1(调用时就执行)

  • 头递归:打印从 1 → 2 → 3(回溯时执行)

将头递归转换为循环 

void fun(int n)
{int i = 1;while(i<=n){printf("%d",i);i++;}
}

 树形递归(Tree Recursion)

树形递归是指函数在每次调用时,会递归地调用自己 两次或更多次,从而形成一个“树状”结构的调用图。

📌 特征:

  • 每个递归分支会继续递归产生更多子分支。

  • 递归调用数量呈指数级增长。

  • 典型例子是斐波那契数列、全排列、子集生成、分治算法等。

线性递归(Linear Recursion)

线性递归是指函数在每次调用时,只进行一次递归调用,然后再进行其他操作。

📌 特征:

  • 递归过程是一条“直线”,只有一条路径向下。

  • 每一层只调用下一层一次。

  • 常见于计数、求和、遍历等线性结构。

void fun(int n)
{if(n > 0){printf("%d", n);fun(n - 1);fun(n - 1);}
}

递归调用树(fun(3))

                          fun(3)/      \printf(3)         printf(3)fun(2)                 fun(2)/       \             /       \printf(2)       printf(2)  printf(2)   printf(2)fun(1)      fun(1)     fun(1)      fun(1)/     \     /     \    /     \     /     \printf(1) printf(1) ...   ...  ...   ...  ...   ...fun(0) fun(0)   fun(0) fun(0) fun(0) fun(0) fun(0) fun(0)
                                  fun(3)/        \fun(2)          fun(2)/     \          /     \fun(1)   fun(1)   fun(1)   fun(1)/   \     /   \    /   \    /   \fun(0) fun(0) fun(0) fun(0) fun(0) fun(0) fun(0) fun(0)每个 fun(n) 节点都对应一次:printf(n)

栈帧变化

我们使用“压栈(调用)”和“弹栈(返回)”来标记栈帧变化。

⬇️ 调用开始:main()fun(3)

+------------------------+
| fun(n=3) 的栈帧        |
+------------------------+
| main() 的栈帧          |
+------------------------+

打印:3

⬇️ fun(3) 调用 fun(2)

+------------------------+
| fun(n=2) 的栈帧        |
+------------------------+
| fun(n=3) 的栈帧        |
+------------------------+
| main() 的栈帧          |
+------------------------+

打印:2

⬇️ fun(2) 调用 fun(1)

+------------------------+
| fun(n=1) 的栈帧        |
+------------------------+
| fun(n=2) 的栈帧        |
+------------------------+
| fun(n=3) 的栈帧        |
+------------------------+
| main() 的栈帧          |
+------------------------+

打印:1

⬇️ fun(1) 调用 fun(0)

+------------------------+
| fun(n=0) 的栈帧        |
+------------------------+
| fun(n=1) 的栈帧        |
+------------------------+
| fun(n=2) 的栈帧        |
+------------------------+
| fun(n=3) 的栈帧        |
+------------------------+
| main() 的栈帧          |
+------------------------+

n == 0,返回,弹出 fun(0) 栈帧:

+------------------------+
| fun(n=1) 的栈帧        |
+------------------------+
| fun(n=2) 的栈帧        |
+------------------------+
| fun(n=3) 的栈帧        |
+------------------------+
| main() 的栈帧          |
+------------------------+

⬇️ fun(1) 再调用第二个 fun(0)

+------------------------+
| fun(n=0) 的栈帧        |
+------------------------+
| fun(n=1) 的栈帧        |
+------------------------+
| fun(n=2) 的栈帧        |
+------------------------+
| fun(n=3) 的栈帧        |
+------------------------+
| main() 的栈帧          |
+------------------------+

返回,弹出 fun(0):

+------------------------+
| fun(n=1) 的栈帧        |
+------------------------+
| fun(n=2) 的栈帧        |
+------------------------+
| fun(n=3) 的栈帧        |
+------------------------+
| main() 的栈帧          |
+------------------------+

fun(1) 完成,弹出:

+------------------------+
| fun(n=2) 的栈帧        |
+------------------------+
| fun(n=3) 的栈帧        |
+------------------------+
| main() 的栈帧          |
+------------------------+

⬇️ fun(2) 现在调用第二个 fun(1)(结构一样)

⬇️ fun(3) 现在调用第二个 fun(2)

调用 fun(2) → fun(1) → fun(0) → fun(0) → 返回

+------------------------+
| fun(n=2) 的栈帧        |
+------------------------+
| fun(n=3) 的栈帧        |
+------------------------+
| main() 的栈帧          |
+------------------------+

 进入 fun(1),打印 1,两次 fun(0),弹栈

 再次 fun(1),打印 1,两次 fun(0),弹栈

 完成所有调用,弹出所有栈帧。

最终输出顺序:3 2 1 1 2 1 1

复杂度分析

时间复杂度分析:递归调用树逐层计数 

调用树节点数量(按层)

层数(Level)调用的是 fun(x)节点个数
第 0 层fun(3)1
第 1 层fun(2)2
第 2 层fun(1)4
第 3 层fun(0)8(终止)

 总调用次数 = 每个 fun(n) 的数量 = 1 + 2 + 4 + 8 = 15

这是一个 等比数列求和:

n = 3 时:

T(3)=2^4−1=15

当 n 变大时,忽略常数与低阶项:时间复杂度:T(n) = O(2^n) 

 空间复杂度分析:根据栈深度(调用路径)分析

空间复杂度 = 递归过程中 最大栈帧深度

一个分支调用路径是:

fun(3)→ fun(2)→ fun(1)→ fun(0)
  • 每次递归调用先执行第一个子调用,然后第二个

  • 每个调用都等第二个调用结束后才能返回

  • 所以是深度优先的栈式调用

最大栈帧数 = 调用最长的一条路径

+------------------------+
| fun(n=0) 的栈帧        |
+------------------------+
| fun(n=1) 的栈帧        |
+------------------------+
| fun(n=2) 的栈帧        |
+------------------------+
| fun(n=3) 的栈帧        |
+------------------------+
| main() 的栈帧          |
+------------------------+

共计 4 层栈帧 

空间复杂度结论:

空间复杂度 = 最大栈帧数 = O(n)

(这里 n 是初始的 fun(n) 的值)


 间接递归(Indirect Recursion)

间接递归是指:函数 A 调用函数 B,函数 B 又调用函数 A,形成“循环调用”结构,但自己不直接调用自己。

📌 特征:

  • A → B → A → B → … 互相递归

  • 没有函数直接调用自己,但仍然形成递归链

  • 本质上仍然需要使用函数栈,类似直接递归的行为

void funA(int n)
{if(n > 0){printf("%d", n);funB(n - 1);}
}void funB(int x)
{if(x > 0){printf("%d", x);funA(x / 2);}
}

递归调用树

                             funA(3)/     \printf(3)       funB(2)/      \printf(2)        funA(1)/     \printf(1)        funB(0)

复杂度分析 

时间复杂度

是否能泛化为 n 的函数?

我们发现:

  • 每次 funA(n) → funB(n-1)

  • 每次 funB(x) → funA(x/2)

所以这是一个交错型链式调用。

建函数调用深度模型(从 n=3 推到 n)

设:

  • A 调用 B(n-1)

  • B 调用 A(n/2)

构成链状结构:

A(n) → B(n-1) → A((n-1)/2) → B((n-1)/2 - 1) → ...
n → n-1 → ⌊(n-1)/2⌋ → ⌊(n-1)/2⌋ - 1 → ...

这是一个交替递减+对数递减的组合过程。

下降几次会结束?

这是一个类似:

n→n−1→⌊2n−1​⌋→⌊2n−1​⌋−1→...

混合了 线性减1 和 对数除2 的过程

最多不会超过:

  • 线性下降:O(n)

  • 对数下降:O(log n)

 所以 时间复杂度:O(log n + n) = O(n)(近似线性) 

✅空间复杂度(最大栈帧)

因为调用是链状交替(A→B→A→B...),所以 最大调用深度 就是这条链的长度。

最多有 O(n) 次连续调用 → 最大栈深度为 O(n)


嵌套递归(Nested Recursion)

嵌套递归是指:递归调用的参数本身又是一个递归调用。

也就是:

fun(fun(n + 11))

它的核心区别是:

  • 递归调用不是 fun(n - 1) 这种简单线性变化;

  • 而是 “求出 fun(n+11) 的结果,然后再用它作为参数再次调用 fun”。

int fun(int n)
{if(n > 100){return n - 10;}else{return fun(fun(n + 11));}
}

递归调用树 

                                  fun(95)/      \fun(106) = 96    fun(96)/      \fun(107) = 97     fun(97)/      \fun(108) = 98     fun(98)/      \fun(109) = 99   fun(99)
                                                         fun(99)/       \fun(110) = 100       fun(100)/     \fun(111) = 101     fun(101)↓base case

最终回溯后 fun(95) 会返回 91


http://www.hkcw.cn/article/auMOYdoucO.shtml

相关文章

Redis底层数据结构之字典(Dict)

Dict基本结构 Dict我们可以想象成目录&#xff0c;要翻看什么内容&#xff0c;直接通过目录能找到页数&#xff0c;翻过去看。如果没有目录&#xff0c;我们需要一页一页往后翻&#xff0c;这样时间复杂度就与遍历的O(n)一样了&#xff0c;而用了Dict我们就可以在O(1)的时间复杂…

高通SoC阵列服务器

高通SoC阵列服务器是基于高通系统级芯片&#xff08;SoC&#xff09;构建的高密度计算解决方案&#xff0c;核心特点为低功耗、高算力集成与模块化设计&#xff0c;主要应用于边缘计算和云服务场景。以下是其技术特性和应用方向的综合分析&#xff1a; 一、核心技术特性 架构…

Linux系统下Google浏览器无法使用中文输入的临时解决方案

文章目录 前言方案描述Edge如何兼容 前言 这个AlamaLinux的ibus-libpinyin确实是让人琢磨不透&#xff0c;就只是无法在Chrome浏览器中、Edge浏览器中使用&#xff0c;而在VSCode、Xfce4-Terminal中使用良好。尝试了很多办法都没有效果&#xff0c;最后在Reddit里面找到了相应…

【AI论文】空间多模态大型语言模型(Spatial-MLLM):增强基于视觉的空间智能中多模态大型语言模型(MLLM)的能力

摘要&#xff1a;多模态大语言模型&#xff08;MLLM&#xff09;的最新进展显著提高了2D视觉任务的性能。 然而&#xff0c;提高他们的空间智能仍然是一个挑战。 现有的3D MLLM总是依赖于额外的3D或2.5D数据来加入空间感知&#xff0c;限制了它们在只有2D输入&#xff08;如图像…

黑马Java面试笔记之 微服务篇(业务)

一. 限流 你们项目中有没有做过限流?怎么做的? 为什么要限流呢? 一是并发的确大(突发流量) 二是防止用户恶意刷接口 限流的实现方式: Tomcat:可以设置最大连接数 可以通过maxThreads设置最大Tomcat连接数,实现限流,但是适用于单体架构 Nginx:漏桶算法网关,令牌桶算法自定…

AWS App Mesh实战:构建可观测、安全的微服务通信解决方案

摘要&#xff1a;本文详解如何利用AWS App Mesh统一管理微服务间通信&#xff0c;实现精细化流量控制、端到端可观测性与安全通信&#xff0c;提升云原生应用稳定性。 一、什么是AWS App Mesh&#xff1f; AWS App Mesh 是一种服务网格&#xff08;Service Mesh&#xff09;解…

《云原生安全攻防》-- K8s网络策略:通过NetworkPolicy实现微隔离

默认情况下&#xff0c;K8s集群的网络是没有任何限制的&#xff0c;所有的Pod之间都可以相互访问。这就意味着&#xff0c;一旦攻击者入侵了某个Pod&#xff0c;就能够访问到集群中任意Pod&#xff0c;存在比较大的安全风险。 在本节课程中&#xff0c;我们将详细介绍如何通过N…

吃透 Golang 基础:数据结构之 Map

文章目录 Map概述初始化删除访问不存在的 key 返回 value 的零值遍历 mapmap 自身的零值map 索引时返回的第二个参数使用 map 实现 set Map Hash Map 是无序的 key/value 对集合&#xff0c;其中所有的 key 都是不同的。通过给定的 key 可以在常数时间复杂度内完成检索、更新或…

手机邮箱APP操作

收发电子邮件方式 邮箱可以在网络段登录&#xff0c;也可以在手机端登录。 大学网络服务 收发电子邮件有三种方式&#xff1a; 1、Web方式&#xff1a; 1&#xff09;登录“网络服务”&#xff08;https://its.pku.edu.cn&#xff09;&#xff0c;点页面顶端“邮箱”。 2&…

Spring AI之RAG入门

目录 1. 什么是RAG 2. RAG典型应用场景 3. RAG核心流程 3.1. 检索阶段 3.2. 生成阶段 4. 使用Spring AI实现RAG 4.1. 创建项目 4.2. 配置application.yml 4.3. 安装ElasticSearch和Kibana 4.3.1. 安装并启动ElasticSearch 4.3.2. 验证ElasticSearch是否启动成功 …

Spring AI Alibaba + Nacos 动态 MCP Server 代理方案

作者&#xff1a;刘宏宇&#xff0c;Spring AI Alibaba Contributor 文章概览 Spring AI Alibaba MCP 可基于 Nacos 提供的 MCP server registry 信息&#xff0c;建立一个中间代理层 Java 应用&#xff0c;将 Nacos 中注册的服务信息转换成 MCP 协议的服务器信息&#xff0c…

19-项目部署(Linux)

Linux是一套免费使用和自由传播的操作系统。说到操作系统&#xff0c;大家比较熟知的应该就是Windows和MacOS操作系统&#xff0c;我们今天所学习的Linux也是一款操作系统。 我们作为javaEE开发工程师&#xff0c;将来在企业中开发时会涉及到很多的数据库、中间件等技术&#…

2025.6.3学习日记 Nginx 基本概念 配置 指令 文件

1.初始nginx Nginx&#xff08;发音为 “engine x”&#xff09;是一款高性能的开源 Web 服务器软件&#xff0c;同时也具备反向代理、负载均衡、邮件代理等功能。它由俄罗斯工程师 Igor Sysoev 开发&#xff0c;最初用于解决高并发场景下的性能问题&#xff0c;因其轻量级、高…

SpringCloud——Nacos注册中心、OpenFeign

一、Nacos注册中心 1.注册中心原理 2.服务注册 添加依赖&#xff1a; <!--nacos 服务注册发现--> <dependency><groupId>com.alibaba.cloud</groupId><artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId> </dep…

WAF绕过,网络层面后门分析,Windows/linux/数据库提权实验

一、WAF绕过文件上传漏洞 win7&#xff1a;10.0.0.168 思路&#xff1a;要想要绕过WAF&#xff0c;第一步是要根据上传的内容找出来被拦截的原因。对于文件上传有三个可以考虑的点&#xff1a;文件后缀名&#xff0c;文件内容&#xff0c;文件类型。 第二步是根据找出来的拦截原…

榕壹云健身预约系统:多门店管理的数字化解决方案(ThinkPHP+MySQL+UniApp实现)

随着全民健身热潮的兴起,传统健身房在会员管理、课程预约、多门店运营等方面面临诸多挑战。针对这一需求,我们开发了一款基于ThinkPHP+MySQL+UniApp的榕壹云健身预约系统,为中小型健身机构及连锁品牌提供高效、灵活的数字化管理工具。本文将详细介绍系统的技术架构、核心功能…

nginx去掉暴漏外边的版本号

背景 在做安全扫描的时候&#xff0c;对方说nginx会暴漏版本号属于中危漏洞 解决方法 nginx 在http{括号里增加 server_tokens off; # 关闭版本号显示# add_header Server "Apache/2.4.3"; # 伪造为 Apache 服务器&#xff08;可选&#xff09;效果

飞牛fnNAS存储模式RAID 5数据恢复

目录 一、添加硬盘 二、创建RAID 5 存储空间 三、上传测试文件 四、拆除硬盘 五、更换硬盘 六、修复RAID 5 七、验证其内文件 八、NAS系统崩溃后的数据盘 前文《飞牛fnNAS存储空间模式详解》 中介绍了fnNAS存储空间的几个模式,细心的网友应该能感受到,我是非常推崇R…

OpenEMMA: 打破Waymo闭源,首个开源端到端多模态模型

1. 概述 OpenEMMA&#xff08;Open-source End-to-end Multimodal Model for Autonomous driving&#xff09;是由德州农工大学、密歇根大学和多伦多大学联合推出的开源端到端自动驾驶多模态模型框架&#xff0c;旨在复现并开源 Waymo 旗下 EMMA 系统的核心思路与方法。 该框…

学习STC51单片机26(芯片为STC89C52RCRC)

每日一言 真正的强者&#xff0c;不是没有眼泪&#xff0c;而是含着泪依然奔跑。 硬件&#xff1a;4G模块 这个是接线原理&#xff0c;我们也只要知道这个4根线的连接就好了&#xff0c;我们也是连接到USB转TTL的模块上 要插卡哈......... 随后我们下载一个叫做亿佰特的调试助…