大家好,日拱一卒,我是梁唐。本文始发于公众号:Coder梁
我们继续来肝伯克利CS61A,今天我们看的是作业8。这一次的作业有些特殊,不再是基于Python,而是一门全新的语言Lisp。
我之前没有接触过Lisp,还是这一次受到课程的影响看了一点。根据阮一峰大佬的说法,Lisp并非是一种编程语言,而是一种理论演算的思想。是从纯数学理论发展出的语言,而非为了某种编程开发的需要而设计的。因此虽然是诞生自上世纪五十年代,但至今仍未过时。
大家感兴趣可以去读一下阮一峰大佬的译文《为什么Lisp语言如此先进?》
这一次作业使用的是Lisp的一个方言版本——scheme,简单介绍一点作业当中会用到的语法。
语法
基本运算
Lisp最大的特点就是它代码的表示形式,和常规的语言完全不同。
比如4 + 5
的运算,在Lisp中写成:
(+ 4 5)
在Lisp中,所有的运算的外围都需要一个括号,先写运算符,再写运算需要的参数。
运算之间可以嵌套,比如:
(quotient (+ 8 7) 5)
这行代码表示(8 + 7) / 5
,所以答案就是3。这里的quotient
表示乘除。
Lisp的语法看起来之所以奇怪,是因为它本身就是以语法树的形式书写的。至于什么是语法树,感兴趣可以去看下编译原理,这里不做过多解释。
定义变量
在Lisp中想要定义变量,使用的是define
运算符。
比如我们要定义一个pi,让它等于3.14,写成:
(define pi 3.14)
定义函数
我们定义函数同样需要使用define
关键字,比如我们定义一个square
函数来计算一个数的平方,写出来就是这样:
(define (square x) (* x x))
这里的(square x)
是一个整体,表示函数名和接收的参数。之后括号里的内容是函数的计算过程。准确的说在Lisp当中,这不叫函数(function),而叫过程(procedure)。
过程这个名字有一些古老了,基本上只在一些上古编程语言中出现,现在主流的编程语言一般都叫函数或者是方法(method)。
那调用过程怎么写呢?其实很简单,和运算符是一样的,比如我们要计算5的平方,写出来就是:
(square 5)
pair和list
Lisp中pair的定义和C++有些类似,两个参数打包成为一个pair。
打包pair也有一个专门的操作符,叫做cons
。不要问我为啥是这个奇怪的名字,我也不知道……
(cons 1 2)
比如上面的代码,我们创建了一个(1 2)
的pair。
除了cons
之外还有三个常用的符号,分别是car
,cdr
和nil
。car
用来返回pair中的第一个元素,cdr
用来返回pair中的第二个元素,nil
表示一个空的list。
我们很容易可以想到,如果我们把多个pair像是链表那样一个一个地串起来,就可以表示一个序列了。是的这个就是Lisp语言中的list。
在链表当中链表的最后一个元素的next
指针指向的是空,在Lisp当中也有类似的要求。要求list中的最后一个pair的第二个值必须是nil
,否则也不会报错,但是输出的时候会多输出一个.
作为区分。
比如一个正常的list是这样的,我们遵守了规定,最后一个元素的第二个值放的是nil
。
如果我们不这么干,就会显示成这样:
表示这不是一个规范的list。
使用cons
一个一个嵌套非常麻烦,所以Lisp中专门提供了一个操作符叫做list,用来直接创建list。
符号
问题来了,我们怎么在Lisp当中区分变量和符号呢?
比如我们定义了两个变量:
(define a 1)
(define b 2)
假设我们想要单纯表示a
和b
这带个字符,应该怎么办呢?
我们可以使用单引号,它可以表示一个字符
(list 'a 'b)
得到的就是一个字符构成的list,(a b)
。
除此之外,单引号还能表示一个list:
'(1 2 3 4)`就相当于Python当中的`[1, 2, 3, 4]
好了,我们简单介绍了一下lisp的语法,可以来做作业了。
作业
Q1: Cadr and Caddr
实现两个过程cadr
和caddr
分别返回list中第二和第三个元素。
老师给了一个例子cddr
,类似于Python中的切片s[2:]
(define (cddr s)
(cdr (cdr s)))
要获取第二个元素很简单,我们先使用cdr
获取除开第一个元素之外的子串,再取一次car
获取子串中的第一个元素:
(define (cadr s)
'YOUR-CODE-HERE
(car (cdr s))
)
获取第三个元素也是类似,由于老师已经写好了cddr
我们可以直接使用cddr
:
(define (caddr s)
'YOUR-CODE-HERE
(car (cddr s))
)
Conditional expressions
在lisp当中,cond
语句可以表示多个if-else
的嵌套结构:
(cond
(<p1> <el1>)
(<p2> <el2>)
...
(<pn> <eln>)
(else <else-expressions>))
这里的p1
到pn
都是判断条件,el1
到eln
是执行语句。执行时会从p1
开始判断,当遇到为True
时执行对应的执行语句。如果都为False
,执行else
后的内容。
在Lisp中用#t
表示True
,#f
表示False
。这个作业中的Scheme是一个特别的版本,允许我们使用True
和False
。
Q2: Sign
使用cond
语句,实现sign
分段函数。当x > 0
时,sign(x) = 1
。当x = 0
时,sign(x) = 0
,当x < 0
时,sign(x) = -1
。
cond
语句小试牛刀,简单套用即可。
(define (sign x)
'YOUR-CODE-HERE
(cond
((> x 0) 1)
((= x 0) 0)
(else -1))
)
Q3: Pow
实现快速幂算法,在log n
时间内计算
提示:
- 可以使用
even?
来判断偶数,odd?
来判断奇数
这是一个经典的递归过程,如果不熟悉可以先写成Python代码,再转换成Lisp。
def square(x):
return x * x
def pow(b, n):
if n == 0:
return 1
if n % 2:
return b * square(pow(b, n // 2))
else:
return square(pow(b, n // 2))
改写成Lisp代码如下:
(define (square x) (* x x))
(define (pow b n)
(cond
((= n 0) 1)
((even? n) (square (pow b (quotient n 2))))
(else (* b (square (pow b (quotient n 2)))))
)
)
Q4: Ordered
实现过程ordered?
来判断list是否是不下降的。
1 2 3 3 4
这是一个不下降的list,而下面这个不是:
1 2 3 3 2
也是简单的递归,判断(car s)
和(cadr s)
是否存在,如果存在判断(car s) > (cadr s)
,如果为真,那么返回False
,否则递归调用(cdr s)
(define (ordered? s)
'YOUR-CODE-HERE
(cond
((null? s) True)
((null? (cdr s)) True)
((> (car s) (cadr s)) False)
(else (ordered? (cdr s)))
)
)
Q5: No Dots!
实现过程nodots
,接收一个可能不符合Lisp中规范的嵌套的list,在list中元素不变的前提下返回一个符合规范的嵌套的list。
一个没有以null
结尾的list会被视为是不规范的,在展示时会显示一个.
比如(cons 1 (cons 2 3))
会展示成:(1 2 . 3)。
经过nodots
之后,会得到(1 2 3)
提示:我们可以使用null?
判断是否是nil
,使用pair?
判断是否是pair
这道题看着不难,但实际上手做还是挺麻烦的。麻烦的点在于list中的元素也可以是一个list。所以只是保证list最后是nil
是不够的,list中间也可能存在list,这些list也要进行处理。
怎么处理呢?没有别的办法,只能使用递归。
绕不清楚还是可以先写成Python:
class List:
def __init__(self, first, second):
self.first = first
self.second = second
def nodots(s):
if s is None:
return s
elif not isintance(s, List):
return List(s, None)
else:
first = s.first
if isinstance(s.first, List):
first = nodots(first)
return List(first, nodots(s.second))
翻译过来代码如下:
(define (nodots s)
'YOUR-CODE-HERE
(cond
((null? s) s)
((not (pair? s)) (cons s nil))
(else (cons
(cond
((pair? (car s)) (nodots (car s)))
(else (car s))
)
(nodots (cdr s))
)
)
)
)
Sets as Ordered Lists
之前的课程讲了如何使用一个链表来实现Set
这个数据结构,简单来说就是维护链表中元素的有序性,来实现Set
的一些功能。
这里我们要做的是使用Lisp中的list来实现Set
,没有看过视频也没有关系,我们直接来看题就行。
Q6: Contains
实现Contains
函数,用来判断元素是否在Set
当中。
老师很贴心地给我们准备了对应的Python代码,我们只要将Python代码翻译成Lisp实现即可。
def empty(s):
return s is Link.empty
def contains(s, v):
if empty(s):
return False
elif s.first > v:
return False
elif s.first == v:
return True
else:
return contains(s.rest, v)
(define (empty? s) (null? s))
(define (contains? s v)
(cond ((empty? s) #f)
((= (car s) v) True)
((> (car s) v) False)
(else (contains? (cdr s) v))
)
)
Q7: Add
实现add
方法,读入一个set s
和一个值 v
。使得在v
不在s
中时,往s
中插入v
。
这也是一道递归的问题,递归的逻辑也不复杂,我们先根据链表的第一个元素判断,如果v
小于头元素,那么将链表向右移动一位。如果v
和头部元素相等,说明v
已经在链表当中。如果v
小于头部元素,说明v
不在链表当中,需要插入,插入的位置就在当前位置之前。
写成Python代码就是:
def add(s, v):
if s is None:
return List(v, None)
elif s.first == v:
return s
elif s.first > v:
return List(s.first, List(v, s.second))
else:
return List(s.first, add(s.second, v))
翻译成Lisp为:
(define (add s v)
(cond ((empty? s) (list v))
((= (car s) v) s)
((> (car s) v) (cons v s))
(else (cons (car s)(add (cdr s) v)))
)
)
Q8: Intersect and Union
最后实现交际和并集的操作,计算两个Set
的交集和并集。这两个函数的逻辑在之前的课程当中都讲过,并且这道题当中老师也给出了Python代码:
def intersect(set1, set2):
if empty(set1) or empty(set2):
return Link.empty
else:
e1, e2 = set1.first, set2.first
if e1 == e2:
return Link(e1, intersect(set1.rest, set2.rest))
elif e1 < e2:
return intersect(set1.rest, set2)
elif e2 < e1:
return intersect(set1, set2.rest)
把这段代码翻译成Lisp即可:
(define (intersect s t)
(cond
((or (empty? s) (empty? t)) nil)
((= (car s) (car t)) (cons (car s) (intersect (cdr s) (cdr t))))
((< (car s) (car t)) (intersect (cdr s) t))
((> (car s) (car t)) (intersect s (cdr t)))
)
)
并集的逻辑和交集类似,唯一的不同是交集需要保留所有的元素,所以在返回的时候需要带上之前的结果。
(define (union s t)
(cond ((empty? s) t)
((empty? t) s)
((= (car s) (car t)) (cons (car s) (union (cdr s) (cdr t))))
((< (car s) (car t)) (cons (car s) (union (cdr s) t)))
((> (car s) (car t)) (cons (car t) (union s (cdr t))))
)
)
到这里,这一次的作业8就讲完了。
单纯从难度上来说,这些题目的难度并不算高,大部分都在之前的课程当中讲过。只不过由于要我们通过Lisp实现,刚一上手难免觉得别扭、不熟悉,这也是非常正常的。Lisp语法虽然看起来奇怪,但是写起来却并不难,并且仔细观察一下代码会发现其实是比Python的代码来得更短的。