理解递归函数之返回机制、与循环的对应关系、n递归与n叉树
yuyutoo 2024-12-17 17:24 17 浏览 0 评论
代码顺序存储,逐条执行。
代码的控制结构(if, while, for, continue, break, return),可以进行跳转。
函数调用机制也是一种控制结构机制,因为每个函数有一条显式或隐式的return语句。
1 函数的调用与return机制
函数都有一条或多条显式的return语句,或一条隐式的return语句(void函数,执行到函数体最后一句时,会隐式执行一条return语句,对应汇编的ret指令)。
函数调用,也就是主调函数(caller)调用被调函数(callee),调用时,代码跳转,离开主调函数caller,执行callee函数体,遇到return语句或执行完函数体后,return回caller的调用点,有返回值的返回给caller中的变量。
callee return回caller调用点就是跳转,能准确跳回是因为存储了一个调用点的地址。这是编译器背后的支持,通过一个栈机制保存了调用点地址(每次调用都会在栈空间内开辟一个栈帧空间给函数)。当有嵌套调用时,能逐级调用,逐级返回。怎样逐级返回?就是先返回到最近存储的调用点,也就是后进先出。如同你从A点出发,经过n个路口,每经过一个路口,用一张扑克牌记录好需要返回的路口,每过一个路口,记录一张牌,放在前面一张牌的上面,最后要返回时,读取最上面的牌上的返回地址,依次返回。这些牌依次叠起来,依次从上面拿牌,十分类似函数嵌套调用时的栈帧机制。
2 递归函数的参数迭代与循环的对应关系
递归函数就是自己调用自己。
同样的代码按约定条件重复执行n次,完全可行。这样一个洋葱,层层相套,代码量一样,执行代码的顺序和代码量可能不一样,问题规律可能逐层递减,参数可能不断迭代变化。
写代码时,对有重复利用价值的功能代码模块可以抽象出函数(包括抽象出函数参数和返回值)。在调用点调用时,可以想象为适当处理好函数参数和返回值后,将函数体扩充到该位置。递归调用也是如此,就是将自己的函数体扩充到自己的调用点(可能也有参数迭代)。
嵌套调用可以想象为嵌套膨胀,或嵌套套容器。
如果递归调用了几次,可以理解为将代码重复次。
另外要注意代码的执行顺序,通常按先后顺序,以调用点(也是返回点)为基准,分为三部分:
a 调用点前的代码,在递推阶段执行,通常有一个if语句,也有可能包含一个return语句,提前return。
b 调用点处的调用代码,是调用点(跳转点),也是回归点(return回)。
c 调用点后的代码,在回归阶段执行,直到return语句或函数体最后一条语句,如果是尾递归,则没有这一部分。如果不是尾递归,历史的局部变量与实参值保存在返回的栈桢上(如同上述记录的扑克牌),如果递归函数用循环同等实现时,是需要一个额外的栈数据结构来辅助保存这些历史的实参与局部变量。
有一点微妙之处在于参数的迭代及与循环的对应关系。
为什么迭代函数内只有if语句,却可以构成循环?理解一下递归函数,同行功能实现的循环实现及对应的goto实现就知道了。
int factRecur(int n){ // 阶乘递归版
if(n<=1)
return 1;
return n*factRecur(n-1); // 递归调用时,参数迭代:n = n-1
}
int factLoop(int n){ // 阶乘循环版
int sum = 1;
while(n>1)
sum *= n--;
return sum;
}
int factGoto(int n){ // 阶乘goto版
int sum = 1;
loop:
if(n<=1)
goto end;
sum *= n--;
goto loop;
end:
return sum;
}
3 单递归
单递归,一个函数只有1次调用自己。求阶乘就是典型的单递归。单递归如果是尾递归时,用循环替代时,比较简单,不需要栈数据结构辅助。如果不是尾递归,需要栈数据结构来辅助记录函数参数(如果有)和局部变量。
单递归是一种线性的call和return。
4 双递归
双递归,一个函数在函数体内有2次调用自己的语句。汉诺塔和斐波那契数列都是典型的双递归。
二递归对应一棵二叉树,递推和回归(return)的顺序,就是二叉树的深度优先遍历。
二叉树深度优先遍历,一个节点的代码三次进入,三次return回:
非叶节点:3次递推进入和3次return回的机会(叶子节点只有一次机会,遇到基准条件即返回)。
void midOrder(struct treeNode* tree) // void函数默认一个return
{
if(tree!=NULL) // 非空时递归,空时return回
{
// printData(tree); // 写在前面就是前序遍历,第1次进入时操作
midOrder(tree->LChild); // 叉1 参数迭代,回溯时往下走
printData(tree); // 写在中间就是中序遍历,第2次进入时操作
midOrder(tree->RChild); // 叉2
// printData(tree); // 写在后面就是后序遍历,第3次进入时操作
}
}
可以理解为代码的重复:
总结一下:递归函数调用自己2次,二叉,加上自己,代码要重复执行3次,三次被call,三次return,形成3个调用和回归点。
5 三递归
三递归,一个函数3次调用自己。
示例代码:
#include <stdio.h>
void r(int n)
{
if(n<=0)
//printf("\n__%d__\n",n);
return;
r(n-3);
r(n-2);
r(n-1);
printf("%d ",n);
}
int main()
{
r(5);
return 0;
}
形成三叉树的调用关系:
后序遍历,及4次进入的机会。
如r(1),
第1次:调用r(-2),再return到r(1);
第2次:调用r(-1),再return到r(1);
第2次:调用r(0),再return到r(1);
完整的三叉树:
三叉树深度优先遍历,一个节点的代码四次进入,四次return回:
6 更多次递归函数
如分书问题:
#include <iostream>
using namespace std;
#define NUMS 5
int like[NUMS][NUMS]={ // like[i][j] = 1 表示第i人喜欢第j本书
{0,0,1,1,0},
{1,1,0,0,1},
{0,1,1,0,1},
{1,0,0,1,0},
{0,1,0,0,1}};
int take[NUMS]={0,0,0,0,0}; // 记录每一本书的分配情况
int n = 0; // n表示分书方案数
void trynext(int i);
int main()
{
trynext(0); // 书(列)1-5,人1-5 ,A-E
cin.get();
return 0;
}
void trynext(int j) // 对第 j 本书进行分配
{
for(int i=0;i<NUMS;i++) // 对每个人进行穷举
if(like[i][j]&&take[i]==0)
{
take[i]=j+1; // 把第i本书分配给第j个人
if(j==NUMS-1) // 第NUMS本书分配结束,也即所有的人已经分配完毕,
{ // 可以将方案进行输出
n++;
cout<<"分配方案"<<n<<":"<<endl;
for(int k=0;k<NUMS;k++)
cout<<"人"<<k<<"→"<<"分到第"<<take[k]<<"本书"<<endl;
cout<<endl;
}
else{
cout<<j+1<<" "; // 调用时的参数记录,辅助理解形成了几次调用关系
trynext(j+1); // 递归,对下一个人进行分配
}
take[i]=0; // 回溯,寻找下一种方案
}
}
/*
1 2 3 4 分配方案1:
人0→分到第3本书
人1→分到第1本书
人2→分到第2本书
人3→分到第4本书
人4→分到第5本书
2 3 4 分配方案2:
人0→分到第3本书
人1→分到第1本书
人2→分到第5本书
人3→分到第4本书
人4→分到第2本书
3 4 4 1 2 3 3 4 分配方案3:
人0→分到第4本书
人1→分到第2本书
人2→分到第3本书
人3→分到第1本书
人4→分到第5本书
2 3 2 3 3 4 分配方案4:
人0→分到第4本书
人1→分到第5本书
人2→分到第3本书
人3→分到第1本书
人4→分到第2本书
*/
如果是5个人5本书,因为有回溯的情况,一个分配方案的递归函数,可能多于或小于4递归。
-End-
相关推荐
- 电脑 CMD 命令大全:简单粗暴收藏版
-
电脑CMD命令大全包括了许多常用的命令,这些命令可以帮助用户进行各种系统管理和操作任务。以下是一些常用的CMD命令及其功能:1、系统信息和管理...
- 电脑维修高手必备!8个神奇DOS命令,自己动手不求人
-
我相信搞电脑维修或者维护的基本都会些DOS的命令。就算Windows操作系统是可视化的界面,但很多维护检查是离不开DOS命令的。掌握好这些命令,你不仅能快速诊断问题,还能解决90%的常见电脑故障。下...
- 一个互联网产品总监的设计技巧总结 - 技术篇
-
古语:工欲善其事必先利其器。往往在利其器后我们才能事半功倍。从这个角度出发成为一个合格的产品经理你需要的是“利其器”,这样你才能产品的设计过程中如鱼得水,得心应手。有些产品经理刚入职,什么都感觉自己欠...
- 超详解析Flutter渲染引擎|业务想创新,不了解底层原理怎么行?
-
作者|万红波(远湖)出品|阿里巴巴新零售淘系技术部前言Flutter作为一个跨平台的应用框架,诞生之后,就被高度关注。它通过自绘UI,解决了之前RN和weex方案难以解决的多端一致性...
- 瑞芯微RK3568|SDK开发之环境安装及编译操作
-
1.SDK简介一个通用LinuxSDK工程目录包含有buildroot、app、kernel、device、docs、external等目录。其中一些特性芯片如RK3308/RV1108/R...
- 且看L-MEM ECC如何守护i.MXRT1170从核CM4
-
大家好,我是痞子衡,是正经搞技术的痞子。今天痞子衡给大家分享的是恩智浦i.MXRT1170上Cortex-M4内核的L-MEMECC功能。本篇是《简析i.MXRT1170Cortex-M7F...
- ECC给i.MXRT1170 FlexRAM带来了哪些变化?
-
大家好,我是痞子衡,是正经搞技术的痞子。今天痞子衡给大家分享的是恩智浦i.MXRT1170上Cortex-M7内核的FlexRAMECC功能。ECC是“ErrorCorrectingCode”...
- PHP防火墙代码,防火墙,网站防火墙,WAF防火墙,PHP防火墙大全
-
PHP防火墙代码,防火墙,网站防火墙,WAF防火墙,PHP防火墙大全资源宝整理分享:https://www.htple.net...
- 从零开始移植最新版本(2023.10)主线Uboot到Orange Pi 3(全志H6)
-
本文将从零开始通过一步一步操作来实现将主线U-Boot最新代码移植到OrangePi3(全志H6)开发板上并正常运行起来。本文从通用移植思路的角度,展现是思考的过程,通过这种方式希望能让读者一通百...
- 可视化编程工具Blockly——定制工具箱
-
1概述本文重点讲解如何定制Blocklytoolbox上,主要包含如下几点目标:如何为toolbox不同类别添加背景色如何改变选中的类别的外观如何为toolbox类别添加定制化的css如何改变类别...
- 用户界面干货盘点(用户界面的基本操作方法)
-
DevExpressDevExpressWPF的DXSplashScreen控件在应用加载的时候显示一个启动界面。添加DXSplashScreen后,会默认生成一个XAML文件,当然,你也可...
- Vue3+Bootstrap5整合:企业级后台管理系统实战
-
简洁而不简单,优雅而不失强大在当今快速发展的企业数字化进程中,高效、美观的后台管理系统已成为企业运营的核心支撑。作为前端开发者,我们如何选择技术栈,才能既保证开发效率,又能打造出专业级的用户体验?答案...
- 什么?这三款i.MXRT型号也开放了IAP API?
-
大家好,我是痞子衡,是正经搞技术的痞子。今天痞子衡给大家介绍的是i.MXRT1050/1020/1015系列ROM中的FlexSPI驱动API使用。今天痞子衡去4S店给爱车做保养了,...
- OneCode基础组件介绍——表格组件(Grid)
-
在企业级应用开发中,表格组件是数据展示与交互的核心载体。OneCode平台自研的Grid表格组件,以模型驱动设计...
- 开源无线LoRa传感器(光照温湿度甲醛Tvoc)
-
本开源项目基于ShineBlinkC2M低代码单片机实现,无需复杂单片机C语言开发。即使新手也可很容易用FlexLua零门槛开发各种功能丰富稳定可靠的IoT硬件,更多学习教程可参考Flex...
你 发表评论:
欢迎- 一周热门
- 最近发表
- 标签列表
-
- mybatis plus (70)
- scheduledtask (71)
- css滚动条 (60)
- java学生成绩管理系统 (59)
- 结构体数组 (69)
- databasemetadata (64)
- javastatic (68)
- jsp实用教程 (53)
- fontawesome (57)
- widget开发 (57)
- vb net教程 (62)
- hibernate 教程 (63)
- case语句 (57)
- svn连接 (74)
- directoryindex (69)
- session timeout (58)
- textbox换行 (67)
- extension_dir (64)
- linearlayout (58)
- vba高级教程 (75)
- iframe用法 (58)
- sqlparameter (59)
- trim函数 (59)
- flex布局 (63)
- contextloaderlistener (56)