日拱一卒,伯克利教你Lisp,CS61A作业8

740 阅读9分钟

大家好,日拱一卒,我是梁唐。本文始发于公众号:Coder梁

我们继续来肝伯克利CS61A,今天我们看的是作业8。这一次的作业有些特殊,不再是基于Python,而是一门全新的语言Lisp。

我之前没有接触过Lisp,还是这一次受到课程的影响看了一点。根据阮一峰大佬的说法,Lisp并非是一种编程语言,而是一种理论演算的思想。是从纯数学理论发展出的语言,而非为了某种编程开发的需要而设计的。因此虽然是诞生自上世纪五十年代,但至今仍未过时。

公开课视频

作业原文说明

Github

大家感兴趣可以去读一下阮一峰大佬的译文《为什么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,cdrnilcar用来返回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)

假设我们想要单纯表示ab这带个字符,应该怎么办呢?

我们可以使用单引号,它可以表示一个字符

(list 'a 'b)

得到的就是一个字符构成的list,(a b)

除此之外,单引号还能表示一个list:

'(1 2 3 4)`就相当于Python当中的`[1, 2, 3, 4]

好了,我们简单介绍了一下lisp的语法,可以来做作业了。

作业

Q1: Cadr and Caddr

实现两个过程cadrcaddr分别返回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>))

这里的p1pn都是判断条件,el1eln是执行语句。执行时会从p1开始判断,当遇到为True时执行对应的执行语句。如果都为False,执行else后的内容。

在Lisp中用#t表示True#f表示False。这个作业中的Scheme是一个特别的版本,允许我们使用TrueFalse

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时间内计算bnb^n

提示:

  1. b2k=(bk)2b^{2k}=(b^k)^2
  2. b2k+1=b(bk)2b^{2k+1}=b*(b^k)^2
  3. 可以使用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的代码来得更短的。