【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

目录
相关文章
|
6月前
|
机器学习/深度学习 算法 C语言
详细介绍递归算法在 C 语言中的应用,包括递归的基本概念、特点、实现方法以及实际应用案例
【6月更文挑战第15天】递归算法在C语言中是强大力量的体现,通过函数调用自身解决复杂问题。递归涉及基本概念如自调用、终止条件及栈空间管理。在C中实现递归需定义递归函数,分解问题并设定停止条件。阶乘和斐波那契数列是经典应用示例,展示了递归的优雅与效率。然而,递归可能导致栈溢出,需注意优化。学习递归深化了对“分而治之”策略的理解。**
127 7
|
7月前
|
机器学习/深度学习 算法 C语言
【新手解答9】深入探索 C 语言:递归与循环的应用2
【新手解答9】深入探索 C 语言:递归与循环的应用2
71 0
|
7月前
|
算法 C语言 C++
【新手解答8】深入探索 C 语言:递归与循环的应用
【新手解答8】深入探索 C 语言:递归与循环的应用
87 0
|
存储 C++
【Scheme】编程学习 (三) —— 闭包
本节主要讲述 Scheme 中闭包概念及使用
111 1
|
IDE 编译器 Linux
你应该搞懂的 C 语言头文件路径问题
聊聊系统路径位置,绝对路径与相对路径,正斜杠 `/` 与 反斜杠 `\` 使用说明 ...... by 矜辰所致
348 0
你应该搞懂的 C 语言头文件路径问题
|
自然语言处理 C语言 C++
【Scheme】编程学习 (二) —— 基础
Scheme 编程语言学习第二节基础
134 0
|
人工智能 算法 Java
【Scheme】编程学习(一) —— 概述
Scheme 是一种编程语言,为 Lisp 的一种变体,本文概述 Scheme 语言
234 0
|
存储 算法 Go
周而复始,往复循环,递归、尾递归算法与无限极层级结构的探究和使用(Golang1.18)
所有人都听过这样一个歌谣:从前有座山,山里有座庙,庙里有个和尚在讲故事:从前有座山。。。。,虽然这个歌谣并没有一个递归边界条件跳出循环,但无疑地,这是递归算法最朴素的落地实现,本次我们使用Golang1.18回溯递归与迭代算法的落地场景应用。
周而复始,往复循环,递归、尾递归算法与无限极层级结构的探究和使用(Golang1.18)
|
机器学习/深度学习 自然语言处理 算法
|
程序员 测试技术 Scala
使用递归的思想去思考和编程 | 学习笔记
快速学习使用递归的思想去思考和编程