【数据结构与算法】第十八篇:递归,尾递归,尾调用

news2024/11/29 22:51:24

知识概览

  • 一、递归的引入(递归现象)
  • 二、递归的调用过程与实例分析
  • 三、递归的基本思想
    • 小tip:链表递归的具体实例
  • 四、递归的一般使用条件
  • 五、实例分析:斐波那契数列
    • 1.原理剖析
    • 2.fib优化1 – 记忆化
    • 3.fib优化2
    • 4.fib优化3
  • 六、实例分析:青蛙跳台阶问题
  • 七、实例分析:汉诺塔问题
  • 八、递归转非递归分析
  • 九、尾调用,尾递归(了解)
    • 1. 尾调用的优化(了解)


一、递归的引入(递归现象)

递归思想想必大家都不陌生。它分为“递”和“归”两个过程。是一种常见的算法策略。类似于以下的故事场景:
1.从前有座山,山里有座庙,庙里有个老和尚,正在给小和
尚讲故事呢!故事是什么呢?【从前有座山,山里有座庙,
庙里有个老和尚,正在给小和尚讲故事呢!故事是什么呢?
『从前有座山,山里有座庙,庙里有个老和尚,正在给小
和尚讲故事呢!故事是什么呢?……』】
2.GNU 是 GNU is Not Unix 的缩写
GNU → GNU is Not Unix → GNU is Not Unix is Not Unix → GNU is Not Unix is Not Unix is Not Unix
3.假设A在一个电影院,想知道自己坐在哪一排,但是前面人很多,
A 懒得数,于是问前一排的人 B【你坐在哪一排?】,只要把 B 的答案加一,就是 A 的排数。
B 懒得数,于是问前一排的人 C【你坐在哪一排?】,只要把 C 的答案加一,就是 B 的排数。
C 懒得数,于是问前一排的人 D【你坐在哪一排?】,只要把 D 的答案加一,就是 C 的排数。

二、递归的调用过程与实例分析

以下面这段代码为例

/**
     * 计算n的阶乘
     * 1*2*3*4......*(n-1)*n
                */
        public int Fac(int n){
            if(n<=1)return n;
            return n*Fac(n-1);
    }

在这里插入图片描述
时间,空间复杂度分析
递归所需要的时间:T(n)=T(n-1)+O(1)
根据时间对应表如下:
在这里插入图片描述可以得出时间复杂度为 : O(n)
空间复杂度为: O(n) (开辟了n个占空间)

几种优化思想
在这里插入图片描述

三、递归的基本思想

拆解思想
1.把规模较大的问题转化为同类型的规模较小的问题。
2.把规模较小的问题转化为规模更小的问题。
3.规模较小的问题可以直接得出他的答案。

求解
1.由最小规模问题的解得出较大规模问题的解
2.由较大规模问题的解不断得出规模更大问题的解
3.最后得出原来问题的解
这两种思想正好体现了递归的 ‘递’和‘归’两个过程
在这里插入图片描述类似于可以利用上面这种思想解题的都可以考虑递归,递归不是为了得到最优解,而是为了简化解题思想。
很多链表、二叉树相关的问题都可以使用递归来解决
✓ 因为链表、二叉树本身就是递归的结构(链表中包含链表,二叉树中包含二叉树)

小tip:链表递归的具体实例

2.两数相加
在这里插入图片描述

class Solution {
    public ListNode addTwoNumbers(ListNode l1, ListNode l2) {
       if(l1==null)return l2;
       if(l2==null)return l1;
       int sum=l1.val+l2.val;
        ListNode head=new ListNode(sum%10);
        head.next=addTwoNumbers(l1.next,l2.next);
        if(sum>9)head.next=addTwoNumbers(head.next,new ListNode(1));
        return head;
    }
}

四、递归的一般使用条件

① 明确函数的功能 (重要!!!)
先不要去思考里面代码怎么写,首先搞清楚这个函数的干嘛用的,能完成什么功能?
② 明确原问题与子问题的关系
寻找 f(n) 与 f(n – 1) 的关系
③ 明确递归基(边界条件)
递归的过程中,子问题的规模在不断减小,当小到一定程度时可以直接得出它的解
重要:寻找递归基,相当于是思考:问题规模小到什么程度可以直接得出解?

五、实例分析:斐波那契数列

斐波那契数列:1、1、2、3、5、8、13、21、34、……
F(1)=1,F(2)=1, F(n)=F(n-1)+F(n-2)(n≥3)
◼ 编写一个函数求第 n 项斐波那契数
在这里插入图片描述
以上写法的空间复杂度为O(n).
你可能会疑惑为什么双路递归那么重复调用,为什么空间复杂度才O(n)级别,而时间复杂度却达到了恐怖的O(2n)?这里我们先抛出一个计算递归空间复杂度复杂度的通式
递归调用的空间复杂度=递归深度*每次递归调用所需要的辅助空间。
具体原理我们下文细细剖析~

1.原理剖析

根据以上的复杂度分析,对于斐波那契这种多路递归,时间复杂度为O(2n),这个时间复杂度是十分恐怖的。双路递归不可避免的重复计算很多。
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

2.fib优化1 – 记忆化

在上面的介绍种,斐波那契额数列性能差的一个重要因素就是拥有大量的重复计算。
假如我们从根节点向着一个方向先进行递归,记住已经算过的递归调用的返回值
然后在进行另一个分支计算时就不用再次进行重复计算了,大大减少了重复计算。我们用数组来实现这个思想。

 public static int fib2(int n){
        if(n<=2)return 1;
        int [] array=new int [n+1];
        array[1]=array[2]=1;
        return fib3(n,array);
    }
    public static int fib3(int n,int []array){
        if(array[n]==0){//说明该位置还没有赋值
            array[n]=fib3(n-1,array)+fib3(n-2,array);
        }
        return array[n];
    }
}

在这里插入图片描述
◼ 时间复杂度:O(n),空间复杂度:O(n)

3.fib优化2

去除递归操作

//去除递归优化时间复杂度O(n)
    public static int fib1(int n){
        int [] array=new int[n+1];
        array[1]=array[2]=1;
        for(int i=3;i<=n;i++){
            array[i]=array[i-1]+array[i-2];
        }
        return array[n];
    }

4.fib优化3

由于每次运算只需要用到数组中的 2 个元素,所以可以使用滚动数组来优化,创建一个空间为2的数组每次,依次向下更新数组元素的值。
这里的&运算符就是%运算符的作用。
在这里插入图片描述

public static int fib4(int n){
            if(n<=2)return 1;
            int [] arr=new int [2];
            arr[0]=arr[1]=1;
            for(int i=3;i<=n;i++){
                //始终着眼于两个数组
                arr[i&1]=arr[(i-1)&1]+arr[(i-2)&1];
            }
            return arr[n&1];
        }
        //上台阶
        public static int f(int n){
            if(n<=2)return n;
            return f(n-1)+f(n-2);

        }

这里为什么可以用&取代%(%2都可以改成&1)
在这里插入图片描述

六、实例分析:青蛙跳台阶问题

楼梯有 n 阶台阶,上楼可以一步上 1 阶,也可以一步上 2 阶,走完 n 阶台阶共有多少种不同的走法?
√假设 n 阶台阶有 f(n) 种走法,第 1 步有 2 种走法
✓ 如果上 1 阶,那就还剩 n – 1 阶,共 f(n – 1) 种走法
✓ 如果上 2 阶,那就还剩 n – 2 阶,共 f(n – 2) 种走法
√所以 f(n) = f(n – 1) + f(n – 2)
在这里插入图片描述

  public int climbStairs(int n){
        if(n<=2)return n;
        return climbStairs(n-1)+climbStairs(n-2);

    }

迭代优化

  public int climbStairs1(int n){
        if(n<=2)return n;
        int first=1;
        int second=2;
        for(int i=3;i<=n;i++){
            second=first+second;
            first=second-first;
        }
        return second;
    }

七、实例分析:汉诺塔问题

编程实现把 A 的 n 个盘子移动到 C(盘子编号是 [1, n] )
1.每次只能移动1个盘子
2.大盘子只能放在小盘子下面
在这里插入图片描述递归的基本思想,找到递归是条件
最初的思想是将n个木盘从A移动到C,B作为中间过渡柱子。
现在可以找到共同性(1)先将n-1个柱子从A移动到B,C作为中间过渡柱子。
(2)然后再将第n个柱子移动到C.(3)然后再将n-1个柱子从B移动到C,A最为中间过渡柱子。
找到则会个规律我们用代码来实现以下。

/**
     * 柱子p1  p2  p3
     * 木板:
     *      1
     *      2
     *      3
     *      4
     *    .......
     *     n-1
     *      n
     *    按位置来说:p1:起始位置  p2:中间过渡位置  p3:最终位置
     *
     */
    //找到递归规律
    public static void  hanoi(int n,String p1,String p2,String p3){
        //就一块板子
        if(n==1){
            move(n,p1,p3);
            return;
        }
        hanoi(n-1,p1,p3,p2);
        move(n,p1,p3);
        hanoi(n-1,p2,p1,p3);
    }
    public static void move(int n,String from,String to){
        System.out.println("将"+n+"号盘子从"+from+"移动到"+to);
    }

八、递归转非递归分析

递归分为递和归两个过程,所以再递的过程中,先一直向更深的方向递归,然后数据规模小到一定程度,直接返回一个数然后递归的空间按时间开辟晚到近依次释放。这和栈的先进后出的特点十分相似。不出所料递归的底层也是通过栈实现的,所以一般递归都可以通过栈转化为非递归。
实例一:递归形式


    public void log(int n){
        if(n<1)return;
        log(n-1);
        int v=n+10;
        System.out.println(v);
    }

非递归实现(创建辅助对象)

 //非递归实现
    public void log1(int n){
        Stack<Model> stack=new Stack<>();
       while(n>0){
           stack.push(new Model(n,n+10));
           n--;
       }
       while(!stack.isEmpty()){
           System.out.println(stack.pop().getV());
       }
    }

package Test01;

public class Model {
   private int n;
    private int v;

    public Model(int n, int v) {
        this.n = n;
        this.v = v;
    }

    public int getN() {
        return n;
    }

    public void setN(int n) {
        this.n = n;
    }

    public int getV() {
        return v;
    }

    public void setV(int v) {
        this.v = v;
    }
}

九、尾调用,尾递归(了解)

◼ 尾调用:一个函数的最后一个动作是调用函数
◼如果最后一个动作是调用自身,称为尾递归(Tail Recursion),是尾调用的特殊情况
在这里插入图片描述◼ 一些编译器能对尾调用进行优化,以达到节省栈空间的目的(但是java编译器并不支持)
上面图中的代码为例,按照代码中的调用应该会向上不断开辟空间,但是如果是尾调用优化的话则不会在想上开辟空间直接重复利用test1的空间。
在这里插入图片描述那么问题来了:如果是尾递归肯定没得说,每次调用的栈空间肯定是相同大小的,如果是尾调用,调用其他方法,栈空间不一样大怎么办?
答:这个大可不必担心,因为,某些编译器的底层会对其在原有基础上进行扩容。

尾调用判断

}
    /**
     * 计算n的阶乘
     * 1*2*3*4......*(n-1)*n
                */
        public int Fac(int n){
            if(n<=1)return n;
            return n*Fac(n-1);
    }

这个不是尾调用因为,最后一步执行的是*,而不是调用函数。

  void test3(int n){
        int a=10;
        int b=a+10;
        test4(b);
        int c=a+b;//这里
        int d;
    }
    void test4(int b){
        int x1=30;
        int x2=50;
    }

不是尾调用,只有最后一句是调用函数的时候才是尾调用
为什么只有最后一句是尾调用的时候才是尾调用?
一上面这段代码为例。
在这里插入图片描述

1. 尾调用的优化(了解)

实例一:n的阶乘改成非递归

 //改成尾调用优化
    public int Fac1(int n) {
        return Fac2(n,1);
    }
    public int Fac2(int n,int result){
            if(n<=1)return n;
            //result:之前的结果
            return Fac2(n-1,result*n);
    }

实例二:斐波那契改成非递归

 //斐波那契实现为调用
    public int Fib(int n){
            return Fib1(n,1,1);
    }
    public int Fib1(int n,int first,int second){
            if(n<=1)return first;
            return Fib1(n-1,second,first+second);
    }

在这里插入图片描述

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/166008.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

mac下ssh连接docker使用centos

配置ssh连接docker本机信息 Apple M2/ macOS Ventura 13.1完整实现如下&#xff1a;使用docker下载centos镜像docker pull centos:centos7 # centos7 指定安装版本查看本地镜像# 使用以下命令查看是否已安装了centos7➜ ~ docker images REPOSITORY TAG IMAGE ID …

c++通讯录管理系统

结构体1&#xff0c;知识点&#xff08;结构体&#xff09;&#xff0c;存放人员详情&#xff0c;名字&#xff0c;性别&#xff0c;年龄等 struct person { string m_name; int m_sex; int m_age; string m_phone; string m_addr; };结构体2&#xff0c;知识点 &#xff08;结…

狗厂的N+1+2毕业,我觉得还是挺良心的

最近又跟朋友打听到了新鲜事&#xff0c;年底的新鲜事&#xff0c;什么209万&#xff0c;就是听个乐子&#xff0c;离我太远&#xff0c;什么HR和技术人员产生矛盾&#xff0c;一巴掌眼镜都打飞了&#xff0c;好乱套&#xff0c;今天我跟朋友打听了一些不太乱套的 一、鹅肠 1.…

Quartz认知篇 - 初识分布式任务调度Quartz

定时任务的使用场景 在遇到如下几种场景可以考虑使用定时任务来解决&#xff1a; 某个时刻或者时间间隔执行任务 批量数据进行处理 对两个动作进行解耦 Quartz 介绍 Quartz 是一个特性丰富的、开源的任务调度库&#xff0c;几乎可以嵌入所有的 Java 程序&#xff0c;包括很…

基于二叉树的改进SPIHT算法(Matlab代码实现)

&#x1f4a5;&#x1f4a5;&#x1f49e;&#x1f49e;欢迎来到本博客❤️❤️&#x1f4a5;&#x1f4a5; &#x1f3c6;博主优势&#xff1a;&#x1f31e;&#x1f31e;&#x1f31e;博客内容尽量做到思维缜密&#xff0c;逻辑清晰&#xff0c;为了方便读者。 ⛳️座右铭&a…

电脑怎么设置动态壁纸?关于Windows和Mac壁纸的设置方法

为了让电脑桌面更加美观舒适&#xff0c;很多人都会给电脑的桌面设置自己喜欢的壁纸。图片壁纸很多人都会设置&#xff0c;但是电脑怎么设置动态壁纸&#xff1f;这是很多人的困扰。其实方法同样很简单&#xff0c;下面有关于Windows和Mac动态壁纸的设置方法&#xff0c;一起来…

【阶段四】Python深度学习03篇:深度学习基础知识:神经网络可调超参数:激活函数、损失函数与评估指标

本篇的思维导图: 神经网络可调超参数:激活函数 神经网络中的激活函数(有时也叫激励函数)。 在逻辑回归中,输入的特征通过加权、求和后,还将通过一个Sigmoid逻辑函数将线性回归值压缩至[0,1]区间,以体现分类概率值。这个逻辑函数在神经网络中被称为…

PyCharm调用远程Python解释器

PyCharm调用远程Python解释器 PyCharm中直接调用远程服务器中Python解释器&#xff1a; 本地不用搭建Python环境。既避免了本地使用Window而服务器使用Linux系统不统一情况&#xff0c;又不用担心本地调试没问题而放到服务器上就出现问题。 PyCharm中打开项目并设置Python解释…

封装chrome镜像

chrome镜像 selenium提供了一个镜像&#xff0c;但这个镜像里面包含了比较多的东西&#xff1a; 镜像地址-github supervisord java chrome webDriver 实际的使用中遇到了一些问题 chrome遇到一些比较耗费内存和cup的操作的时候&#xff0c;有的时候会kill掉java进程&a…

干货 | 大数据交易所数据安全流通体系标准化尝试

以下内容整理自清华大学《数智安全与标准化》课程大作业期末报告同学的汇报内容。第一部分&#xff1a;国内大数据交易所发展现状第二部分&#xff1a;国外大数据交易模式及法律法规欧盟的数据交易模式是基于2022年5月16日所提出的《数据治理法案》&#xff0c;其中提出了数据中…

【C++11】—— 包装器

目录 一、function包装器 1. function包装器基本介绍 2. function包装器统一类型 3. function包装器的使用场景 二、bind包装器 一、function包装器 1. function包装器基本介绍 function包装器 也叫作适配器。C中的function本质是一个类模板&#xff0c;也是一个包装器…

第四章 基本数据

在第2章中&#xff0c;我们讨论了多种导入数据到R中的方法。遗憾的是&#xff0c;将你的数据表示为矩阵或数据框这样的矩形形式仅仅是数据准备的第一步。这里可以演绎Kirk船长在《星际迷航》“末日决战的滋味”一集中的台词&#xff08;这完全验明了我的极客基因&#xff09;&a…

聚观早报|春节档新片预售总票房破千万;苹果获可折叠iPhone新专利

今日要闻&#xff1a;比亚迪据称拟在越南建汽车零部件厂&#xff1b;2023 年春节档新片预售总票房破 7000 万&#xff1b;苹果获得可折叠 iPhone 新专利&#xff1b;北京汽车获1000台EU5 PLUS约旦订单&#xff1b;娃哈哈要解决100万农户农产品出路 比亚迪据称拟在越南建汽车零部…

C 语言目标文件

前言 一个 C 语言程序经编译器和汇编器生成可重定位目标文件&#xff0c;再经链接器生成可执行目标文件。那么目标文件中存放的是什么&#xff1f;我们的源代码在经编译以后又是怎么存储的&#xff1f; 文章为 《深入理解计算机系统》的读书笔记&#xff0c;更为详细的内容可…

【数据结构】双向链表

1.双向链表的结构2.双向链表的实现首先在VS里面的源文件建立test.c和List.c,在头文件里面建立List.hList.h:#pragma once #include <stdio.h> #include <stdlib.h> #include <assert.h> typedef int LTDateType; typedef struct ListNode {LTDateType data;s…

LeetCode 329. 矩阵中的最长递增路径(C++)*

思路&#xff1a; 1.用动态规划&#xff0c;但是时间复杂度太高&#xff0c;效率太低 2.使用常规的DFS&#xff0c;时间复杂度高&#xff0c;包含了太多重复无效遍历&#xff0c;会超时 3.在DFS的基础上使用记忆化搜索&#xff0c;帮助消去重复的遍历&#xff0c;提高效率 原题…

解决: 您目前无法访问 因为此网站使用了 HSTS。网络错误和攻击通常是暂时的,因此,此网页稍后可能会恢复正常

目录 问题描述 报错信息 问题原因 如何解决 参考资料 问题描述 您目前无法访问 因为此网站使用了 HSTS。网络错误和攻击通常是暂时的&#xff0c;因此&#xff0c;此网页稍后可能会恢复正常。 报错信息 今天使用Edge浏览器在访问一个平时常用的emoji网站时&#xff0c;…

springboot整合spring-security

在web开发中&#xff0c;安全性问题比较重要&#xff0c;一般会使用过滤器或者拦截器的方式对权限等进行验证过滤。此博客根据b站up主&#xff0c;使用demo示例进行展示spring-security的一些功能作用。 目 录 1、创建项目 2、编写controller 3、添加spring-security依赖 …

Spring Cloud OpenFeign 配置

最少的配置&#xff08;使用默认配置&#xff09; 最少/默认配置示例如下&#xff08;使用Nacos作为服务的注册与发现中心&#xff09;&#xff1a; application.properties server.port8082 spring.application.namenacos-consumer spring.cloud.nacos.discovery.server-ad…

[拆轮子] PaddleDetection中__shared__、__inject__ 和 from_config 三者分别做了什么

在上一篇中&#xff0c;PaddleDetection Register装饰器到底做了什么 https://blog.csdn.net/HaoZiHuang/article/details/128668393 已经介绍了 __shared__ 和 __inject__ 的作用: __inject__ 表示引入全局字典中已经封装好的模块。如loss等。__shared__为了实现一些参数的配…