百度360必应搜狗淘宝本站头条
当前位置:网站首页 > 编程网 > 正文

理解递归函数之返回机制、与循环的对应关系、n递归与n叉树

yuyutoo 2024-12-17 17:24 14 浏览 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-

相关推荐

TCP协议原理,有这一篇就够了

先亮出这篇文章的思维导图:TCP作为传输层的协议,是一个软件工程师素养的体现,也是面试中经常被问到的知识点。在此,我将TCP核心的一些问题梳理了一下,希望能帮到各位。001.能不能说一说TC...

Win10专业版无线网络老是掉线的问题

有一位电脑基地的用户,使用...

学习计算机网络需要掌握以下几方面基础知识

计算机基础知识操作系统:了解常见操作系统(如Windows、Linux)的基本操作和网络配置,例如如何设置IP地址、子网掩码、网关和DNS服务器等,以及如何通过命令行工具(如ping、tr...

网络工程师的圣经!世界级网工手绘268张图让TCP/IP直接通俗易懂

要把知识通俗地讲明白,真的不容易。——读者说TCP/IP从字面意义上讲,有人可能会认为TCP/IP是指TCP和IP两种协议。实际生活当中有时候也确实就是这两种协议。然而在很多情况下,它只是...

三分钟了解通信知识TCP与IP协议(含“通信技术”资料分享)

TCP/IPTCP/IP分层模型①应用层...

网闸与防火墙:网络安全设备的差异与应用

在网络安全领域,网闸(安全隔离网闸,GAP)和防火墙(Firewall)是两类重要的防护设备。尽管它们都服务于网络安全防护,但在设计理念、技术原理、安全效能及适用场景等方面存在显著差异,以下从五个维度...

S7-300的TCP/IP通信

一、首先在项目中创建2个S7-300的站点;二、硬件组态中,设置合适的TCP/IP地址,在同一网段内;...

西门子S7-1500 PLC的 MODBUS TCP通信

MODBUSTCP使MODBUS_RTU协议运行于以太网,MODBUSTCP使用TCP/IP和以太网在站点间传送MODBUS报文,MODBUSTCP结合了以太网物理网络和网络标准TC...

系统规划与管理师新版备考必备:第7章考点思维导图解析

备考系统规划与管理师的小伙伴们,福利又来啦!今天为大家带来《系统规划与管理师(第2版)》第7章考点的思维导图,助你高效梳理重点,让备考更有方向!...

TCP/IP、Http、Socket 有何区别与联系?

HTTP协议对应于应用层,Socket则是对TCP/IP协议的封装和应用(程序员层面上)。HTTP是应用层协议,主要解决如何包装数据。而我们平时说的最多的Socket是什么呢?实际上...

西门子PLC串口协议与以太网通信协议对比

西门子plc品牌众多,通信协议的类型就更多了,具体可分为串口协议和以太网通信协议两大类。...

网络编程懒人入门(十三):一泡尿的时间,快速搞懂TCP和UDP的区别

本文引用了作者Fundebug的“一文搞懂TCP与UDP的区别”一文的内容,感谢无私分享。1、引言...

程序员必备的学习笔记《TCP/IP详解(一)》

为什么会有TCP/IP协议在世界上各地,各种各样的电脑运行着各自不同的操作系统为大家服务,这些电脑在表达同一种信息的时候所使用的方法是千差万别。就好像圣经中上帝打乱了各地人的口音,让他们无法合作一样...

一文读懂TCP/IP协议工作原理和工作流程

简述本文主要介绍TCP/IP协议工作原理和工作流程。含义TCP/IP协议,英文全称TransmissionControlProtocol/InternetProtocol,包含了一系列构成互联网...

如何在 Windows 10 和 Windows 11 上重置 TCP/IP 堆栈

传输控制协议/Internet协议,通常称为TCP/IP,是您的WindowsPC如何与Internet上的其他设备进行通信的关键部分。但是当事情出错时会发生什么?你如何解决它?幸运的...

取消回复欢迎 发表评论: