【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

相关推荐

小米13怎么在锁屏显示步数
beat365网页登录

小米13怎么在锁屏显示步数

2025-08-25 👁️ 1681
王者荣耀5级铭文需要多少钱
beat365网页登录

王者荣耀5级铭文需要多少钱

2025-08-28 👁️ 2344
😀郇斯楠U19世界杯首秀8盖帽后 收获NCAA D1西北大学Offer
365流水不够不能提现

😀郇斯楠U19世界杯首秀8盖帽后 收获NCAA D1西北大学Offer

2025-07-08 👁️ 2017
抖音老梗是什么梗_是什么意思
beat365网页登录

抖音老梗是什么梗_是什么意思

2025-09-18 👁️ 921
最显脚小的6双鞋子,都在这里了!
beat365网页登录

最显脚小的6双鞋子,都在这里了!

2025-08-07 👁️ 4752
如何在支付宝花呗中进行额度提现:详细步骤和注意事项