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

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

相关文章

C语言函数递归实际应用举例详解

《C语言函数递归实际应用举例详解》程序调用自身的编程技巧称为递归,递归做为一种算法在程序设计语言中广泛应用,:本文主要介绍C语言函数递归实际应用举例的相关资料,文中通过代码介绍的非常详细,需要的朋... 目录前言一、递归的概念与思想二、递归的限制条件 三、递归的实际应用举例(一)求 n 的阶乘(二)顺序打印

SpringBoot利用@Validated注解优雅实现参数校验

《SpringBoot利用@Validated注解优雅实现参数校验》在开发Web应用时,用户输入的合法性校验是保障系统稳定性的基础,​SpringBoot的@Validated注解提供了一种更优雅的解... 目录​一、为什么需要参数校验二、Validated 的核心用法​1. 基础校验2. php分组校验3

Jackson库进行JSON 序列化时遇到了无限递归(Infinite Recursion)的问题及解决方案

《Jackson库进行JSON序列化时遇到了无限递归(InfiniteRecursion)的问题及解决方案》使用Jackson库进行JSON序列化时遇到了无限递归(InfiniteRecursi... 目录解决方案‌1. 使用 @jsonIgnore 忽略一个方向的引用2. 使用 @JsonManagedR

基于.NET编写工具类解决JSON乱码问题

《基于.NET编写工具类解决JSON乱码问题》在开发过程中,我们经常会遇到JSON数据处理的问题,尤其是在数据传输和解析过程中,很容易出现编码错误导致的乱码问题,下面我们就来编写一个.NET工具类来解... 目录问题背景核心原理工具类实现使用示例总结在开发过程中,我们经常会遇到jsON数据处理的问题,尤其是

Rust中的BoxT之堆上的数据与递归类型详解

《Rust中的BoxT之堆上的数据与递归类型详解》本文介绍了Rust中的BoxT类型,包括其在堆与栈之间的内存分配,性能优势,以及如何利用BoxT来实现递归类型和处理大小未知类型,通过BoxT,Rus... 目录1. Box<T> 的基础知识1.1 堆与栈的分工1.2 性能优势2.1 递归类型的问题2.2

利用Python编写一个简单的聊天机器人

《利用Python编写一个简单的聊天机器人》这篇文章主要为大家详细介绍了如何利用Python编写一个简单的聊天机器人,文中的示例代码讲解详细,感兴趣的小伙伴可以跟随小编一起学习一下... 使用 python 编写一个简单的聊天机器人可以从最基础的逻辑开始,然后逐步加入更复杂的功能。这里我们将先实现一个简单的

使用PyQt5编写一个简单的取色器

《使用PyQt5编写一个简单的取色器》:本文主要介绍PyQt5搭建的一个取色器,一共写了两款应用,一款使用快捷键捕获鼠标附近图像的RGB和16进制颜色编码,一款跟随鼠标刷新图像的RGB和16... 目录取色器1取色器2PyQt5搭建的一个取色器,一共写了两款应用,一款使用快捷键捕获鼠标附近图像的RGB和16

轻松掌握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