算法一看就懂之「 递归 」

之前的文章咱们已经聊过了「 数组和链表 」「 堆栈 」「 队列 」,今天咱们来看看「 递归 」,当然「 递归 」并不是一种数据结构,它是很多算法都使用的一种编程方法。它太普遍了,并且用它来解决问题非常的优雅,但它又不是那么容易弄懂,所以我特意用一篇文章来介绍它。

一、「 递归 」是什么?

递归 就是指函数直接或间接的调用自己,递归是基于栈来实现的。递归的经典例子就是 斐波拉契数列(Fibonacci)。一般如果能用递归来实现的程序,那它也能用循环来实现。用递归来实现的话,代码看起来更清晰一些,但递归的性能并不占优势,时间复杂度甚至也会更大一些。

上图为 斐波拉契数列 图例。

要实现递归,必须满足2个条件:

  1. 可调用自己

    就是我们要解决的这个问题,可以通过函数调用自己的方式来解决,即可以通过将大问题分解为子问题,然后子问题再可以分解为子子问题,这样不停的分解。并且大问题与子问题/子子问题的解决思路是完全一样的,只不过数据不一样。因此这些问题都是通过某一个函数去解决的,最终我们看到的就是不停得函数调用自己,然后就把问题化解了。

    如果这个问题不能分解为子问题,或子问题的解决方法与大问题不一样,那就无法通过递归调用来解决。

  2. 可停止调用自己

    停止调用的条件非常关键,就是大问题不停的一层层分解为小问题后,最终必须有一个条件是来终止这种分解动作的(也就是停止调用自己),做递归运算一定要有这个终止条件,否则就会陷入无限循环。

下面还是以 斐波拉契数列(Fibonacci)为例,我们来理解一下递归:

斐波拉契数列就是由数字 1,1,2,3,5,8,13…… 组成的这么一组序列,特点是每位数字都是前面相邻两项之和。如果我们希望得出第N位的数字是多少?

  1. 可以使用循环的方式求解:

    这里就不列代码了,思路是:我们知道最基本的情况是 f(0)=0,f(1)=1,因此我们可以设置一个一个循环,循环从i=2开始,循环N-1次,在循环体内 f(i)=f(i-1)+f(i-2),直到i=N-1,这样循环结束的时候就求出了f(N)的值了。

  2. 更优雅的方式是使用递归的方式求解:

    我们知道斐波拉契数列的逻辑就是:

    可以看出,这个逻辑是满足上面2个基本条件,假如求解 f(3),那 f(3)=f(2)+f(1),因此我们得继续去求解f(2),而 f(2)=f(1)+f(0),因此整个求解过程其实就在不断的分解问题的过程,将大问题f(3),分解为f(2)和f(1)的问题,以此类推。既然可以分解成子问题,并且子问题的解决方法与大问题一致,因此这个问题是满足“可调用自己”的递归要求。

    同时,我们也知道应该在何时停止调用自己,即当子问题变成了f(0)和f(1)时,就不再需要往下分解了,因此也满足递归中“可停止调用自己”的这个要求。

    所以,斐波拉契数列问题可以采用递归的方式去编写代码,先看图:

    我们将代码写出来:

    int Fb(int n){   if(n<=1) return n==0?0:1;   return Fb(n-1)+Fb(n-2); //这里就是函数自己调用自己}

    从上面的例子可以看出,我们写递归代码最重要的就是写2点:

  3. 递推公式

    上面代码中,递推公式就是 Fb(n)=Fb(n-1)+Fb(n-2),正是这个公式,才可以一步步递推下去,这也是函数自己调用自己的关键点。因此我们在写递归代码的时候最首先要做的就是思考整个逻辑中的递推公式。

  4. 递归停止条件

    上面代码中的停止条件很明显就是:if(n<=1) return n==0?0:1;这就是递归的出口,想出了递推公司之后,就要考虑递归停止条件是啥,没有停止条件就会无限循环了,通常递归的停止条件是程序的边界值。

    我们对比实现斐波拉契数列问题的2种方式,可以看出递归的方式比循环的方式在程序结构上更简洁清晰,代码也更易读。但递归调用的过程中会建立函数副本,创建大量的调用栈,如果递归的数据量很大,调用层次很多,就会导致消耗大量的时间和空间,不仅性能较低,甚至会出现堆栈溢出的情况。

    我们在写递归的时候,一定要注意递归深度的问题,随时做好判断,防止出现堆栈溢出。

    另外,我们在思考递归逻辑的时候,没必要在大脑中将整个递推逻辑一层层的想透彻,一般人都会绕晕的。大脑很辛苦的,我们应该对它好一点。我们只需要关注当前这一层是否成立即可,至于下一层不用去关注,当前这一层逻辑成立了,下一层肯定也会成立的,最后只需要拿张纸和笔,模拟一些简单数据代入到公式中去校验一下递推公式对不对即可。

二、「 递归 」的算法实践?

我们看看经常涉及到 递归 的 算法题(来源leetcode)

算法题:实现 pow(x, n) ,即计算 x 的 n 次幂函数。

说明:    -100.0 < x < 100.0    n 是 32 位有符号整数,其数值范围是 [−2^31, 2^31 − 1]

示例:输入: 2.00000, 10输出: 1024.00000

解题思路:

方法一:暴力解法,直接写一个循环让n个x相乘嘛,当然了这种方式就没啥技术含量了,时间复杂度O(1),代码省略了。

方法二:基于递归原理,很容易就找出递推公式 f(n)=x*f(n-1),再找出递归停止条件即n==0或1的情况就可以了。不过稍微需要注意的是,因为n的取值可以是负数,所以当n小于0的时候,就要取倒数计算。代码如下:class Solution {    public double myPow(double x, int n) {        if(n==0) return 1;        if(n==1) return x;        if(n<0) return 1/(x*myPow(x,Math.abs(n)-1));        return x*myPow(x,n-1);    }}这个方法其实也有问题,当n的数值过大时,会堆栈溢出的,看来也是不最佳解,继续往下看。

方法三:利用分治的思路,将n个x先分成左右两组,分别求每一组的值,然后再将两组的值相乘就是总值了。即 x的n次方 等于 x的n/2次方 乘以 x的n/2次方。以此类推,左右两组其实还可以分别各自继续往下分组,就是一个递推思想了。但是这里需要考虑一下当n是奇数的情况,做一个特殊处理即可,代码如下:class Solution {    public double myPow(double x, int n) {        //如果n是负数,则改为正数,但把x取倒数        if(n<0) {            n = -n;            x = 1/x;        }        return pow(x,n);

    }

    private double pow(double x, int n) {        if(n==0) return 1;        if(n==1) return x;        double half = pow(x,n/2);        //偶数个        if(n%2==0) {            return half*half;        }        //奇数个        return half*half*x;    }}这种方法的时间复杂度就是O(logN)了。

以上,就是对数据结构中「 递归 」的一些思考。

码字不易啊,喜欢的话不妨转发朋友,或点击文章右下角的“在看”吧。??

本文原创发布于微信公众号「 不止思考 」,欢迎关注。涉及 思维认知、个人成长、架构、大数据、Web技术 等。

原文地址:https://www.cnblogs.com/jsjwk/p/11496795.html

时间: 2024-10-03 18:41:15

算法一看就懂之「 递归 」的相关文章

「递归」求第n个斐波纳契数

用「递归」方法求第n个斐波纳契数 1 #include<stdio.h> 2 long int dog(int p) 3 { 4 if(p>1) 5 return dog(p-1)+dog(p-2); 6 else if (p==1||p==0) 7 return 1; 8 } 9 int main() 10 { 11 printf("您要求第几个斐波纳契数:\n"); 12 int n; 13 scanf("%d",&n); 14 pri

分布式系统关注点——99%的人都能看懂的「熔断」以及最佳实践

当我们工作所在的系统处于分布式系统初期的时候,往往这时候每个服务都只部署了一个节点. 如果想学习Java工程化.高性能及分布式.深入浅出.微服务.Spring,MyBatis,Netty源码分析的朋友可以加我的Java高级交流:854630135,群里有阿里大牛直播讲解技术,以及Java大型互联网技术的视频免费分享给大家. 那么在这样的背景下,如果某个服务A需要发布一个新版本,往往会对正在运行的其它依赖服务A的程序产生影响.甚至,一旦服务A的启动预热过程耗时过长,问题会更严重,大量请求会阻塞,产

「递归」拆分整数

12.11第一步:拆分整数为正整数之和的个数 1 #include<stdio.h> 2 int qw(int m,int n) 3 { 4 int i,count=0; 5 if (m==n||n==1) 6 return 1; 7 else if (m>n) 8 { 9 for(i=n;i>=1;i--) 10 count=count+qw(m-n,i); 11 return count; 12 } 13 else if(m<n) 14 return 0; 15 } 16

给人工智能「好奇心」会变成什么样?答案不出所料

如果赋予人工智能好奇心,它会去做些什么?科学家们尝试用「好奇心」来驱动人工智能自主学习.这群工程师想知道,在没有人类事先提供引导时,人工智能的「好奇心」会使它对什么产生兴趣?他们在七月发表的研究结果显示,有好奇心的人工智能会永无止尽地看电视. 让「好奇心」驱使人工智能学习目前常见的人工智能在运作上,都需要人类先给予一些原始数据才能开始.比如说,要让Google人工智能帮你翻译,最一开始得先告诉它,不同语言中的哪些字汇具有相同意涵:脸书的人脸辨识系统在自动标记你时,仰赖的是早就上传过的有你在内的照

为什么学习C语言这么久,看的懂代码,做不出题没项目

我看得懂别人的程序,可是我自己却写不出来,我应该怎么办啊?你了解这些嘛? 你只是能从别人书写的代码知道每一步都做些什么吧? 你明白别人的解题思路吗? 你知道别人为什么要用那样的算法吗? 如果你看着题目,你能写出实现同一功能的代码吗? 你能知道别人在写这个程序的过程中会遇到什么样的问题吗? 你能在看了别人的程序之后写出比他好的代码吗? 你能用另一种算法写出实现同一程序的代码吗? 你真的能看懂别人的程序吗?创一个小群,供大家学习交流聊天如果有对学C++方面有什么疑惑问题的,或者有什么想说的想聊的大家

从有限状态机的角度去理解Knuth-Morris-Pratt Algorithm(又叫KMP算法,”看毛片“算法)

转载请加上:http://www.cnblogs.com/courtier/p/4273193.html 在开始讲这个文章前的唠叨话: 1:首先,在阅读此篇文章之前,你至少要了解过,什么是有限状态机,什么是KMP算法,因为,本文是从KMP的源头,有限状态 机来讲起的,因为,KMP就是DFA(Deterministic Finite Automaton)上简化的. 2:很多KMP的文章(有限自动机去解释的很少),写得在我看来不够好,你如果,没有良好的数学基础就很难去理解他们(比如下图), 因为,你

看得懂的 Node.js(三)—— Express 启航

如果看过上一篇<看得懂的 Node.js>,就会发现手动搭建一个 web 服务器还是比较繁琐 而 express 就是一个可以极大地提高开发效率的 web 开发框架 一.创建项目 在 express 4.0 之前,我们使用 npm install -g express 来全局安装 express 但是 4.0 之后,express 的命令行工具被单独分离出来,叫做 express-generator npm install -g express-generator 如果了解过 vue,expr

一看就懂的Android APP开发入门教程

一看就懂的Android APP开发入门教程 作者: 字体:[增加 减小] 类型:转载 这篇文章主要介绍了Android APP开发入门教程,从SDK下载.开发环境搭建.代码编写.APP打包等步骤一一讲解,非常简明的一个Android APP开发入门教程,需要的朋友可以参考下 工作中有做过手机App项目,前端和android或ios程序员配合完成整个项目的开发,开发过程中与ios程序配合基本没什么问题,而android各种机子和rom的问题很多,这也让我产生了学习android和ios程序开发的

只有重庆人才看得懂的笑话!

一外省男,进重庆的饭店,点了个鱼香茄子,于是发生下面一段话 "老板,老板!!" "啥子事哦?" "你这鱼香茄子咋没得鱼呢?" "鱼香茄子本来就没得鱼嘛!" "没得鱼干嘛叫鱼香茄子呢?" "日你个先人板板-照你娃这么说,如果你要点个"虎皮青椒",老子还得给你弄张老虎皮不成?:点个"老婆饼",老子还给你发老婆不?:你P人点个"夫妻肺片",我不