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

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

相关文章

mysql递归查询语法WITH RECURSIVE的使用

《mysql递归查询语法WITHRECURSIVE的使用》本文主要介绍了mysql递归查询语法WITHRECURSIVE的使用,WITHRECURSIVE用于执行递归查询,特别适合处理层级结构或递归... 目录基本语法结构:关键部分解析:递归查询的工作流程:示例:员工与经理的层级关系解释:示例:树形结构的数

使用Java编写一个字符脱敏工具类

《使用Java编写一个字符脱敏工具类》这篇文章主要为大家详细介绍了如何使用Java编写一个字符脱敏工具类,文中的示例代码讲解详细,感兴趣的小伙伴可以跟随小编一起学习一下... 目录1、字符脱敏工具类2、测试工具类3、测试结果1、字符脱敏工具类import lombok.extern.slf4j.Slf4j

Java并发编程之如何优雅关闭钩子Shutdown Hook

《Java并发编程之如何优雅关闭钩子ShutdownHook》这篇文章主要为大家详细介绍了Java如何实现优雅关闭钩子ShutdownHook,文中的示例代码讲解详细,感兴趣的小伙伴可以跟随小编一起... 目录关闭钩子简介关闭钩子应用场景数据库连接实战演示使用关闭钩子的注意事项开源框架中的关闭钩子机制1.

Spring Boot中JSON数值溢出问题从报错到优雅解决办法

《SpringBoot中JSON数值溢出问题从报错到优雅解决办法》:本文主要介绍SpringBoot中JSON数值溢出问题从报错到优雅的解决办法,通过修改字段类型为Long、添加全局异常处理和... 目录一、问题背景:为什么我的接口突然报错了?二、为什么会发生这个错误?1. Java 数据类型的“容量”限制

Java实现优雅日期处理的方案详解

《Java实现优雅日期处理的方案详解》在我们的日常工作中,需要经常处理各种格式,各种类似的的日期或者时间,下面我们就来看看如何使用java处理这样的日期问题吧,感兴趣的小伙伴可以跟随小编一起学习一下... 目录前言一、日期的坑1.1 日期格式化陷阱1.2 时区转换二、优雅方案的进阶之路2.1 线程安全重构2

使用Python实现一个优雅的异步定时器

《使用Python实现一个优雅的异步定时器》在Python中实现定时器功能是一个常见需求,尤其是在需要周期性执行任务的场景下,本文给大家介绍了基于asyncio和threading模块,可扩展的异步定... 目录需求背景代码1. 单例事件循环的实现2. 事件循环的运行与关闭3. 定时器核心逻辑4. 启动与停

浅析Java中如何优雅地处理null值

《浅析Java中如何优雅地处理null值》这篇文章主要为大家详细介绍了如何结合Lambda表达式和Optional,让Java更优雅地处理null值,感兴趣的小伙伴可以跟随小编一起学习一下... 目录场景 1:不为 null 则执行场景 2:不为 null 则返回,为 null 则返回特定值或抛出异常场景

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