递归专题---如何优雅的编写递归函数

2024-05-02 00:12

本文主要是介绍递归专题---如何优雅的编写递归函数,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

目录

一、递归函数的编写

二、题目练习

1、汉诺塔----点击跳转题目

2、合并两个有序链表---点击跳转题目

3、反转链表----点击跳转题目


在计算机科学中,递归是一种重要的概念和技术。它允许函数直接或间接地调用自身,以解决复杂的问题。

递归的基本思想是将一个大问题分解成若干个较小的、相似的子问题,然后通过不断地递归调用自身来解决这些子问题,直至达到基本情况。

递归在许多领域都有广泛的应用,如算法设计、数据结构处理等。它可以帮助我们更简洁、优雅地解决一些原本复杂的问题。

在算法题中,我们什么时候会使用递归,为什么会使用递归呢?当我们分析一道算法题时,发现主问题可以划分为相同的子问题,此时就可以考虑使用递归。

对于递归,我们可以尝试理解其中的运作原理,但在编写一道算法题时,其实大可不必,并且总是思考递归的细节还会影响我们编写的速度,递归其实就是用函数的栈帧帮助我们保存信息以及自动回退到上一层问题,针对一道题,如何编写递归呢?

一、递归函数的编写

对于一个递归函数,我们可以把它分为三部分:函数头、函数主体、递归出口;分别思考每一部分如何设计即可!

  • 先找到相同的子问题--->函数头; 对于找到的相同的子问题,我们就该考虑怎么解决这些相同的问题,思考函数的功能该如何定义?需要传入哪些参数?此时,一个递归函数的函数头已经完成。
  • 只关心某一个子问题如何解决---->函数体;完成函数头的定义后,我们把目光从观察多个子问题转变到聚焦于某一个子问题,思考如何通过定义的函数功能来解决这一个子问题(此时就会出现递归调用)。
  • 找到不可再划分的子问题---->递归出口;递归函数在层层调用解决相同子问题时这些子问题的相同只是解决方式相同,但是问题的规模是在逐渐变小的,当达到某个条件时子问题不可再划分,此时就是递归出口(通过条件判断结束函数调用)。



二、题目练习

1、汉诺塔----点击跳转题目

通过推演可以发现,无论A有多少个盘子,解决的思路都是,先把A上的n-1个盘子移动到B,在把A中最后一个盘子(也是最大的盘子)移动到C,再把B上的n-1个盘子移动到C上,如何移动呢?先把B上的n-2个盘子移动到A,再把B上的最后一个盘子.........

我们可以发现,每个子问题,除了移动盘子的起始柱子和目标柱子不一样,但是每次对盘子的操作方式都是一样的,通过辅助柱子来放置上面的盘子,再把起始柱子上最大的盘子移动到目标柱子,再递归的把刚才辅助柱子上的盘子移动到目标柱子(此时上一步的辅助柱子在这一步中就作为了起始柱子)

如何编写解决汉诺塔问题的递归函数呢?

  • 函数头:每个子问题都是把起始柱子的n个盘子通过辅助柱子合法的移动到目标柱子;递归函数的功能就可以如上定义,需要的参数就是三个柱子分别扮演的角色和需要移动的盘子个数 ----> 将x柱子上最上面的n个盘子借助y柱子转移到z柱子上。
 dfs(vector<int>& x,vector<int>& y,vector<int>& z,int n)
  • 函数体:

先把起始柱子的n-1个盘子移动到移动到辅助柱子:

dfs(x,z,y,n-1);

再把起始柱子上剩下的一个盘子移动到目标柱子上:

//模拟移动盘子的过程
z.push_back(x.back());x.pop_back();

最后把辅助柱子上的盘子递归的移动到目标柱子上:

  dfs(y,x,z,n-1);//这个函数调用后,辅助柱子就作为了起始柱子
  • 递归出口:当起始柱子上只有一个盘子时,只需要把盘子移动到目标柱子,此时问题不可再分。
if(n == 1) {z.push_back(x.back());x.pop_back();return ;}

完整代码;

class Solution {
public://将x柱子上最上面的n个盘子借助y柱子转移到z柱子上void dfs(vector<int>& x,vector<int>& y,vector<int>& z,int n){if(n == 1) {z.push_back(x.back());x.pop_back();return ;}dfs(x,z,y,n-1);z.push_back(x.back());x.pop_back();dfs(y,x,z,n-1);}void hanota(vector<int>& A, vector<int>& B, vector<int>& C) {dfs(A,B,C,A.size());}
};

2、合并两个有序链表---点击跳转题目

本题可以循环解决,主要研究如何递归解决,循环方法不过多叙述

递归理解:对于两个链表每一个结点,我们都是返回最小的结点,并让这个结点后面的链表与另一个链表合并

算法思路: 

  • 递归函数的含义:交给你两个链表的头结点,你帮我把它们合并起来,并且返回合并后的头结点;
  •  函数体:选择两个头结点中较⼩的结点作为最终合并后的头结点,然后将剩下的链表交给递归函数 去处理,再把返回的头节点连接起来。
  •  递归出⼝:当某⼀个链表为空的时候,返回另外⼀个链表。

代码:

class Solution {
public:ListNode* mergeTwoLists(ListNode* list1, ListNode* list2) {if(!list1) return list2;if(!list2) return list1;ListNode* ret;if(list1->val < list2->val){ret = list1;list1 = list1->next;ret->next = mergeTwoLists(list1,list2);}else{ret = list2;list2 = list2->next;ret->next = mergeTwoLists(list1,list2);}return ret;}
};

3、反转链表----点击跳转题目

本题也可以用循环解决(创捷一个虚拟头节点,遍历链表不断对虚拟头节点做头插操作)

递归如何解决呢?

给你一个链表,返回反转后的链表头节点,相同子问题就是反转链表

算法思路:

  •  递归函数的含义:交给你⼀个链表的头指针,你帮我逆序之后,返回逆序后的头结点;
  •  函数体:先把当前结点之后的链表逆序,逆序完之后,把当前结点添加到逆序后的链表后⾯即可;
  •  递归出⼝:当前结点为空或者当前只有⼀个结点的时候,不⽤逆序,直接返回。

代码:

class Solution {
public:ListNode* reverseList(ListNode* head) {if(head == nullptr || head->next == nullptr) return head;ListNode* newHead = reverseList(head->next);head->next->next = head;head->next = nullptr;return newHead;}
};

其实这份代码还有一种解读的视角,把这条链表看作是一根树,对树做DFS,直到遍历到最后一个结点,在每一次在回到上一层时该改变指针的指向,并把两个结点间的较上层的结点的next置为空(这一步是为了统一操作,把原来的其实结点的next置空)

置为空并不影响找到之前的结点,因为DFS时通过递归调用已经把结点信息都存储在函数栈帧中了

通过上面的题目练习,是不是对递归更加熟悉与了解了呢?其实在编写和理解递归函数时,我们要把递归函数当成一个能一定完成任务的黑盒,坚定不移的相信它可以完成它的使命,我们只需在递归函数中在合适的时机自信的调用它即可!

下一篇博客,会继续通过算法题来练习递归,敬请期待.....

这篇关于递归专题---如何优雅的编写递归函数的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/953012

相关文章

轻松掌握python的dataclass让你的代码更简洁优雅

《轻松掌握python的dataclass让你的代码更简洁优雅》本文总结了几个我在使用Python的dataclass时常用的技巧,dataclass装饰器可以帮助我们简化数据类的定义过程,包括设置默... 目录1. 传统的类定义方式2. dataclass装饰器定义类2.1. 默认值2.2. 隐藏敏感信息

Go信号处理如何优雅地关闭你的应用

《Go信号处理如何优雅地关闭你的应用》Go中的优雅关闭机制使得在应用程序接收到终止信号时,能够进行平滑的资源清理,通过使用context来管理goroutine的生命周期,结合signal... 目录1. 什么是信号处理?2. 如何优雅地关闭 Go 应用?3. 代码实现3.1 基本的信号捕获和优雅关闭3.2

C#如何优雅地取消进程的执行之Cancellation详解

《C#如何优雅地取消进程的执行之Cancellation详解》本文介绍了.NET框架中的取消协作模型,包括CancellationToken的使用、取消请求的发送和接收、以及如何处理取消事件... 目录概述与取消线程相关的类型代码举例操作取消vs对象取消监听并响应取消请求轮询监听通过回调注册进行监听使用Wa

使用Java编写一个文件批量重命名工具

《使用Java编写一个文件批量重命名工具》这篇文章主要为大家详细介绍了如何使用Java编写一个文件批量重命名工具,文中的示例代码讲解详细,感兴趣的小伙伴可以跟随小编一起学习一下... 目录背景处理1. 文件夹检查与遍历2. 批量重命名3. 输出配置代码片段完整代码背景在开发移动应用时,UI设计通常会提供不

【专题】2024飞行汽车技术全景报告合集PDF分享(附原数据表)

原文链接: https://tecdat.cn/?p=37628 6月16日,小鹏汇天旅航者X2在北京大兴国际机场临空经济区完成首飞,这也是小鹏汇天的产品在京津冀地区进行的首次飞行。小鹏汇天方面还表示,公司准备量产,并计划今年四季度开启预售小鹏汇天分体式飞行汽车,探索分体式飞行汽车城际通勤。阅读原文,获取专题报告合集全文,解锁文末271份飞行汽车相关行业研究报告。 据悉,业内人士对飞行汽车行业

如何编写Linux PCIe设备驱动器 之二

如何编写Linux PCIe设备驱动器 之二 功能(capability)集功能(capability)APIs通过pci_bus_read_config完成功能存取功能APIs参数pos常量值PCI功能结构 PCI功能IDMSI功能电源功率管理功能 功能(capability)集 功能(capability)APIs int pcie_capability_read_wo

音视频入门基础:WAV专题(10)——FFmpeg源码中计算WAV音频文件每个packet的pts、dts的实现

一、引言 从文章《音视频入门基础:WAV专题(6)——通过FFprobe显示WAV音频文件每个数据包的信息》中我们可以知道,通过FFprobe命令可以打印WAV音频文件每个packet(也称为数据包或多媒体包)的信息,这些信息包含该packet的pts、dts: 打印出来的“pts”实际是AVPacket结构体中的成员变量pts,是以AVStream->time_base为单位的显

如何更优雅地对接第三方API

如何更优雅地对接第三方API 本文所有示例完整代码地址:https://github.com/yu-linfeng/BlogRepositories/tree/master/repositories/third 我们在日常开发过程中,有不少场景会对接第三方的API,例如第三方账号登录,第三方服务等等。第三方服务会提供API或者SDK,我依稀记得早些年Maven还没那么广泛使用,通常要对接第三方

PHP实现二叉树遍历(非递归方式,栈模拟实现)

二叉树定义是这样的:一棵非空的二叉树由根结点及左、右子树这三个基本部分组成,根据节点的访问位置不同有三种遍历方式: ① NLR:前序遍历(PreorderTraversal亦称(先序遍历)) ——访问结点的操作发生在遍历其左右子树之前。 ② LNR:中序遍历(InorderTraversal) ——访问结点的操作发生在遍历其左右子树之中(间)。 ③ LRN:后序遍历(PostorderT

专题二_滑动窗口_算法专题详细总结

目录 滑动窗口,引入: 滑动窗口,本质:就是同向双指针; 1.⻓度最⼩的⼦数组(medium) 1.解析:给我们一个数组nums,要我们找出最小子数组的和==target,首先想到的就是暴力解法 1)暴力: 2)优化,滑动窗口: 1.进窗口 2.出窗口 3.更新值 2.⽆重复字符的最⻓⼦串(medium) 1)仍然是暴力解法: 2)优化: 进窗口:hash[s[rig