Common Lisp 数据结构实现

特殊符号

宏:#

类似于其他语言里面的反斜杠转义字符,该符号用于标记特殊意义的部分。

  • #\ : 紧跟字符字符或者字符的名称,如 #\a
  • #' : 紧跟函数名,如 #'equal = function EQUAL
  • #b#o#x : 表示二进制、八进制、十六进制
  • # : 紧跟的是一个向量 vector

存储表示

Lisp 中一个cons cell 表示一个有两个指针的内存区域。通常可以如下表示


Cons cell

图中,箭头表示的是指针。在Lisp 的最常见的 List 数据结构中,左指针指向是 (car list),右指针指向的是 (cdr list)

(a b c d)

也可以这样表示


另外一种表示

如果 list 中有 其他list 元素,例如((a b) c (d e)),则可以这样表示

((a b) c (d e))

如果一个cons cell 里面的每个指针都指向⚛ 原子元素,那么可以近似地表示为dotted pair (a . b).该数据结构产生自(cons 'a 'b) 操作.

(cons 'a 'b)

注意: 这和 (a b) 表示的区别(setq x '(a b c))(setq y '(a b c))在内存里将会以两个list 存储但是(setq x 'a)(setq y 'b) 在内存里面的表示形式则是

image.png

注意: 任何 list 都可以用dotted pair 形式表示。例如 (a b c d) 可以写成(a . (b . (c . (d . nil)))),但是输出显示的时候还是(a b c d).但是请注意的是,(a . (b . (c . d)))在输出时将会显示为(a b c . d).

如果cons cell 为 (setq x ‘a ‘(d e)) 形式,则应该表示为


image.png

对比(setq y ‘(a d e))的结构则是


image.png

再看看(setq x ‘(a))的表示,和(setq x ‘a)有一定差别.


image.png

以下是(cons ‘a ‘(b))的表示,和(cons ‘a ‘b)也有一定的区别


image.png

等于函数

  1. =: 参数为数字
  2. equal: 参数为数据对象.判断标准为值相等.
  3. eql: 参数为数据对象.判断标准为引用相同.
  4. eq: 同上. 效率: eq > eql > equal. 以下程序示例将会说明使用的区别:
    image

比较数字的需要注意的是: �equal 不关注对象是否有相同的存储结构,只是关心其值是否相等,但是 eq 就严格很多,需要比较值是否相等和存储结构是否相等.

常见数据对象的表示

Tables

Tables 是高频率使用的用于组织信息的数据结构。通常,Tables 在� Lisp 中的表示方式有很多种,最常见的是用 list 嵌套 list。例如,现在有一个电话本,有名字和电话号码两个字段,可以这样写

(setq table1 '((jones 1573 2 330 660)
               (adams 1408 3 250 750)
               (mitsu 4321 5 100 500)))
(defun print-table(tt)
    (cond ((null tt) nil)
           (t print (car tt)
           (print-table (cdr tt)))))

可以使用一些函数获取 table 里面的数据.例如 assoc 函数

(assoc 'jones table1) ==> (JONES 1573 2 330 660)
(assoc 'smith table1) ==> NIL ;smith is not in table1

使用 tables 常常涉及 list 的操作 , 所以需要注意常用的两个函数 copy-alistcopy-list的区别。

copy-alist 使用示例

(setq org1 '(new-york (state capital) albany))
(setq copy1 (copy-alist org1))
;; org1 ==> (NEW-YORK (STATE CAPITAL) ALBANY)
;; copy1 ==> (NEW-YORK (STATE CAPITAL) ALBANY)

存储表示为如下:
image.png
image.png

如果对 copy1 中的元素做出更改

image.png

可以查看一下更改的结果
image.png

可以发现的是:copy-alist 的复制会创建一个相同的空间并赋值,两者的修改是不相关的,是相互独立的。

copy-list 使用示例

(setq org2 '(a b (c d) e))
(setq copy2 (copy-list org2))
;; org2 ==> (A B (C D) E)
;; copy2 ==> (A B (C D) E)

存储表示为:
image.png

如果修改 copy2

image.png

可以发现的是:copy-list 复制一个 list 会为 list 的顶级元素创建空间,但是嵌套的 list 使用引用进行存储,所以如果修改的是 copy2 的顶级元素不会影响到原来的 list 但是如果修的是嵌套的元素则会导致原 list 中的元素造成改变。

Pair Lists

Lisp 中数据常常以 dotted pairs的形式存储于 ALIST 中。让我们以一个电话☎️清单的例子来了解一下从这种 list中存取数据的系统函数。

(setq phones '((jones . 1234)
               (adams . 1235)
               (mitsu . 1236)))
(assoc 'jones phones)
;; ==> (JONES . 1234) 根据获取 dotted pair 的前部分获取整个dotted pair

(rassoc 1234 phones)
;; ==> (JONES . 1234) 根据获取 dotted pair 的后部分获取整个dotted pair

(acons 'harman 1237 phones)
;;==> ((HARMAN . 1237) (JONES . 1234) (ADAMS .1235)(MITSU . 1236))
;;使用 acons 增加dotted pair

;; `acons` 增加 dotted pair 的函数

(pairlis '(meyer messla brindisi)
         '(1238  1239   1230) phones)

;; `pairlis` 批量增加函数
;; ==> ((MEYER . 1238)(MESSLA . 1239)(BRINDISI . 1230)(HARMAN . 1237) (JONES . 1234) (ADAMS .1235)(MITSU . 1236))

;; `pairlis` 也能用来创建一个 pair lists
(setq new-list (pairlis '(a b c a b a d) '(1 2 3 4 5 6 7)))
;; ==> ((A . 1)(B . 2)(C . 3)(A . 4)(B . 5)(A . 6)(D . 7))

;; 以下需要特别注意:
(assoc 'a new-list);;==> (A . 1)
(rassoc 2 new-list);;==> (B . 2)
;; `assoc` 和 `rassoc` 只会返回第一个匹配参数的 `dotted pair`

Property Lists

Property Lists (PLIST) 与 Lisp 中每一个符号息息相关,这个 list 存储了描述符号的特定属性的元素。当用户创建一个 symbol 的时候,系统就会自动为其分配一个 PLIST ,如果用户不自行赋值,将会默认为 NIL。不同的 Lisp 方言的 PLIST 有区别。 下面首先将会介绍一些 Common Lisp 中关于 PLIST 的系统函数。

  • setf类似于 setqset,但是作用域更广,使用方法类似于 setq,用于变量赋值. setf 函数的存在让 common lisp 里面的许多函数变的冗余,但是为了程序的兼容性,那些函数依然被保留了。
(setf x 5)
(setf y '(a b c d))
(setf a 1 b 2 c 3 d 4)

;; setf 也有 rplaca 以及 rplacd 的功能
;; y ==> (A B C D)
(setf (car y) 2) ==> (2 B C D)
(setf (cdr y) 3) ==> (2 . 3)
;;y ==> (2 . 3)

image.png
  • getf: 接收两个参数,一个是 symbol 一个是 property name。语法为: (getf <plist> <item> )
(setf values '(x 1 y 2 z 3))
(getf values 'y) ;;==> 2
(getf values 'w) ;;==> NIL

  • get: 接收两个参数,但是 get 操作的是 symbol 而不是 PLISTS
(setf (get 'maxwell 'sex) 'male) ;; ==> MALE
(setf (get 'maxwell 'age) 35) ;; ==> 35
(setf (get 'maxwell 'job) 'coder) ;; ==> CODER

(symbol-plist 'maxwell) ;; ==> ((JOB CODER AGE 35 SEX MALE))
(get 'maxwell 'age) ;;==> 35

;; 注意这里不能使用 getf 因为 maxwell 不是一个 list

(setf (get 'was 'pos) 'verb)
(setf (get 'was 'type) 'be)
(setf (get 'was 'num) 'sing)
(setf (get 'was 'tense) 'past)
(setq dict nil)
(setq dict (cons 'was dict))

image.png

还有一些函数,如 remprop: 用于移除属性名称以及属性的值 get-properties: 可以返回一个 list 中从参数开始的新 list。注意它的第一个参数必须是有个 PLIST

Characters and Strings

Common lisp 中可以用 #<character> 表示字符。不同的 Common lisp 实现可能出现返回值为原来字符或者对应 ASCII 码对应十进制数字的情况。 Lisp 的众多方言达成了一致,使用 96 个核心字符集。这里面包含了两个特殊字符 空格 (#\space) 、 换行 (#\newline) 以及 94 个键盘上出现的可打印字符。 字符处理相关函数:

;;;;创建 / 操作字符
(character "a") ;; ==> #\a 或者 97 根据实现
(character "A") ;; ==> #\A
(character 'a) ;; ==> #\A 或者 65 (A 的 ASCII 码 )
(character 97) ;; ==> #\a
(setq x #\a)
(code-char x) ;; ==> #\a
(char-upcase #\a) ;; ==> #\A 或者 65
(char-downcase #\A) ;; ==> #\a 或者 97

;;;; 字符比较
(alpha-char-p #\a) ;; ==> T
(alpha-char-p 'a) ;; 'a 不是一个字符
                  ;; ’a 是一个符号

(alpha-char-p "a") ;; ==> NIL "a" 是一个 string
(upper-case-p #\A) ;; ==> 是否是大写字符
(digit-char-p #\a) ;; ==> 是否是数字字符

字符串是使用引号标记的由字符组成的一维数组或者向量 字符串处理的相关函数:

(string 'abcd) ;; ==> "ABCD"
(string 'a) ;; ==> "A"
(string #\a) ;; ==> "a"
(string #\A) ;; ==> "A"
(make-string 10 :initial-element #\x) ;; ==> "xxxxxxxxxx"
(make-string 5 :initial-element #\ ) ;; ==> "      " a string of 5 blanks
(make-string 5 :initial-element #\space) ;; ==> "     " 同上
(reverse "骊山语罢清宵半") ;; ==> "半宵清罢语山骊"
(length "辛苦遭逢起一经") ;; ==> 7

Sets

常用系统函数:

  • union: 以两个 set 或者 list作为参数,返回两个的和,并去除重复的元素.最终的set集合中元素的顺序与实现有关
  • intersection: 以两个set 或者list作为参数,返回两个的公共部分。这里可以指定用于比较的等于函数,由此看来后来者 Java 的比较器也是借鉴了Lisp的思想。
  • set-difference: 以两个set 或者list作为参数, 返回第一个里面有的第二个里面没有的元素集合。
  • member: 以一个元素或者列表为参数返回集合中从参数开始的剩余集合。如果没有则返回为NIL
  • member-if:用于测试一个集合中的某一项是否满足预测函数或者返回一个从该项开始的子表
  • member-if-not:用于测试一个集合中的某一项是否不满足预测函数或者返回一个从该项开始的子表
  • subsetp: 验证第一个集合是否是第二个集合的子集
  • adjoin: 如果集合中没有第一个参数就在集合头部加入,否则返回集合

(union '(a b c d) '(e f g a b)) ;; ==> (C D E F G A B) 
(intersection '((a b) c) '(a b c)) ;; ==> (C)
(set-difference '(a b c d) '(e f g a b)) ;; ==> (C D)

(setq set1 '(a b c d e f))
(setq set2 '((a b) (c d) (e f)))
(setq set3 '((a b) e f))
(setq str1 '("b" "c" "a" "d"))
(setq str2 '("c" "a"))

(intersection set2 set3) ;; ==> NIL
(intersection set2 set3 :test #'equal) ;; ==> ((a b)) 注意这里 list 的比较函数使用的差别
(intersection str1 str2 :test #'string-equal) ;; ==> ("c" "a")

(member 'c set1) ;; ==> (C D E F)

(member '(a b) set2) ;; ==> NIL
(member '(a b) set2 :test #'equal) ;; ==> ((A B) (C D) (E F))

(member-if #'atom set1) ;; ==> (A B C D E F) 都是原子
(member-if #'listp set1) ;; ==> NIL
(member-if #'listp set2) ;; ==> ((A B) (C D) (E F))

(setq set4 '(a b c (e f) (g h)))
(member-if #'listp set4) ;; ==> ((E F) (G H))
(member-if-not #'listp set4) ;; ==> (A B C (E F) (G H)) 第一个就是 list 所以直接返回
(member-if-not #'atom set1) ;; ==> NIL
(member-if #'(lambda (x) (not (atom x))) set4) ;; ==> ((E F) (G H)) 创建匿名函数用于寻找第一个不是原子的元素
(setq digits '(2 3 4 5 6 1 0 2 3 -5 -6))
(member-if #'(lambda (x) (> x 5)) digits) ;; ==> (6 1 0 2 3 -5 -6)
(member-if-not #'plusp digits) ;; ==> (0 2 3 -5 -6)

;;特别注意以下两种写法的区别了
(subsetp '(a b) set2) ;; ==> NIL  集合中的元素都是原子的,当然不能匹配 set2 的元素了
(subsetp '((a b)) set2) ;; ==> T 集合中的元素是 list

(subsetp '((a c)) set2) ;; ==> NIL

(adjoin 'a set1) ;; ==> (A B C D E F)
(adjoin 'k set1) ;; ==> (K A B C D E F)

Trees

image.png

这棵树的节点集合可以表示为:

(A (B (d)(E (H (m)(n))))
   (C (F (i) (J (o) (p)))
      (G (k) (L (q)))))

;;获取叶节点序列方法
(defun foliage(tree)
        (cond ((null tree) nil)
               ((atom (car tree)) (foliage (cdr tree)))
               ((null (cdar tree)) (append (car tree)
                                    (foliage (cdr tree))))
               (t (append (foliage (car tree))
                        (foliage (cdr tree)))))

Stacks

栈是后进先出的线性数据结构。 常用系统函数:

  • push : 压栈
  • pop : 弹栈
  • pushnew : 如果栈中没有元素则压入,否则不压入栈
  • endp : 如果栈为空返回 T,否则返回 NIL
  • list-length : 返回list 中顶级元素的个数
(setq stack1 '(a b c))
(setq stack2 '(a (f g) h k))
(push 'a stack1) ;; ==> stack1 (A A B C)
(pop stack1) ;; ==> A   stack1  (A B C)
(pop stack2) ;; ==> A
(pop stack2) ;; ==> (F G) stack2 (H K)
(pop stack2) ;; ==> H stack2 (K)
(pop stack2) ;; ==> K stack2 NIL
(pop stack2) ;; ==> NIL

Queues

队列是在一端插入另一端删除的先进先出数据结构。 借助list的实现:

(defun queue(s w)
    (cond ((null s) (push w s))
          (t (setq s (reverse (cons w (reverse s)))))))
(setq x '( a b c d))
(setq x (queue x 'e)) ;; ==> (A B C D E)
(pop x)   ;; ==> A    x (B C D E)

推荐阅读更多精彩内容