在 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