【Scheme】编程学习 (四) —— 递归

简介: Scheme 编程通常的使用方法为递归

在 Scheme 中函数的通常写法,the normal way to write functions in Scheme,通常会用到递归 (recursion),本节的主要内容为

  • Factorial 阶乘
  • Fibonacci 斐波那契
  • Countdown 倒计时
  • Map
  • Recursion "shapes" 不同形式的递归
  • Tail call optimisation 尾调用优化
  • Iterative forms
  • Discussion

为了更好的理解递归如何运行 (make it easier for understand how recursion works)

1. Factorial 阶乘

(define (fact n)
    (if (= 0 n)
        1
        (* n (fact (- n 1)))))

定义 fact 函数,只接受一个入参 n,当 n 为 0 时结束,其他情况调用 n 乘以 fact(n-1),此函数调用自身,即递归调用,类似的 C语言代码为:

int fact(int n)
{
   
    if (0 == n)
        return 1;
    else
        return n * fact(n-1);
}
> (fact 3)
; 6
> (fact 4)
; 24
> (fact 5)
; 120

2. Fibonacci 斐波那契数列

(define (fib n)
    (if (<= n 2)
        1
        (+ (fib (- n 1))
           (fib (- n 2)))))

此函数也是经典的斐波那契序列,经典的递归编程题,此函数写为 C语言为:

int fib(int n)
{
   
    if (n <= 2)
        return 1;
    else 
        return fib(n-1) + fib(n-2);
}
> (fib 3)
; 2
> (fib 4)
; 3
> (fib 5)
; 5
> (fib 6)
; 8

3. Countdown 倒计时

(define (countdown n)
    (if (= n 0)
        null
        (begin
            (display n) ; 打印 n 
            (newline) ; 换行
            (countdown (- n 1)))))

这是一个比较简单的递归,没太多需要讲解的内容,打印显示,换行,递归调用 入参减一。
begin 用于将后续的三个语句合在一起,类似于 C语言中的大括号,我们都知道 if / else 分支在不写大括号时,最多后跟一个语句。为了使 else 分支可以调用此三个语句,可以写为 C语言

void countdown(int n)
{
   
    if (n == 0)
        return;
    else
    {
   
        printf("%d", n);
        printf("\n");
        countdown(n - 1);
    }
}
> (countdown 4)
4
3
2
1
() ; null / empty list

4. Map

这次我们再来看一下 map 函数,map 是一个 built-in (内置)函数,这里是一个实现尝试,对表 lst 中的每个元素应用函数 fn,

(define (my-map fn lst)
    (if (null? lst)
        null
        (cons (fn (car lst))
            (my-map fn (cdr lst)))))

类似的 C语言函数可尝试写为

int abs(int a) {
    a >= 0 ? return a : return -a; }
int (*f)(int) = abs;

void my_map(int (*comp)(int), int arr[], int n)
{
   
    if (0 == n)
        return;
    else
    {
   
        arr[0] = comp(arr[0]);
        my_map(comp, arr+1, n-1);
    }
}
> (my-map abs (list 2 -3 4))
; (2 3 4)
int arr[] = {
   2, -3, 4};
int n = sizeof(arr);
my_map(f, arr, n);

5. Recursion "shapes" 递归的形式

5.1 factorial model

(define (fact n)
    (if (= 0 n)
        1
        (* n (fact (- n 1))))

让我们尝试一下 (fact 3) 看看内部发生了什么

(fact 3) ; 当我们调用 (fact 3) 时 内部为:
(* 3 (fact 2)); 接下来我们展开 (fact 2)
(* 3 (* 2 (fact 1))) ; 接着展开 (fact 1)
(* 3 (* 2 (* 1 (fact 0))));展开 (fact 0) 为 1
(* 3 (* 2 (* 1 1))); 先计算 (* 1 1) 结果为 1
(* 3 (* 2 1)); 再计算 (* 2 1) 结果为 2
(* 3 2); 计算 (* 3 2) 结果为 6
6

5.2 - fibonacci model

(define (fib n)
    (if (<= n 2)
        1
        (+ (fib (- n 1))
           (fib (- n 2)))))

调用斐波那契函数并设置 n 为 5

(fib 5)
(+ (fib 4) (fib 3)); 展开为 (fib 4) 和 (fib 3) 的和,再展开 (fib 4) 和 (fib 3)
(+ (+ (fib 3) (fib 2)) (+ (fib 2) (fib 1))); 为 (fib 3) (fib 2) 的和 与 (fib 2) (fib 1) 的和之和 
(+ (+ (+ (fib 2) (fib 1)) 1) (+ 1 1)); 展开 (fib 3) ,由于(fib 2) (fib 1) 结果为 1,替换
(+ (+ (+ 1 1) 1) 2); 全部转为具体的数字计算
(+ (+ 2 1) 2)
(+ 3 2)
5; 累加结果为 5

此种递归展开结构类似三角形

5.3 - countdown model

(define (countdown n)
    (if (= n 0)
        null
        (begin (display n) (newline)
            (countdown (- n 1)))))
(countdown 4)
(countdown 3)
(countdown 2)
(countdown 1)
(countdown 0)
()

it's a kind of uni-function,执行时 countdown 得到的形式类似单一函数

6. Tail call optimisation 尾调用优化

一般语言调用递归时 每次都会产生一个栈帧 (stack frame),在 scheme 中

  • The previous stack frame is no longer needed 前一个栈帧不再需要,当我们计算 (countdown 3) 时,我们不需要前一个栈帧 (countdown 4) 的任何信息
  • Throw it away 这时我们就可以将其丢弃掉

7. Iterative forms 迭代形式

7.1 - my-map model

(define (my-map fn lst)
    (if (null? lst)
        null
        (cons (fn (car lst))
              (my-map fn (cdr lst)))))
(my-map abs (list 2 -3 4)); 调用 my-map 展开 会创建一个点对,car 为对列表第一个元素应用 abs  
(cons (abs 2) (my-map abs (list -3 4))); 继续对剩余更少的元素进行 my-map
(cons 2 (cons (abs -3) (my-map abs (list 4)))); (abs 2) 的结果为 2
(cons 2 (cons 3 (cons (abs 4) (my-map abs null)))); (abs -3) 的结果为 3
(cons 2 (cons 3 (cons 4 null))); (my-map abs null) 会返回 null 
(cons 2 (cons 3 (list 4))); 根据之前的章节,这里会产生一个 list 
(cons 2 (list 3 4)); 继续合并为 (list 3 4)
(list 2 3 4)

the "shape" of my-map is again a kind of triangular form,my-map 的展开形式仍然是一种三角形格式

7.2 - iterative forms - fact

让我们尝试以不同的方式实现 fact ,为了避免这种三角形形式

(define (fact-it n)
    (define (impl acc count)
        (if (= 0 count)
            acc
            (impl (* count acc) (- count 1))))
    (impl 1 n))

acc 这里的意思为 accumulate 累计,上述代码中为累乘的结果,初值为 1 ,依次累乘 n,n-1,... 1
类似的 C++ 代码 (untested) 可以写作:

int fact_it(int n)
{
   
    auto impl = [](int acc, int count)-> int{
    
        if (0 == count) return acc;
        else return impl(count*acc, count-1);
    };
    return imp(1,n);
}
> (fact-it 3)
; 6
> (fact-it 4)
; 24
> (fact-it 5)
; 120

让我们看一下它的形式 (Shape)

(fact-it 3)
(impl 1 3)
(impl (* 3 1) (- 3 1))
(impl 3 2)
(impl (* 2 3) (- 2 1))
(impl 6 1)
(impl (* 1 6) (- 1 1))
(impl 6 0); 此时 count 为 0 ,返回 acc 即 6
6

7.3 - Iterative forms - fib

让我们看一下如何以同样的方式实现 fib

(define (fib-it n)
    (define (impl acc1 acc2 count)
        (if (= count 2)
            acc1
            (impl (+ acc1 acc2) acc1 (- count 1))))
    (impl 1 1 n)) ; 函数实际的函数体

acc1 acc2 为两个累加

> (fib-it 2)
; 1
> (fib-it 3)
; 2
> (fib-it 4)
; 3
> (fib-it 5)
; 5
(fib-it 5)
(impl (+ 1 1) 1 (- 5 1))
(impl 2 1 4)
(impl (+ 2 1) 2 (- 4 1))
(impl 3 2 3)
(impl (+ 3 2) 3 (- 3 1))
(impl 5 3 2); 当 n 为 2 时返回 acc1 即 5
5

此种形式的展开均为独立的计算,并不基于前一次的计算结果

我们可以以同样的方式优化 map 吗?

  • No, but "Tail call optimisation modulo cons" 不行,但是尾调用优化了 cons

原视频地址: https://www.bilibili.com/video/BV1Kt411R7Wf?p=4&spm_id_from=pageDriver

目录
相关文章
|
存储 C++
【Scheme】编程学习 (三) —— 闭包
本节主要讲述 Scheme 中闭包概念及使用
127 1
|
人工智能 算法 Java
【Scheme】编程学习(一) —— 概述
Scheme 是一种编程语言,为 Lisp 的一种变体,本文概述 Scheme 语言
277 0
|
自然语言处理 C语言 C++
【Scheme】编程学习 (二) —— 基础
Scheme 编程语言学习第二节基础
146 0
|
SQL AliSQL 安全
为更强大而生的开源关系型数据库来了!阿里云RDS for MySQL 8.0 上线!
阿里云RDS for MySQL 8.0上线,使得阿里云成为紧跟社区步伐,发布MySQL最新版本的云厂商。RDS for MySQL 8.0 产品是阿里云推出的 MySQL 系列云产品之一,使用完全兼容 MySQL 8.0 的阿里云 AliSQL 8.0 分支,除了官方在 MySQL 8.0 推出的全新功能外,AliSQL 沉淀了许多在 Alibaba 集团电商业务和云上几十万客户在使用 MySQL 过程中遇到的问题和需求,以此来加固AliSQL, 提升 AliSQL 的性能和稳定性。
567 0
为更强大而生的开源关系型数据库来了!阿里云RDS for MySQL 8.0 上线!
|
4月前
|
Java Maven
maven打包出现没有主类的原因,详细分析并解决
本文分析了使用Maven打包Java应用时找不到或无法加载主类的问题,通常是由于未配置主类或打包时未包含依赖,并通过添加Maven插件解决了依赖问题,同时指出了JavaFX应用可能遇到的运行时组件缺失的错误。
259 0
maven打包出现没有主类的原因,详细分析并解决
|
Java 程序员 应用服务中间件
离开了IDEA,你还会运行JAVA项目吗?
作为一名Java开发者,你还会用命令编译执行Java项目吗?工具固然方便,但是不能忘本,如果你忘了,一块来回忆吧。
1099 4
离开了IDEA,你还会运行JAVA项目吗?
|
Dart 算法 Java
你不必担心Dart的垃圾回收器(译)
在学习Flutter的过程中,我们知道Widget只是最终渲染对象(RenderObject)的配置文件,它会在build的时候频繁的销毁和创建,那么,我们不需要担心他的创建和销毁带来的性能问题吗? 其实大可不必,因为Dart针对Flutter的Widget的创建和销毁专门做过优化,这也是Flutter在多种语言中选择Dart的一个重要因素,甚至我们还可以刻意利用这一点。 下面这篇文章解析了Dart的GC(Garbage Collector),对它做了个翻译以及部分内容的解析,包括一些排版,有不对的地方大家多多指正。 原文地址:https://medium.com/flutter/flu
422 0
你不必担心Dart的垃圾回收器(译)
|
自然语言处理 编译器 程序员
带你读《计算机程序的构造和解释(原书第2版)典藏版》之一:构造过程抽象
《计算机程序的构造和解释(原书第2版)》1984年出版,成型于美国麻省理工学院(MIT)多年使用的一本教材,1996年修订为第2版。在过去的二十多年里,《计算机程序的构造和解释(原书第2版)》对于计算机科学的教育计划产生了深刻的影响。第2版中大部分重要程序设计系统都重新修改并做过测试,包括各种解释器和编译器。作者根据其后十余年的教学实践,还对其他许多细节做了相应的修改。
|
对象存储 开发者
对象OSS生命周期(LifeCycle)管理功能|学习笔记
快速学习对象 OSS 生命周期(LifeCycle)管理功能
2711 0
对象OSS生命周期(LifeCycle)管理功能|学习笔记
|
SQL 消息中间件 JSON
微服务开发系列——第一篇:项目搭建(保姆级教程)
本节实现目标 搭建ac-mall2-cloud微服务基础骨架。 搭建微服务子项目:mall-pom、mall-common、mall-member、mall-product。 MyBatis-Plus配置:雪花ID、创建时间/修改时间 自动填充。 单个微服务子项目Swagger配置及访问。 返回JSON数据日期格式化。 Swagger优化:mall-common支持多个微服务Swagger配置、Swagger传参(语言参数、token、测试账号)

热门文章

最新文章