什么是递归?
看ANSI Common Lisp手册,里面提到递归第二章:欢迎来到 Lisp — ANSI Common Lisp 中文版,说:不要把递归看作一个普通函数来理解,因为普通函数经常被当成一个“机器”,原料从入口进,从出口出,但是对递归就有点难理解,因为这个机器又调用了自己.....所以要把递归想像成一个处理过程,不管是“机器”还是函数,它都只是针对的输入物料或信息的一种处理过程。
好吧,我刚看到原话的时候有种醍醐灌顶的感觉,但是请原谅我贫瘠的语言无法把它准确描述出来。我可以举个例子,比如查字典,它的处理过程就是“先按照某种规则翻到某一页,在这一页里面查看是否有要查找的‘字’,如果有,查找结束,如果没有,那就继续这个处理过程” 。当然这个翻页规则可以有多种,比如从前向后翻,比如从后向前翻,比如每次选取页数的1/2 来说翻。
我将使用python和HY(lisp)源代码的方式来辅助自己的理解,也希望能对大家有所帮助。
ps,不要有负担,其实递归真的很简单,我每隔一段时间就会弄明白一次^-^。
设定任务查字典
结合对“处理过程”的理解,我们设定了一个简化的查字典的任务,即给出一个字符(也可以是字符串)和一个包含字符(串)的列表,从列表中找出匹配项,并把匹配项后面的元素一起打出来。
还可以增加倒查模式,即查到后把匹配项前面的元素一起打出来。
比如给出x = "c" ; ListB = ["a", "b", "c", "d"] ,那么从左边查找的返回值是["c", "d"],从右边查找的返回值是 ["a", "b", "c"]
编码实现查字符串
首先学习一下HY
HY的语法
不要有负担,HY语言不会也没有关系,看完有个印象就行。毕竟“处理过程”是LISP书里提到的,我们用HY lisp语言跑通例子会更优仪式感^-^。
Hy | Python |
---|---|
(setv foobar (+ 2 2)) (setv [tim eric] ["jim" "derrick"]) (setv alpha "a" beta "b") | foobar = 2 + 2 tim, eric = 'jim', 'derrick' alpha = 'a'; beta = 'b' |
(sorted "abcBC" :key (fn [x] (.lower x))) | sorted("abcBC", key = lambda x: x.lower()) |
(defn test [a b [c "x"] #* d] [a b c d]) | def test(a, b, c="x", *d): return [a, b, c, d] |
(with [o (open "file.txt" "rt")] (setv buffer []) (while (< (len buffer) 10) (.append buffer (next o)))) | with open('file.txt', 'rt') as o: buffer = [] while len(buffer) < 10: buffer.append(next(o)) |
(lfor x (range 3) y (range 3) :if (= (+ x y) 3) (* x y)) | [x * y for x in range(3) for y in range(3) if x + y == 3] |
(defmacro do-while [test #* body] `(do ~@body (while ~test ~@body))) (setv x 0) (do-while x (print "Printed once.")) | x = 0 print("Printed once.") while x: print("Printed once.") |
HY的安装
在python环境下,直接使用pip安装即可
pip install hy
安装完成后,命令行模式下直接键入“hy”即可进入hy的交互界面
hy
Hy 0.29.0 using CPython(main) 3.10.13 on FreeBSD
=> (print "hello world")
hello world
pyhton实现
任务需求:给出x = "c" ; ListB = ["a", "b", "c", "d"] ,那么从左边查找的返回值是["c", "d"],从右边查找的返回值是 ["a", "b", "c"]
python代码如下:
# 学习递归 从右边查找匹配项
def thirdright(x, ListA):
if not ListA :
return None
else:
if x == ListA[-1]:
return ListA
else:
return thirdright(x, ListA[:-1])
# 学习递归 从左边查找匹配项
def thirdleft(x, ListA):
if not ListA :
return None
else:
if x == ListA[0]:
return ListA
else:
return thirdleft(x, ListA[1:])
运行测试
x = "c" ; ListB = ["a", "b", "c", "d"]
y = thirdleft(x, ListB)
z = thirdright(x, ListB)
print(y,z)
输出结果:
['c', 'd'] ['a', 'b', 'c']
HY lisp代码实现
任务需求:给出x = "c" ; ListB = ["a", "b", "c", "d"] ,那么从左边查找的返回值是["c", "d"],从右边查找的返回值是 ["a", "b", "c"]
当然在HY里,赋值语句是(setv x "c") (setv ListB ["a" "b" "c" "d"])
从左向右查找HY代码:
(defn searchleft [x ListA]
(if (= ListA [])
None
(if (= (get ListA 0) x)
ListA
(searchleft x (cut ListA 1 100)))))
(setv x "c") (setv ListB ["a" "b" "c" "d"])
(searchleft x ListB)
输出
["c" "d"]
从左向右查找HY代码:
(defn searchright [x ListA]
(if (= ListA [])
None
(if (= (get ListA -1) x)
ListA
(searchright x (cut ListA 0 -1)))))
(setv x "c") (setv ListB ["a" "b" "c" "d"])
(searchright x ListB)
输出
=> (searchright x ListB)
["a" "b" "c"]
HY lisp代码确实完成了功能需求,除了有太多右括号“)))))”,程序代码整体看着还是挺清爽的。
HY里面没有常规lisp语言里的cdr和car两条指令,所以这里用了get 和cut两个函数,感觉不如cdr和car丝滑。
递归深度限制和尾递归
python不支持尾递归,且有递归深度限制
def recursion(n):
if n==1:
return n
else:
return n+recursion(n-1)
最大递归深度1000,所以数大点就报错了
recursion(1000)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "<stdin>", line 5, in recursion
File "<stdin>", line 5, in recursion
File "<stdin>", line 5, in recursion
[Previous line repeated 995 more times]
File "<stdin>", line 2, in recursion
RecursionError: maximum recursion depth exceeded in comparison
HY是基于python的,所以也有最大递归深度限制
(defn recursion [x]
(if (= x 1)
1
(+ x (recursion (- x 1)))))
(recursion 1000)
报错:RecursionError: maximum recursion depth exceeded in comparison
hy的尾递归代码没调通。
common Lisp的代码
(defun recursion (x)
(if (= x 1)
1
(+ x (recursion (- x 1)))))
3000的时候没问题,到3400的也是报错
(recursion 3400)
*** - Lisp stack overflow. RESET
sbcl尾递归
目前只有sbcl支持尾递归
(defun recursion-tail (x &optional (sum 0))
(if (= x 1)
(+ sum 1)
(recursion-tail (- x 1) (+ sum x))))
; 调用函数
(recursion-tail 1000)
在sbcl的尾递归里,可以轻松到五百万深度
* (recursion-tail 5000000)
12500002500000
证明尾递归优化确实了不起!
总结
递归就是行为规则,是一种处理过程,递归也是最符合人类原生行为的编程方法,所以即使很复杂的问题,也比较容易用递归来实现算法。比如汉诺塔,使用递归代码非常简洁,使用循环则代码复杂很多。
关于递归的资源消耗,确实要比普通函数多,不过很多编译器专门对递归进行了优化,许多Common Lisp 编译器(比如sbcl)都可以把尾递归转化成循环函数,这样就没有额外的资源消耗了。
再回过头来看看ANSI Common Lisp关于递归的描述,加强一下记忆
起初,许多人觉得递归函数很难理解。大部分的理解难处,来自于对函数使用了错误的比喻。人们倾向于把函数理解为某种机器。原物料像实参一样抵达;某些工作委派给其它函数;最后组装起来的成品,被作为返回值运送出去。如果我们用这种比喻来理解函数,那递归就自相矛盾了。机器怎可以把工作委派给自己?它已经在忙碌中了。
较好的比喻是,把函数想成一个处理的过程。在过程里,递归是在自然不过的事情了。日常生活中我们经常看到递归的过程。举例来说,假设一个历史学家,对欧洲历史上的人口变化感兴趣。研究文献的过程很可能是:
- 取得一个文献的复本
- 寻找关于人口变化的资讯
- 如果这份文献提到其它可能有用的文献,研究它们。
过程是很容易理解的,而且它是递归的,因为第三个步骤可能带出一个或多个同样的过程。
所以,别把
our-member
想成是一种测试某个东西是否为列表成员的机器。而是把它想成是,决定某个东西是否为列表成员的规则。如果我们从这个角度来考虑函数,那么递归的矛盾就不复存在了。
调试
hy里没有切片
=> ["a" "b"][0]
[0]
=> ["a" "b"][0:1]
Traceback (most recent call last):
File "stdin-ff769649939a083bf75b8d8e4be71e7a4f33d180", line 1, in <module>
["a" "b"][0:1]
NameError: name 'hyx_0XcolonX1' is not defined
可以用cut代替
=> (cut [1 2] 0)
[]
=> (cut [1 2 3] 1)
[1]
=> (cut [1 2 3] 2)
[1 2]
=> (cut [1 2 3] -1)
[1 2]
如果读取某个元素,更适合的是get
=> ListB
["a" "b" "c" "d"]
=> (get ListB 0)
"a"
=> (get ListB 1)
"b"
hy源码:GitHub - hylang/hy: A dialect of Lisp that's embedded in Python
手册:Contents — Hy 0.29.0 manual
指引:Tutorial — Hy 0.29.0 manual
代码报错parse error for pattern macro 'if': got unexpected token:
... (+ x (recursion (- x 1)))))
Traceback (most recent call last):
File "stdin-9598c437ea17b110b2200908cd5c44900ea2edef", line 4
(+ x (recursion (- x 1)))))
^
hy.errors.HySyntaxError: parse error for pattern macro 'if': got unexpected token: hy.models.Expression([
hy.models.Symbol('+'),
hy.models.Symbol('x'),
hy.models.Expression([
hy.models.Symbol('recursion'),
hy.models.Expression([
hy.models.Symbol('-'),
hy.models.Symbol('x'),
hy.models.Integer(1)])])]), expected: end of macro call
python递归报错
>>> recursion(1000)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "<stdin>", line 5, in recursion
File "<stdin>", line 5, in recursion
File "<stdin>", line 5, in recursion
[Previous line repeated 995 more times]
File "<stdin>", line 2, in recursion
RecursionError: maximum recursion depth exceeded in comparison
python有最大递归深度限制,最大1000
import sys
sys.getrecursionlimit()
1000
可以使用sys.setrecursionlimit进行设置。
hy尾递归代码报错
(defn recursion-tail [x &optional [sum 0]]
... (if (= x 1)
... (+ sum 1)
... (recursion-tail (- x 1) (+ sum x))))
=> (recursion-tail 1000)
Traceback (most recent call last):
File "stdin-db38a56499b0c167c6ac4f20a4f89229b594a496", line 1, in <module>
(recursion-tail 1000)
TypeError: recursion_tail() missing 1 required positional argument: 'hyx_XampersandXoptional'
先搁置