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

2023-05-16


什么是递归

简单的定义: “当函数直接或者间接调用自己时,则发生了递归.” 说起来简单, 但是理解起来复杂, 因为递归并不直观, 也不符合我们的思维习惯, 相对于递归, 我们更加容易理解迭代. 因为我们日常生活中的思维方式就是一步接一步的, 并且能够理解一件事情做了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时, 其实也就相当于直接移动. 看起来这么复杂的问题, 其实用递归这么容易, 没有想到吧. 要是想用迭代来解决这个问题呢? 还是你自己试试吧, 你试的越多, 就能越体会到递归的好处.

递归的问题

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

参考

  1. Ansi Common Lisp
  2. 精通递归程序设计 
  3. http://blog.csdn.net/vagrxie/article/details/8470798

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

写递归函数的正确思维方法 的相关文章

随机推荐

  • 4种方法可以解决远程连接时出现的蓝屏问题

    其他一切应用是不受影响的 xff0c 网站依旧可以打开 xff0c FTP依旧可以上传下载 xff0c 不属于紧急故 障 解决办法1 xff1a 远程桌面连接 gt 选项 gt 高级 gt 去掉 34 主题 34 和 34 位图缓存 34
  • ARM64 ubuntu20.04根文件系统制作

    ARM64 ubuntu20 04根文件系统制作 虚拟机环境搭建创建镜像文件官网下载ubuntu base切换根文件系统安装工具包安装桌面环境 netplan配置添加用户卸载没用的软件ubuntu修改串口实现自动登录 关闭自动休眠 虚拟机环
  • 无法打开kernel32.lib msvcprt.lib等问题

    解决方法 xff1a 在VC的库包含目录里面 xff0c 库目录 lib 添加 xff1a LibraryPath
  • centos7.6远程图形桌面开启和VNC连接

    centos7 6远程图形桌面开启和VNC连接 xff08 一 xff09 安装 yum install tigervnc tigervnc xff1b yum install tigervnc tigervnc server y 搞什么鬼
  • Mac配置sublime text3+python3+brew+boost+cmake+kenlm环境

    1 首先安装python3 xff0c 配置python3环境 下载python3 7 1安装包 xff0c 链接 https pan baidu com s 1JaPaoUCGNeYj60gATpb9eg 密码 0mh6 将python3
  • Centos7安装Python3.7详细教程

    Centos7安装Python3 7详细教程 注 xff1a 如果安装更高的python版本 xff0c 只需修改wget 后面的地址即可 xff0c 然后注意执行命令时候的路径等问题 如 xff1a 安装python3 7 5 则 xff
  • 多用户同时修改同一条数据(并发修改数据)

    如果两个用户同时打开一条记录 xff0c 修改后提交会产生更新冲突 办法有三 xff1a 1 打开同时锁定表的记录 2 用lock对修改方法加锁 2 捕获错误 xff0c 撤消其中一个用户的修改 场景描述如下 xff1a 用户A B同时打开
  • Go 语言入门很简单:Go 实现简易Web应用

    前言 截止到目前为止 xff0c 几乎我们的 Go 入门文章都是在终端运行的 在终端运行的代码或者运用运用程序只适合自己在环境搭好的环境下使用 也就是说 xff0c 如果用户没有安装 Go 语言环境 xff0c 是根本没法运行我们所写的 G
  • 【待解决】使用su或sudo出现Segmentation fault

    一台服务器上 xff0c 使用sudo会出现Segmentation fault xff0c 见下 xff1a 使用root登录后 xff0c 使用su命令 xff0c 一样的会出现Segmentation fault 暂时还未找到答案 相
  • go换源|go更换国内源

    Windows 版本 SETX GO111MODULE on go env w GOPROXY 61 https goproxy cn direct SETX GOPROXY https goproxy cn direct Linux 版本
  • linux安装go环境并配置国内源

    linux安装go环境并配置国内源 第一步 官网下载安装包 https golang google cn go1 4 linux amd64 tar gz 第二步 解压缩 tar C usr local xzf go1 4 linux am
  • python - 获取时间戳(10位和13位)

    在python 开发web程序时 xff0c 需要调用第三方的相关接口 xff0c 在调用时 xff0c 需要对请求进行签名 需要用到unix时间戳 在python里 xff0c 在网上介绍的很多方法 xff0c 得到的时间戳是10位 而j
  • curl命令模拟post请求发送json格式数据

    以下代码可以作为测试接收请求的程序直接复制使用 xff1a from flask import Flask request app 61 Flask name 64 app route 39 service 39 methods 61 39
  • pip换源 -pip更换国内镜像源

    更换pip源到国内镜像 pip国内的一些镜像 阿里云 http mirrors aliyun com pypi simple 中国科技大学 https pypi mirrors ustc edu cn simple 豆瓣 douban ht
  • 使用python的requests 发送multipart/form-data 请求

    发送post请求 1 r 61 requests post 34 http pythontab com postTest 34 data 61 34 key 34 34 value 34 以上得知 xff0c post请求参数是以data关
  • SHELL - shell 脚本获取本机ip并将ip复制给变量待用

    bin bash VAR 61 34 eth0 34 HOST IP 61 ifconfig VAR grep 34 inet addr 34 awk 39 print 2 39 awk F 39 print 2 39 echo HOST
  • shell - sed匹配某一行开头,替换整行内容

    sed i 39 cloud server ip ccloud server ip 61 update skyeye 360safe com 39 name txt
  • caffe安装系列——安装cuda和cudnn

    博客新址 http blog xuezhisd top 邮箱 xff1a xuezhisd 64 126 com 说明 网上关于caffe的安装教程非常多 xff0c 但是关于每一步是否操作成功 xff0c 出现了什么样的错误又该如何处理没
  • caffe安装系列——安装OpenCV

    博客新址 http blog xuezhisd top 邮箱 xff1a xuezhisd 64 126 com 说明 网上关于caffe的安装教程非常多 xff0c 但是关于每一步是否操作成功 xff0c 出现了什么样的错误又该如何处理没
  • 写递归函数的正确思维方法

    什么是递归 简单的定义 当函数直接或者间接调用自己时 xff0c 则发生了递归 说起来简单 但是理解起来复杂 因为递归并不直观 也不符合我们的思维习惯 相对于递归 我们更加容易理解迭代 因为我们日常生活中的思维方式就是一步接一步的 并且能够