写递归函数的正确思维方法

递归思想入门-回归解决问题最简单的版本 递归思维实质上体现了一种逆向推理的逻辑方式,即从最终结果回溯至初始条件。在一步步的倒推过程中,我们发现每一步要解决的问题本质上都是一样的。通过将复杂的大问题分解为多个相似的小规模子问题,进而揭示出解决问题的最基本单元,为寻找最简化的解决方案提供了有效途径。接下来,本篇文章通过几个例子带你感受下利用递归思维解决问题的魔力。 阅读详情


什么是递归

简单的定义: “当函数直接或者间接调用自己时,则发生了递归.” 说起来简单, 但是理解起来复杂, 因为递归并不直观, 也不符合我们的思维习惯, 相对于递归, 我们更加容易理解迭代. 因为我们日常生活中的思维方式就是一步接一步的, 并且能够理解一件事情做了N遍这个概念. 而我们日常生活中几乎不会有递归思维的出现.
举个简单的例子, 即在C/C++中计算一个字符串的长度. 下面是传统的方式, 我们一般都这样通过迭代来计算长度, 也很好理解.

size_t length(const char *str) {
  size_t length = 0;
  while (*str != 0) {
    ++length;
    ++str;
  }

  return length;
}

而事实上, 我们也可以通过递归来完成这样的任务.

size_t length(const char *str) {
  if (*str == 0) {
    return 0;
  }
  return length(++str) + 1;
}

只不过, 我们都不这么做罢了, 虽然这样的实现有的时候可能代码更短, 但是很明显, 从思维上来说更加难以理解一些. 当然, 我是说假如你不是习惯于函数式语言的话. 这个例子相对简单, 稍微看一下还是能明白吧.
迭代的算法可以这样描述: 从第一个字符开始判断字符串的每一个字符, 当该字符不为0的时候, 该字符串的长度加一.
递归的算法可以这样描述: 当前字符串的长度等于当前字符串除了首字符后, 剩下的字符串长度+1.
作为这么简单的例子, 两种算法其实大同小异, 虽然我们习惯迭代, 但是, 也能看到, 递归的算法无论是从描述上还是实际实现上, 并不比迭代要麻烦.

理解递归

在初学递归的时候, 看到一个递归实现, 我们总是难免陷入不停的回溯验证之中, 因为回溯就像反过来思考迭代, 这是我们习惯的思维方式, 但是实际上递归不需要这样来验证. 比如, 另外一个常见的例子是阶乘的计算. 阶乘的定义: “一个正整数的阶乘(英语:factorial)是所有小于或等于该数的正整数的积,并且0的阶乘为1。” 以下是Ruby的实现:

def factorial(n) 
  if n <= 1 then
    return 1
  else
    return n * factorial(n - 1)
  end
end

我们怎么判断这个阶乘的递归计算是否是正确的呢? 先别说测试, 我说我们读代码的时候怎么判断呢?
回溯的思考方式是这么验证的, 比如当n = 4时, 那么factoria(4)等于4 * factoria(3), 而factoria(3)等于3 * factoria(2), factoria(2)等于2 * factoria(1), 等于2 * 1, 所以factoria(4)等于4 * 3 * 2 * 1. 这个结果正好等于阶乘4的迭代定义.
用回溯的方式思考虽然可以验证当n = 某个较小数值是否正确, 但是其实无益于理解.
Paul Graham提到一种方法, 给我很大启发, 该方法如下:

  1. 当n=0, 1的时候, 结果正确.
  2. 假设函数对于n是正确的, 函数对n+1结果也正确.
    如果这两点是成立的,我们知道这个函数对于所有可能的n都是正确的。

这种方法很像数学归纳法, 也是递归正确的思考方式, 事实上, 阶乘的递归表达方式就是1!=1,n!=(n-1)!×n(见wiki). 当程序实现符合算法描述的时候, 程序自然对了, 假如还不对, 那是算法本身错了…… 相对来说, n,n+1的情况为通用情况, 虽然比较复杂, 但是还能理解, 最重要的, 也是最容易被新手忽略的问题在于第1点, 也就是基本用例(base case)要对. 比如, 上例中, 我们去掉if n <= 1的判断后, 代码会进入死循环, 永远不会结束.

使用递归

既然递归比迭代要难以理解, 为啥我们还需要递归呢? 从上面的例子来看, 自然意义不大, 但是很多东西的确用递归思维会更加简单……
经典的例子就是斐波那契数列, 在数学上, 斐波那契数列就是用递归来定义的:

·F0 = 0
·F1 = 1 
·Fn = Fn – 1 + Fn – 2

有了递归的算法, 用程序实现实在再简单不过了:

def fibonacci(n)
  if n == 0 then
    return 0
  elsif n == 1 then
    return 1
  else
    return fibonacci(n - 1) + fibonacci(n - 2)
  end
end

改为用迭代实现呢? 你可以试试.
上面讲了怎么理解递归是正确的, 同时可以看到在有递归算法描述后, 其实程序很容易写, 那么最关键的问题就是, 我们怎么找到一个问题的递归算法呢?
Paul Graham提到, 你只需要做两件事情:

  1. 你必须要示范如何解决问题的一般情况, 通过将问题切分成有限小并更小的子问题.
  2. 你必须要示范如何通过有限的步骤, 来解决最小的问题(基本用例).
    如果这两件事完成了, 那问题就解决了. 因为递归每次都将问题变得更小, 而一个有限的问题终究会被解决的, 而最小的问题仅需几个有限的步骤就能解决.

这个过程还是数学归纳法的方法, 只不过和上面提到的一个是验证, 一个是证明.
现在我们用这个方法来寻找汉诺塔这个游戏的解决方法.(这其实是数学家发明的游戏)

有三根杆子A,B,C。A杆上有N个(N>1)穿孔圆盘,盘的尺寸由下到上依次变小。要求按下列规则将所有圆盘移至C杆:
1.每次只能移动一个圆盘.
2.大盘不能叠在小盘上面.

汉诺塔

这个游戏在只有3个盘的时候玩起来较为简单, 盘越多, 就越难, 玩进去后, 你就会进入一种不停的通过回溯来推导下一步该干什么的状态, 这是比较难的. 我记得第一次碰到这个游戏好像是在大航海时代某一代游戏里面, 当时就觉得挺有意思的. 推荐大家都实际的玩一下这个游戏, 试试你脑袋能想清楚几个盘的情况.
现在我们来应用Paul Graham的方法思考这个游戏.

一般情况:
当有N个圆盘在A上, 我们已经找到办法将其移到C杠上了, 我们怎么移动N+1个圆盘到C杠上呢? 很简单, 我们首先用将N个圆盘移动到C上的方法将N个圆盘都移动到B上, 然后再把第N+1个圆盘(最后一个)移动到C上, 再用同样的方法将在B杠上的N个圆盘移动到C上. 问题解决.

基本用例:
当有1个圆盘在A上, 我们直接把圆盘移动到C上即可.

算法描述大概就是上面这样了, 其实也可以看作思维的过程, 相对来说还是比较自然的. 下面是Ruby解:

def hanoi(n, from, to, other)
  if n == 1 then
    puts from + ' -> ' + to
  else
    hanoi(n-1, from, other, to)
    hanoi(1, from, to, other)
    hanoi(n-1, other, to, from)
  end
end

当n=3时的输出:

A -> C
A -> B
C -> B
A -> C
B -> A
B -> C
A -> C

上述代码中, from, to, other的作用其实也就是提供一个杆子的替代符, 在n=1时, 其实也就相当于直接移动. 看起来这么复杂的问题, 其实用递归这么容易, 没有想到吧. 要是想用迭代来解决这个问题呢? 还是你自己试试吧, 你试的越多, 就能越体会到递归的好处.

递归的问题

当然, 这个世界上没有啥时万能的, 递归也不例外, 首先递归并不一定适用所有情况, 很多情况用迭代远远比用递归好了解, 其次, 相对来说, 递归的效率往往要低于迭代的实现, 同时, 内存好用也会更大, 虽然这个时候可以用 尾递归 来优化, 但是尾递归并不是一定能简单做到.

什么是递归?如何在C语言中编写递归函数? 递归是一种问题解决方法,其中一个函数通过调用自身来解决问题,直到达到某个终止条件。递归在数学、计算机科学和编程中都有广泛的应用,因为它可以简化问题的描述和解决。自我调用:递归函数会在自己的执行过程中调用自身,这是递归的核心特征。通过这种方式,问题可以被分解为更小的子问题。基本情况:递归函数必须具有一个或多个基本情况(也称为终止条件),用于确定递归何时结束。没有终止条件,递归将无限地继续下去,导致栈溢出或无限循环。分解问题:递归函数通常将问题分解为更小、更简单的子问题。 阅读详情

相关推荐

群晖NAS实战:Python项目部署全流程解析

本文详细解析了在群晖NAS上部署Python项目的完整流程。从开启SSH、选择连接工具,到通过套件中心一键安装Python3、配置pip及国内镜像源,再到上传项目、安装依赖、后台运行及排错,提供了清晰的避坑指南。旨在帮助用户将群晖NAS从存储设备转变为可执行自定义任务的智能服务器,实现自动化脚本、数据监控等应用。

weixin_29057163的博客 593

函数递归(详解)

函数递归详解

m0_72536629的博客 1万+

银河麒麟高级服务器操作系统V10SP2(X86)PXE无人值守批量部署实战指南

本文详细介绍了银河麒麟高级服务器操作系统V10SP2(X86)PXE无人值守批量部署的实战指南。通过PXE网络引导技术,实现全自动化、高效率的系统部署,特别适用于企业级数据中心和国产化替代项目。文章涵盖基础环境准备、关键服务配置、Kickstart高级定制及部署测试等核心内容,帮助用户快速掌握批量部署技巧。

weixin_42525637的博客 383

递归函数详解

文章目录1. 什么是递归函数2. 递归求阶乘3. 递归的进入4. 递归的退出5. 递归的条件 1. 什么是递归函数 一个函数在它的函数体内调用它自身称为递归调用,这种函数称为递归函数。执行递归函数将反复调用其自身,每调用一次就进入新的一层,当最内层的函数执行完毕后,再一层一层地由里到外退出。 一句话总结递归:自我调用且有完成状态 2. 递归求阶乘 举个例子,我们来计算阶乘n! = 1 x 2 x 3 x ... x n,用函数factorial(n)表示,可以看出: 所以,factorial(n)

Lianuuu的博客 2万+

告别双系统!用Ventoy把Ubuntu 22.04装进U盘,打造你的随身开发环境(附保姆级避坑指南)

本文详细介绍了如何使用Ventoy将Ubuntu 22.04安装到U盘,打造便携式开发环境。通过即插即用的特性,开发者可以在任何x86电脑上快速启动完整的Ubuntu系统,无需配置双系统或虚拟机。文章提供了从硬件准备到系统优化的全流程指南,帮助用户高效创建随身开发环境。

weixin_31569671的博客 364

Autowired的使用介绍

我们都知道在spring中@Autowired注解,是用来自动装配对象的。@Service@Autowired没错,这样是能够装配成功的,因为默认情况下spring是按照类型装配的,也就是我们所说的byType方式。此外,@Autowired注解的required。

白话机器学习 1350

函数递归(详细易懂)

函数递归

qq_74319491的博客 1万+

保姆级教程:用CST 2023一步步复现一个1-8GHz Vivaldi天线(含参数优化与结果分析)

本文提供了一份详细的CST 2023教程,指导读者如何从零开始设计并优化1-8GHz Vivaldi天线。内容涵盖工程设置、参数化建模、仿真加速技巧以及性能优化策略,特别适合需要实现宽带天线设计的工程师和研究人员。通过实战案例,帮助用户掌握Vivaldi天线的核心设计要点和CST高级功能应用。

weixin_30800987的博客 618

【转】写递归函数的正确思维方法

版权声明:本作品由九天雁翎创作,采用知识共享署名-非商业性使用 4.0 国际许可协议进行许可。http://www.jtianling.com https://blog.youkuaiyun.com/vagrxie/article/details/8470798 递归是编程中一个相对难以理解但是却又很重要...

每天都要有进步鸭! 342

〔重庆理工大学〕大学物理实验--变温霍耳效应及其参数测定

每个老师要求不同,实验报告内容和数据仅供参考。

Pretty Boy Fox的博客 3044

基于C++的MMC仿真与PSCAD建模

MMC是一种基于模块化设计的多电平换流器,通常由若干个基本单元(子模块)组成。每个子模块包含一个电容和两个开关,通过控制开关的开闭,实现输出电压的级联和多电平化。高效率:由于开关频率较低,开关损耗小,效率高。高可靠性:模块化设计使得故障模块易于更换,提高了系统的可靠性。灵活扩展:可以通过增加或减少子模块的数量,方便地调整输出电压和功率。PSCAD(Power Systems Computer Aided Design)是广泛应用于电力系统仿真的专业软件,具有强大的建模和仿真能力。

qq_38334677的博客 295

如何轻而易举的写出递归函数

专栏原创出处:github-源笔记文件 ,github-源码 ,欢迎 Star,转载请附上原文出处链接和本声明。 文章目录1. 递归的定义2. 如何写出一个递归函数2.1 寻找递归跳出条件2.2 分析逻辑,寻找递归等式(难点)2.3 优化递归函数2.4 改为循环3. 递归乘法的案例实战3.1 审题思路3.2 尝试递归3.3 尝试优化4. 再谈由上到下、由下到上5. 递归算法推荐参考 1. 递...

2.wa 2889

win10解决IE浏览器安装不上的问题(卸载依然显示不能安装)

目前win10 系统不支持外部ie安装程序,所以必须用win10自带的应用来安装 一、错误的卸载的方式 在控制面板的当前安装的程序界面是看不到ie的,需要选择控制面板左上角的启用或关闭windows功能 将ie11前面的勾勾去掉 这种方法是很多网站上的误区,其实这并没有卸载成功ie只是把ie的功能关闭了,但是他还在你的电脑上。你可以理解为我们说的对文件夹执行的隐藏文件夹功能。那个隐藏的文件夹依...

清平乐的技术专栏 3万+

如何写递归函数

一、递归的意思是函数自己调用自身(直接方式),或者在自己函数调用的下级函数中又调用自己(间接方式)。        递归可拆分为递和归两个过程,递就是向下层传递参数,归是指当函数不再向下传递时会一层一层的返回。 二、写递归函数需考虑的要素:        a.怎样取一个变量

4587

数学建模模型总结

这是本人在两年内准备参加数学建模竞赛搜集到的模型资料,已经经过了本人的筛选和整理,希望能够对准备参加数学建模竞赛的学弟学妹们提供一些帮助!

Statistics and Machine Learning Toolbox

Statistics and Machine Learning Toolbox(带书签),MATLAB统计学习工具

递归函数的设计方法

step3:联合step1和step2,证明由p(1) -> p(n)成立。->则 p(n) = p(n-1) +2n-1 = n^2成立。3.假设递归函数调用返回结果是正确的,实现本层函数逻辑。step2:验证p(k)成立,那么p(k+1)也成立。证明:1+3+5+......+2n-1 = n^2。第二步:假设:p(n-1) = (n-1)^2 成立。第三步:1,2步成立 ->成立。step1:验证p(1)成立。1.给递归函数一个明确的语义。第一步:p(1)=1,成立。2.实现边界条件的程序逻辑。

2603_96243894的博客 265

C# WinForm 基于DirectShow.dll编写的程序,可边预览边保存视频,音频和图片

基于微软的DirectShow.dll的二次开发1.自动识别连接的摄像头跟麦克风且可以对其进行选择(默认选中第一个)2.可录制wmv和avi两种格式的视频并且都带有音频,avi可以录制的分辨率和帧数进行选择,wmv默认进行了淸晰度优化如不喜欢可去除代码中有相关提示3.可在预览点开之后进行拍照,也可边录制视频边拍照4.添加了视频和音频的压缩功能(酌情使用)这次二次开发发现C#对DirectShow的相关信息太少,这次做完也不敢藏私共享出来,还望大家指正

递归函数:编程中的思维艺术

递归函数是一种通过自我调用来解决问题的编程技术,其核心在于将复杂问题分解为相似子问题。本文系统介绍了递归的基本概念、实现方法和应用场景,重点分析了Go语言中的递归实现,包括阶乘计算、斐波那契数列等经典案例。文章详细探讨了递归的终止条件、性能优化技术(如尾递归和记忆化)以及潜在问题(栈溢出风险),并提供了迭代转换等替代方案。通过目录遍历和归并排序等实例,展示了递归在实践中的典型应用。最后总结了递归的优缺点,并推荐了相关学习资源,为开发者全面掌握递归技术提供了实用指南。

weixin_46060074的博客 1105

java递归函数例子_Java递归函数详解附案例

递归函数在Java语言中得到了广泛的应用,它使得程序的编写过程更加的清晰明了。对Java初学者来讲,递归函数是需要学习的一个重要知识点。本文将附上案例和演示代码,具体为大家介绍递归函数的概念、要素以及和循环的区别。想要学习递归函数的小伙伴,赶紧一起来梳理下相关的知识点吧!1、递归函数的概念递归是一种针对使用简单的循环难以编程实现的问題,提供优雅解决方案的技术。简单来说,递归方法是调用自身的方法。在...

weixin_39907762的博客 1406
上一篇: C语言预处理
下一篇: C语言中的字符串与字符串函数
donCoder
博客等级 码龄9年 65粉丝 79原创
评论 1
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

当前余额3.43元 前往充值 >
需支付:10.00元
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付元
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值