递归究竟是什么?如何快速编写正确的递归代码? —— 力扣经典面试题详解

这篇具有很好参考价值的文章主要介绍了递归究竟是什么?如何快速编写正确的递归代码? —— 力扣经典面试题详解。希望对大家有所帮助。如果存在错误或未考虑完全的地方,请大家不吝赐教,您也可以点击"举报违法"按钮提交疑问。

一、递归

1.1 什么是递归?

下面是来自百度百科对递归算法的定义:

 递归是一种算法设计技术,它允许一个函数在其定义或说明中有直接或间接调用自身的方法。
 递归在数学和计算机科学中有着广泛的应用,它通过将复杂问题分解为规模较小、形式相同的子问题来求解。递归的基本原理包括:每一级的函数调用都有自己的变量;每一次函数调用都会有一次返回;递归函数中,位于递归调用前的语句和各级被调用函数具有相同的执行顺序;递归函数中,位于递归调用后的语句的执行顺序和各个被调用函数的顺序相反;虽然每一级递归都有自己的变量,但是函数代码并不会得到复制。

简单来说,递归的本质就是函数自己调用自己!

1.2 为什么会用到递归?

 在回答这个问题前,我们先来看看二叉树的先序遍历是如何实现的?

 所谓先序遍历即依次遍历二叉树的根节点、左子树、右子树。在遍历过程中,我们发现左子树和右子树的遍历同样可以采用先序遍历来实现。即将先序遍历整颗二叉树转化先遍历完根节点后,在先序遍历来遍历左子树、右子树。而在先序遍历左/右子树时,我们可以将左子树和右子树当成一颗完整的二叉树,重复上述的过程,直到为空才结束。

  在求解问题时,我们发现可以将主问题可以分解为若干个相同的子问题,而这些子问题同样都可以分解为若干个相同的子子问题,不断重复。换句话说,假设我们可以通过一个方法f(可以将f想象成数学中求解问题的函数表达式f(x))来求解主问题,而主问题所转化出的子问题同样可以通过方法f来解决(而子问题所衍生出的子子问题同样可以通过方法f来解决,不断重复下去),从而实现函数自己调用自己!当面临这种情况时,即可使用递归算法来解决问题。

1.3 如何快速编写正确的递归代码?

  1. 找到相同的子问题,该过程决定了函数头的设计(即函数参数需要传哪些)
  2. 将其中一个子问题进行分析,在此过程中将递归函数当作一个黑盒,该函数能完成我们所赋予的功能。该过程决定了函数的函数体。
  3. 最后当然是返回值了。在何种情况下,递归结束。

下面以二叉树的先序遍历为例进行分析:
  首先先序遍历过程:根、左子树、右子树。而左子树和右子树显然是一个相同的子问题(左子树和右子树还可以继续细分下去,都行相同子问题,就不多说了)。所有我们需要给函数传一个根节点参数,用于分割整颗二叉树

void TreePrev(TreeNode* root)//函数头

  现在我们赋予了该递归函数功能:先序遍历二叉树。现在我们取其中一个子问题进行分析,首先我们需要遍历根节点,然后可以通过该递归函数来实现左子树、右子树的先序遍历了,即:

cout << root -> val <<" ";//根节点
//我们赋予了函数TreePrev先序遍历二叉树的功能,所有我们可以把该函数当作一个黑盒。
//只要我们将二叉树的根节点传入该黑盒,便可实现这颗二叉树的先序遍历。(实际该过程可以由画递归展开图验证,由于篇幅问题博主就不多说了)
TreePrev(root -> left);//遍历左子树
TreePrev(root -> right);//遍历右子树

  最后就是递归什么时候结束了。显然当根节点未空指针时,递归结束。此时返回空即可

if(root == nullptr)
	return;

所以整体代码就是:

void TreePrev(TreeNode* root)//函数头
{
	if(root == nullptr)
		return;
	cout << root -> val <<" ";//根节点
	//我们赋予了函数TreePrev先序遍历二叉树的功能,所有我们可以把该函数当作一个黑盒。
	//只要我们将二叉树的根节点传入该黑盒,便可实现这颗二叉树的先序遍历。(实际该过程可以由画递归展开图验证,由于篇幅问题博主就不多说了)
	TreePrev(root -> left);//遍历左子树
	TreePrev(root -> right);//遍历右子树	
}

二、力扣相关笔试题解析

面试题 08.06. 汉诺塔问题

递归究竟是什么?如何快速编写正确的递归代码? —— 力扣经典面试题详解,算法指南,leetcode,算法,职场和发展,学习和成长,递归
【分析】:
 题目指让A柱子中的N个圆盘(自下而上降序)借助B转移到C柱子上,并且保存顺序不变。所以我们可以考虑将A柱子上的后N-1个圆盘当作一个整体移到B上,然后将A中剩余的最后一个圆盘移到C上;最后在将B盘上的所有圆盘当作一个整体移到C上即可达成目的!
 在题目的分析过程中,出现将A上的N-1个圆盘移到B上,将B上的N-1个圆盘移到C上的过程。而这两个步骤显然是一个递归子问题(将一个柱子上的N个圆盘,借助另一个柱子转移到第三个柱子)。由于该过程中是具体圆盘在3根柱子间的移到,所以函数头因有4个参数:柱A、柱B、柱C、移到圆盘个数!!

 由于该过程中,我们通过递归函数(黑盒)将A柱上的N-1个圆盘移到B柱上、将A柱上的剩余的最后一个圆盘移到C柱、通过递归函数(黑盒)将B中的所有圆盘移到C柱。所以函数体为:

//_hanota为我们待实现的递归函数,该函数的功能是将将一个柱子上的N个圆盘,借助另一个柱子转移到第三个柱子
//所以将该函数想成一个黑盒,只需向该函数传递正确参数,即可实现对应的功能!(具体由函数展开图证明)
//先将A中前num-1个圆盘借助C移到B
_hanota(A, C, B, num - 1);
//将A剩余的最后一个元素移到C柱子
C.push_back(A.back());
A.pop_back();
//将B上的num-1个圆盘借助A移到C柱子上
_hanota(B, A, C, num - 1);

 最后返回值就不多说了,A中只有一个圆盘时,无需解决B,直接将圆盘从A移到C上。

【下面是其中一个子问题的圆盘转移过程图解】:
递归究竟是什么?如何快速编写正确的递归代码? —— 力扣经典面试题详解,算法指南,leetcode,算法,职场和发展,学习和成长,递归
【代码编写】:

class Solution {
public:
    void hanota(vector<int>& A, vector<int>& B, vector<int>& C) {
        _hanota(A, B, C, A.size());
    }
    void _hanota(vector<int>& A, vector<int>& B, vector<int>& C, int num) 
    {
        if(num == 1)
        {
            C.push_back(A.back());
            A.pop_back();
            return;
        }
        //先将A中前num-1个圆盘借助C移到B
        _hanota(A, C, B, num - 1);
        //将A剩余的最后一个元素移到C柱子
        C.push_back(A.back());
        A.pop_back();
        //将B上的num-1个圆盘借助A移到C柱子上
        _hanota(B, A, C, num - 1);
    }
};

21. 合并两个有序链表

【题目】:
递归究竟是什么?如何快速编写正确的递归代码? —— 力扣经典面试题详解,算法指南,leetcode,算法,职场和发展,学习和成长,递归
【解析】:
 这道题目的就是将两个升序的链表合并成一个升序链表,并返回新链表的头节点。
 在本题中,我们可以先选出两链表中节点值较小的节点,然后想办法将该链表中剩下的节点和另一条链表合并为升序链表;最后将最开始选出的较小节点的next指向该新合并的新节点即可解决问题。显然将该链表中剩下的节点和另一条链表合并为升序链表就是一个递归子问题,所以可以采用递归方式解决问题。
 由于递归子问题是合并两个有序链表,所有函数头传的参数为待合并的两个链表头节点。至于函数体,我们只需挑选出原来两链表中头节点值较小的节点作为新链表的头节点,然后通过递归函数将剩下的元素和另一个链表合并,最后将新链表的头节点的next指向合并链表的头节点。当链表节点为空时,递归结束。

【代码解析】:

class Solution {
public:
    ListNode* mergeTwoLists(ListNode* list1, ListNode* list2) {
    	//特殊处理
        if (list1 == nullptr)
            return list2;
        if (list2 == nullptr)
            return list1;
         
        if (list1->val < list2->val)
        {
            list1->next = mergeTwoLists(list1->next, list2);
            return list1;
        }
        else
        {
            list2->next = mergeTwoLists(list1, list2->next);
            return list2;
        }
    }
};

LCR 024. 反转链表

递归究竟是什么?如何快速编写正确的递归代码? —— 力扣经典面试题详解,算法指南,leetcode,算法,职场和发展,学习和成长,递归
反转链表我们可以将后n-1个节点反转(并返回反转后链表的头节点),然后在将原链表的头结合和新链表链接即可。
递归究竟是什么?如何快速编写正确的递归代码? —— 力扣经典面试题详解,算法指南,leetcode,算法,职场和发展,学习和成长,递归

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;
    }
}

24. 两两交换链表中的节点

递归究竟是什么?如何快速编写正确的递归代码? —— 力扣经典面试题详解,算法指南,leetcode,算法,职场和发展,学习和成长,递归
这道题本质上和反转链表类似,我们可以先将原链表头两个节点反转,然后利用递归子问题将后续所有节点当成一个完成链表,完成两两交换链表中的节点。最后将交换后的链表和原链表的头两个数据链接即可。(如果链表为空,或只有一个元素,则递归结束)

递归究竟是什么?如何快速编写正确的递归代码? —— 力扣经典面试题详解,算法指南,leetcode,算法,职场和发展,学习和成长,递归文章来源地址https://www.toymoban.com/news/detail-845252.html

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

到了这里,关于递归究竟是什么?如何快速编写正确的递归代码? —— 力扣经典面试题详解的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用

相关文章

  • Linux:进程等待究竟是什么?如何解决子进程僵尸所带来的内存泄漏问题?

     进程等待通常是指: 父进程通过wait()/waitpid()的方式,让父进程对子进程进行资源回收的等待过程!!  进程等待通常是为了解决以下两种情况: 解决子进程僵尸所带来的内存泄漏问题,对僵尸子进程进行资源回收! 原因在于当子进程僵尸后,便“刀枪不入”了。即使是

    2024年04月16日
    浏览(36)
  • 【数据结构】论如何拿捏快速排序?(含非递归)

    目录 一,快速排序(递归) 1,快排思想 2,霍尔排序 3,挖坑法 4,前后指针法 5,快速排序优化 1,三数取中法选key 2,小区间优化 二,快速排序(非递归) Stack.h Stack.c 三,快速排序源代码 1,快排思想 快速排序是 Hoare于1962年 提出的一种 二叉树结构的交换 排序方法,其

    2024年02月08日
    浏览(48)
  • 【数据结构与算法】如何对快速排序进行细节优化以及实现非递归版本的快速排序?

    君兮_的个人主页 即使走的再远,也勿忘启程时的初心 C/C++ 游戏开发 Hello,米娜桑们,这里是君兮_,国庆长假结束了,无论是工作还是学习都该回到正轨上来了,从今天开始恢复正常的更新频率,今天为大家带来的内容是快速排序的两大优化和非递归实现 好了废话不多说,开

    2024年02月08日
    浏览(37)
  • Git第十讲 Git如何正确使用log快速查找内容/提交

    在Git中,你可以使用不同的命令来快速查找指定内容或指定提交。下面我将介绍两种常用的方法。 要快速查找包含特定内容的文件或代码行,可以使用 git grep 命令。它类似于常见的 grep 命令,但是专门用于搜索Git仓库中的文件。 以下是使用 git grep 命令的示例: 在上述命令

    2024年02月09日
    浏览(35)
  • 如何正确看待低代码

    低代码(Low-Code)是一种软件开发方法,旨在通过最小化手动编码工作,使用图形用户界面和可视化建模工具,从而加速应用程序的开发过程。低代码平台提供了可视化的开发环境,让开发人员使用图形界面和少量的手动编码来创建应用程序。 主要的低代码平台特征包括:

    2024年02月03日
    浏览(37)
  • 【Algorithm】动态规划和递归问题:动态规划和递归有什么区别?如何比较递归解决方案和它的迭代版本?

    动态规划(Dynamic Programming,DP)和递归(Recursion)是解决复杂问题的两种不同方法,它们在计算机科学中常用于解决具有重叠子问题和最优子结构特性的问题。 1.1 递归 (Recursion)定义及优缺点 递归是一种通过将问题分解为更小的子问题来解决问题的方法。在递归中,一个函数

    2024年03月17日
    浏览(35)
  • k8s-如何快速编写yaml文件(新手)

    但是这个过程并没有在集群中执行,只是把结果通过yaml格式的方式输出出来,包括咱们可把它输出到文件里 场景:适用于部署好的项目,可以把部署好的项目中的yaml文件导出出来,实际效果比较实用

    2024年02月13日
    浏览(28)
  • 如何使用 ChatGPT 来快速编写产品需求文档(PRD)

    ChatGPT 即了解具体的编程知识,也了解编程之前的需求设计过程。因此产品经理也可以使用 ChatGPT 来快速编写PRD(产品需求文档, production requirement documentation)。 首先,我们可以尝试把需求交给 ChatGPT,发挥它的格式生成能力,快速扩充成一篇像模像样的 PRD 文档。 比如我们来规

    2024年02月11日
    浏览(30)
  • MVCC究竟是什么?

    MVCC,全称多版本并发控制 MVCC究竟是什么? 通俗的来说MVCC就是为了在读取数据时不加锁来提高读取效率的一种办法,MVCC解决的是读写时线程安全问题,线程不用去抢占读写锁。MVCC中的读就是快照读,也就是普通的select语句。 mvcc的具体实现通过数据库中的三个隐式字段、

    2024年02月10日
    浏览(37)
  • 补码究竟是什么?

    计算机底层均是通过二进制表示数据,原码、反码、补码是计算机中对数字的二进制表示方法。计算机以二进制补码的形式存储整数,最高位是符号位,0 表示正数,1 表示负数,其余位为数字位。 原码:将最高位作为符号位(0 表示正,1 表示负),其它数字位代表数值本身

    2024年02月02日
    浏览(28)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包