递归为什么这么难?一篇文章带你了解递归

news2024/11/15 5:55:08

递归为什么这么难?一篇文章带你了解递归

美国计算机科学家——彼得·多伊奇(L Peter Deutsch)在《程序员修炼之道》(The Pragmatic Programmer)一书中提到“To Iterate is Human, to Recurse, Divine”——我理解的这句话为:人理解迭代,神理解递归。

毋庸置疑递归的代码是非常简洁的,但是想要理解递归也是非常不容易的,本文介绍了递归的常见场景与例题和递归的基本用法与思想,希望能帮助新人理解递归的思想,相信看完这篇文章再动手敲一下代码,一定对递归有更加深入的了解。

文章列举了一些递归的经典操作包括:斐波纳契数列、汉诺塔、冒泡排序的递归写法。以及力扣的一些链表的练习题使用递归去完成——206. 反转链表 - 力扣(LeetCode)、203. 移除链表元素、19. 删除链表的倒数第 N 个结点、83. 删除排序链表中的重复元素、82. 删除排序链表中的重复元素 II、21. 合并两个有序链表、23. 合并 K 个升序链表。

首先让我们思考:

  1. 什么是递归?
  2. 递归的思想是什么?
  3. 怎么使用递归?
  4. 使用递归应该注意什么问题?
  5. 递归的时间复杂度应该怎么计算

一、什么是递归?

在计算机中,递归(Recursion)是指在函数的定义中使用函数自身的方法。实际上,递归,顾名思义,其包含了两个意思:

二、递归的思想是什么?

既然叫做递归,那么肯定分为“递”与“归”。

image-20231027174229279

递归的基本思想就是将大规模问题转为小规模问题,问题一直被不断缩小,一直递归到符合结束条件为止。

因为每次递归都是相同的函数,所以很重要的一件事是寻找到递归的条件,找到应该如何解决大问题和小问题的同一个方法。

三、怎么使用递归

我们在了解了递归的基本概念以后就需要思考递归应该怎么用?

首先需要明确递归的三要素:

  • 明确递归终止条件;

    递归既然有去有回,那么必须有一个明确的结束条件。当到达这个条件递归就会终止。

  • 给出递归终止时的处理办法;

    当递归结束时,递归函数每一次返回值都需要有处理的方法,我们需要在这里给出问题的解决方法。

  • 提取重复的逻辑,缩小问题规模。找出递归关系式

    寻找一个递归的关系,如何将这个问题不停分解为小问题。

注意:判断“递”还是“归”

判断是在“递”的过程中解决问题还是在“归”的过程中解决问题

四、使用递归应该注意什么?

首先我们要知道递归有两种模型。

1、在“递”的的过程中解决问题。

{
    1、递归结束条件
        
    2、问题的解决方法
        
    3、递归的等价关系,缩小规模的方法。
}

2、在“归”的的过程中解决问题。

{
    1、递归结束条件
        
    2、递归的等价关系,缩小规模的方法。
        
    3、问题的解决方法
}

这两种模型都是属于单路递归的模型,既然有单路递归那么肯定会有多路递归。

在之后的经典场景分析会介绍到冒泡排序的递归写法,就是单路递归。汉诺塔、斐波纳契数列就是属于多路递归。

五、递归的时间复杂度怎么计算?

若有递归式
T ( n ) = a T ( n b ) + f ( n ) T(n) = aT(\frac{n}{b}) + f(n) T(n)=aT(bn)+f(n)
其中

  • T ( n ) T(n) T(n) 是问题的运行时间, n n n 是数据规模
  • a a a 是子问题个数
  • T ( n b ) T(\frac{n}{b}) T(bn) 是子问题运行时间,每个子问题被拆成原问题数据规模的 n b \frac{n}{b} bn
  • $ f(n)$ 是除递归外执行的计算

x = log ⁡ b a x = \log_{b}{a} x=logba,即 x = log ⁡ 子问题缩小倍数 子问题个数 x = \log_{子问题缩小倍数}{子问题个数} x=log子问题缩小倍数子问题个数

那么
T ( n ) = { Θ ( n x ) f ( n ) = O ( n c ) 并且 c < x Θ ( n x log ⁡ n ) f ( n ) = Θ ( n x ) Θ ( n c ) f ( n ) = Ω ( n c ) 并且 c > x T(n) = \begin{cases} \Theta(n^x) & f(n) = O(n^c) 并且 c \lt x\\ \Theta(n^x\log{n}) & f(n) = \Theta(n^x)\\ \Theta(n^c) & f(n) = \Omega(n^c) 并且 c \gt x \end{cases} T(n)= Θ(nx)Θ(nxlogn)Θ(nc)f(n)=O(nc)并且c<xf(n)=Θ(nx)f(n)=Ω(nc)并且c>x

例1

T ( n ) = 16 T ( n 4 ) + n 2 T(n) = 16T(\frac{n}{4}) + n^2 T(n)=16T(4n)+n2

  • a = 16 , b = 4 , x = 2 , c = 2 a=16, b=4, x=2, c=2 a=16,b=4,x=2,c=2
  • 此时 x = 2 = c x=2 = c x=2=c,时间复杂度 Θ ( n 2 log ⁡ n ) \Theta(n^2 \log{n}) Θ(n2logn)

例2 二分查找递归

int f(int[] a, int target, int i, int j) {
    if (i > j) {
        return -1;
    }
    int m = (i + j) / 1;
    if (target < a[m]) {
        return f(a, target, i, m - 1);
    } else if (a[m] < target) {
        return f(a, target, m + 1, j);
    } else {
        return m;
    }
}
  • 子问题个数 a = 1 a = 1 a=1
  • 子问题数据规模缩小倍数 b = 2 b = 2 b=2
  • 除递归外执行的计算是常数级 c = 0 c=0 c=0

T ( n ) = T ( n 2 ) + n 0 T(n) = T(\frac{n}{2}) + n^0 T(n)=T(2n)+n0

  • 此时 x = 0 = c x=0 = c x=0=c,时间复杂度 Θ ( log ⁡ n ) \Theta(\log{n}) Θ(logn)

六、递归的实战

遇见递归请不要害怕,只是因为你做题少了而已。做完这些题一定对递归的感悟会更深刻。

1、基本运算中的递归

a、冒泡排序的递归写法
public class bubble {
    public static void main(String[] args) {
        int a[] = {1, 5, 7, 2, 0, 3, 6};
        Bubble(a, a.length - 1);
        for (int i : a) {
            System.out.println(i);
        }
    }

    public static void Bubble(int[] a, int len) {
        //1、递归结束条件
        if (len == 0) return;

        //2、处理方法
        for (int i = 0; i < len; i++) {
            if (a[i] > a[i + 1]) {
                int temp = a[i];
                a[i] = a[i + 1];
                a[i + 1] = temp;
            }
        }

        //3、递归关系,缩小问题规模
        Bubble(a, len - 1);
    }
}

通过这个简单算法,可以看出这个就属于在递的过程中解决问题的模型。

我们逐轮分析:

初始数据:{1, 5, 7, 2, 0, 3, 6}

第一轮:找到最大的数进行下沉,得到:{1, 5, 2, 0, 3, 6,7},将7移动到最后一位,那么第二轮就不需要对7进行排序。

第二轮:找到最大的数进行下沉,因为上一轮已经找到7,那么这一轮只需要找7前面的数就行了,得到:{1, 5, 2, 0, 3, 6,7},那么第三轮就不需要对6进行排序。

b、汉诺塔的实现

image-20231027182318701

通过这个移动过程我们很容易找到一个移动规律,具体就不多说了,代码如下:

import java.util.LinkedList;

public class Hanoi {

    static LinkedList<Integer> a = new LinkedList<>();
    static LinkedList<Integer> b = new LinkedList<>();
    static LinkedList<Integer> c = new LinkedList<>();

    public static void main(String[] args) {
        long startTime = System.nanoTime();
        init(3);
        long ebdTime  = System.nanoTime();
        print();
    }

    private static void print() {
        System.out.println("**************************");
        System.out.println(a);
        System.out.println(b);
        System.out.println(c);
    }

    /**
     * 汉诺塔的递归
     * @param n 塔的层数
     * @param a 原
     * @param b 借
     * @param c 目标
     */
    public static void towerOfHanoi(int n,LinkedList<Integer> a
                    ,LinkedList<Integer> b,LinkedList<Integer> c){

        //结束条件
        if (n == 0){
            return;
        }

        //等价关系
        towerOfHanoi(n-1,a,c,b);
        //处理方法
        c.add(a.removeLast());  //将a移动到c
        //等价关系
        towerOfHanoi(n-1,b,a,c);

    }

    public static void init(int n){
        for (int i = n; i>=1; i--){
            a.add(i);
        }
        System.out.println(a);
        towerOfHanoi(n,a,b,c);
    }
}
c、斐波纳契数列

如果不知道斐波纳契的具体请看这篇文章——用C语言写爬楼梯(斐波那契数列的应用,迭代与递归)爬楼梯问题超详细,看完这一篇就够了。,就不多赘述了,这里主要介绍一下在递归中的减枝操作。

未减枝前的递归分解过程:

image-20221207092417933

可以看到(颜色相同的是重复的):

  • f ( 3 ) f(3) f(3) 重复了 2 次
  • f ( 2 ) f(2) f(2) 重复了 3 次
  • f ( 1 ) f(1) f(1) 重复了 5 次
  • f ( 0 ) f(0) f(0) 重复了 3 次

随着 n n n 的增大,重复次数非常可观,如何优化呢?

Memoization 记忆法(也称备忘录)是一种优化技术,通过存储函数调用结果(通常比较昂贵),当再次出现相同的输入(子问题)时,就能实现加速效果,改进后的代码

public class Fibonacci {
    public static void main(String[] args) {
        Scanner cin = new Scanner(System.in);
        for (int i = 1; i < 30; i++) {
            System.out.println(pruning(i));
        }

    }力kou

    //进行剪枝
    public static int pruning(int n){
        int[] cache = new int[n+1];
        Arrays.fill(cache,-1);
        cache[0] = 0;
        cache[1] = 1;
        return f(n,cache);
    }

    public static int f(int n,int[] cache) {

        if (cache[n] != -1){
            return cache[n];
        }

        cache[n] = f(n - 1,cache) + f(n - 2,cache);

        return cache[n];
    }
}

在这儿我们使用了一个数组去保存了重复的运算结果。

image-20221213173225807

2、链表操作的递归

在使用递归进行链表的操作时,希望大家牢记这句话:

在链表的递归过程中以单个结点操作的思想递归

a、206. 反转链表 - 力扣(LeetCode)
    /**
     * 递归实现反转链表,在同一个链表上反转
     *
     * @param head 待反转链表
     * @return 反转后的新头节点
     */
    public ListNode reverseList(ListNode head) {

        //递归结束条件
        if (head == null || head.next == null) {
            return head;
        }

        ListNode list = reverseList2(head.next);

        //操作链表进行反转
        head.next.next = head;
        head.next = null;

        System.out.println(list);
        return list;
    }
b、203. 移除链表元素
    /**
     * 使用递归的方法进行删除指定数据
     *
     * @param head 待处理链表
     * @param val 待删除值
     * @return 处理完成的链表
     */
    public  ListNode removeElements(ListNode head, int val) {
        if(head == null){
            return null;
        }

        if(head.val == val){
            //如果是删除该节点那么就相当于返回下一个节点的递归结果,
            // 此时上一个节点就会避开当前节点,而去链接下一个节点
            return removeElements1(head.next,val);
        }else {
            //当前节点链接到后面的链表
            head.next = removeElements1(head.next,val);
            return head;
        }

    }
c、19. 删除链表的倒数第 N 个结点
    /**
     * 使用递归的方法
     * @param head
     * @param n
     * @return
     */
    public  int recursion(ListNode head, int n){
        if(head == null){
            return 0;
        }

        int nth = recursion(head.next,n);//下一个节点的位置

        if (nth == n){
            //判断出下一个节点的的位置刚好是需要被删除的节点
            head.next = head.next.next;
        }

        return nth+1;   //当前节点的位置
    }
d、83. 删除排序链表中的重复元素
    /**
     * 使用递归的方法
     *
     * @param head 待处理链表
     * @return 处理完成的链表
     */
    public static ListNode deleteDuplicates1(ListNode head) {

        if (head == null || head.next == null) {
            return head;
        }

        if (head.val == head.next.val) {
            return deleteDuplicates1(head.next);
        } else {
            head.next = deleteDuplicates1(head.next);
            return head;
        }
    }
e、82. 删除排序链表中的重复元素 II
    /**
     * 使用递归的方法
     *
     * @param head 待处理链表
     * @return 处理完成的链表
     */
    public  ListNode deleteDuplicates(ListNode head) {
        if (head == null || head.next == null) {
            return head;
        }

        if (head.val == head.next.val) {

            //如果一直相同则不停移动指针,一直到找到不相同的节点为止
            ListNode t = head.next.next;
            while (t != null && t.val == head.val) {
                t = t.next;
            }

            return deleteDuplicates(t);
        } else {
            head.next = deleteDuplicates(head.next);
            return head;
        }
    }
f、21. 合并两个有序链表
    /**
     * 使用递归的方法进行合并链表
     *
     * @param list1
     * @param list2
     * @return 返回添加以后的链表
     */
    public  ListNode mergeTwoLists1(ListNode list1, ListNode list2) {

        if(list1 == null){
            return list2;
        }else if (list2 == null){
            return list1;
        }

        if(list1.val < list2.val){
            list1.next = mergeTwoLists1(list1.next,list2);
            return list1;
        }else {
            list2.next = mergeTwoLists1(list1,list2.next);
            return list2;
        }

    }
g、23. 合并 K 个升序链表
    /**
     * 合并两个有序链表
     *
     * @param list1
     * @param list2
     * @return
     */
    public static ListNode mergeTwoLists(ListNode list1, ListNode list2) {

        if (list1 == null) {
            return list2;
        } else if (list2 == null) {
            return list1;
        }

        if (list1.val < list2.val) {
            list1.next = mergeTwoLists(list1.next, list2);
            return list1;
        } else {
            list2.next = mergeTwoLists(list1, list2.next);
            return list2;
        }
        

    }

    /**
     * 合并K个有序链表
     *
     * @param lists
     * @return
     */
    public  ListNode mergeKLists(ListNode[] lists) {
        if (lists.length == 0) {
            return null;
        }
        return split(lists, 0, lists.length-1);
    }

    /**
     * 进行拆分利用分治的思想(类似于快排)
     *
     * @param listNodes
     * @param i 左值
     * @param j 右值
     * @return 返回两个链表合并的结果
     */
    public static ListNode split(ListNode[] listNodes, int i, int j) {

        if(i == j){
            return listNodes[i];
        }

        int t = (i + j) / 2;    //中间值
        ListNode left = split(listNodes,i,t);
        ListNode right = split(listNodes,t+1,j);

        return mergeTwoLists(left,right);
    }

如果你看到这里,并且上手敲了这几道题,我相信你对于递归一定有自己的理解了。递归今天就暂时学到这吧,“To Iterate is Human, to Recurse, Divine”,你距离God又近了一步。

img-1

七、最后的最后——力扣 2698. 求一个整数的惩罚数

自己动手练练吧!2698. 求一个整数的惩罚数

image-20231027185302409

这个题比较难,多动手画一下递归过程。

    public  int punishmentNumber(int n) {
        int sum = 0;
        for (int i = 1; i <= n; i++) {
            if (check(i * i, i)) {
                sum += i * i;
            }
        }
        return sum;
    }

    public  boolean check(int n, int i) {
        if (n == i) {
            return true;
        }

        int k = 10;

        /**
         *   判断数据是否可以进行拆分,可以拆分的条件为:数字应该大于10并且拆分后的尾数应该小于基准数
         *   例如:121与11,可以拆分为1和21,此时n>k符合条件,
         *   但是n%k = 21,大于11,不可能出现这样的情况符合条件
         */
        while (n >= k && n % k <= i) {
            /*
                将拆分后的数据进行比较,例如121拆分为12与1
                此时n/10 = 12,n%k = 1。得到i-(n%k) = 11
                判断出12 + 1 != i
             */
            if (check(n / k, i - (n % k))) {
                return true;
            }

            //依次从个位,百位....开始拆分。
            //例如121,第一次拆分为1,21;第二次为12,1
            k *= 10;
        }
        return false;
    }

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

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

相关文章

电脑下载视频号视频:微信视频号如何下载到电脑桌面上?

很多人做视频的时候需要将视频号的视频下载到电脑上&#xff0c;该如何操作呢&#xff1f;接下来我们通过电脑下载视频号视频希望对大家有所帮助。 方法一&#xff1a;使用在线视频下载网站 有一些在线视频下载网站可以帮助你从视频号上下载视频到电脑。你只需将视频的链接复制…

三个禁止使用U盘的方案

三个禁止使用U盘的方案 安企神U盘管理系统下载使用 U盘是一种小型便携式的存储设备&#xff0c;可以方便地传输和存储各种类型的数据&#xff0c;但也因此带来了一些安全隐患。在一些特殊的场合&#xff0c;如公司、商业场地等&#xff0c;为了保护内部数据被盗取外泄&#x…

怎么才能写好宣传软文?媒介盒子为你揭秘

数字化时代改变了企业的宣传方式&#xff0c;软文成为企业宣传的主要方式&#xff0c;但是企业真的会写宣传软文吗&#xff1f;为什么宣传软文不起效果&#xff1f;下面媒介盒子先和大家分享宣传软文的写作技巧 一、为什么要宣传 企业为什么需要宣传&#xff0c;主要有三类情况…

JS实现商品SKU

<!DOCTYPE html> <html> <head><title>商品SKU</title><link rel"stylesheet" href"element/css/element.css"><style>*{ margin:0; padding:0px; box-sizing: border-box; }ul,li{ list-style-type: none;}bod…

Python武器库开发-高级特性篇(八)

高级特性篇(八) 高阶函数 Python作为一门高级编程语言&#xff0c;拥有着强大的函数式编程能力。其中高阶函数就是Python函数式编程的重要组成部分。在Python中&#xff0c;函数可以被当作变量一样进行操作&#xff0c;包括作为参数传递给其他函数&#xff0c;或者作为返回值…

DBSCAN算法c++实现

首先计算出距离每个点e以内的点&#xff0c;如果个数>minPts,则找出它的直接密度可达和间接可达的点&#xff0c;用visited标记点是否已经在簇中&#xff0c;循环直到最后一个点。 #include <fstream> #include <vector> #include <iostream> #include &…

Leetcode.树形DP

目录 543.二叉树的直径 124.二叉树中的最大路径和 2246.相邻字符不同的最长路径 543.二叉树的直径 用递归来写 考虑 树形DP 维护以当前节点为根节点的最大值&#xff0c;同时返回给父节点经过当前节点的最大链的长度&#xff0c;这有个trick 当遍历到空节点的时候返回-1 递归…

mq消费并发排队及幂等机制实现

一&#xff0c;解决什么问题及实现方案 主要为了解决并发访问排队执行及重复下发保证幂等的问题 实现技术方案 如何实现并发访问时&#xff0c;请求排队执行方法。Redssion 如何实现MQ重复下发时&#xff0c;保证幂等。redis 先插入一个分布式锁的话题&#xff0c;如下&am…

LeetCode 740.删除并获得点数---->打家劫舍

前言&#xff1a;简单写写自己对这道题的拙见&#xff0c;如有意见或者建议可以联系笔者owo 首先&#xff0c;看看完整题目描述&#xff1a; 给你一个整数数组 nums &#xff0c;你可以对它进行一些操作。 每次操作中&#xff0c;选择任意一个 nums[i] &#xff0c;删除它并获…

归并排序(java)

大家好我是苏麟 , 今天说说归并排序 . 归并排序 递归 正式学习归并排序之前&#xff0c;我们得先学习一下递归算法。 定义&#xff1a; 定义方法时&#xff0c;在方法内部调用方法本身&#xff0c;称之为递归. public void show(){System.out.println("aaaa")…

SEACALL海外呼叫中心系统的优势包括

SEACALL海外呼叫中心系统的优势包括 封卡封号问题解决 海外呼叫中心系统通过API开放平台能力&#xff0c;定制电话营销系统&#xff0c;提供多项功能如自动拨打、智能应答、真人语音交互等&#xff0c;帮助企业克服员工离职率高、客户资源流失严重等挑战。 - 高级管理者操控 …

《实现领域驱动设计》

DDD入门 1.1 DDD是什么&#xff1f; DDD是一种软件开发方法 DDD将领域专家和开发人员聚集到一起&#xff0c;开发的软件能够反映出领域专家的思维模型。目标是&#xff1a;交付最具业务价值的软件。DDD关注业务战略&#xff1a;指引我们如何实现面向服务架构&#xff08;ser…

开题报告怎么写?-案例+模板保姆级)

以前导师让们带本科生开题报告&#xff0c;我深知开题报告在学术研究中的重要性。一个出色的开题报告能够展示学生的科学思维、研究能力和创新潜力。 在本篇博客中&#xff0c;我将为大家详细介绍如何撰写史上最强开题报告。 将从课题的 科学意义国内外研究概况和发展趋势应…

SourceTree 使用

如何拉取远程仓库&#xff1f;如何拉去远程分支&#xff1f;如何创建本地分支&#xff1f;如何删除本地分支&#xff1f;如何删除远端分支&#xff1f; 删除了远程分支&#xff0c;如果本地还有此分支&#xff0c;那么是可以通过推送本地分支来还原远端分支。如何合并本地分支&…

2023中国计算机大会:蚂蚁集团连发两支百万级科研基金

10月26日&#xff0c;中国计算机学会&#xff08;CCF&#xff09;主办的第二十届中国计算机大会(CNCC2023)在沈阳举行。在“CCF-蚂蚁科研基金及产学研合作交流活动”上&#xff0c;蚂蚁集团发布了2023年度“CCF-蚂蚁科研基金”绿色计算及隐私计算两支百万级专项基金&#xff0c…

动态代理IP怎么设置?动态代理IP有哪些应用场景?

动态代理IP是指代理服务器会根据实际IP地址的变化而变化&#xff0c;可以帮助用户隐藏真实的IP地址&#xff0c;同时可以在不同的网络环境下使用不同的代理IP地址。下面是动态代理IP的设置和应用场景&#xff1a; 一、动态代理IP怎么设置&#xff1f; 1. 手动设置代理IP地址 用…

基于Arduino的物流分拣控制系统设计

欢迎大家点赞、收藏、关注、评论啦 &#xff0c;由于篇幅有限&#xff0c;只展示了部分核心代码。 技术交流认准下方 CSDN 官方提供的联系方式 文章目录 概要 一、控制系统设计1.1系统方案1.2 系统工作原理1.3方案设计1.3.1快递检测电路方案设计1.3.2控制电路方案设计 二、硬件…

【Java基础】File类与IO流

File类与IO流 文章目录 File类与IO流1. java.io.File类的使用1.1 概述1.2 构造器1.3 作用 2. IO流原理及流的分类2.1 流的分类2.3 流的API 3. 节点流之一&#xff1a;FileReader\FileWriter3.1 Reader与Writer3.3 关于flush&#xff08;刷新&#xff09; 4. 节点流之二&#xf…

GPIO常见名词——推挽、开漏、浮空、上拉、下拉、高阻态

&#x1f64c;秋名山码民的主页 &#x1f602;oi退役选手&#xff0c;Java、大数据、单片机、IoT均有所涉猎&#xff0c;热爱技术&#xff0c;技术无罪 &#x1f389;欢迎关注&#x1f50e;点赞&#x1f44d;收藏⭐️留言&#x1f4dd; 获取源码&#xff0c;添加WX 目录 1、前言…

VTK OrientationMarker 方向 三维坐标系 相机坐标轴 自定义坐标轴

本文 以 Python 语言开发 我们在做三维软件开发时&#xff0c;经常会用到相机坐标轴&#xff0c;来指示当前空间位置&#xff1b; 坐标轴效果&#xff1a; 相机方向坐标轴 Cube 正方体坐标轴 自定义坐标轴&#xff1a; Code&#xff1a; Axes def main():colors vtkNamedC…