第六章 列表數(shù)據(jù)結(jié)構(gòu)
6.1 導語
本章會展示更多列表處理函數(shù)和列表如何被應(yīng)用在其他數(shù)據(jù)結(jié)構(gòu)中,比如集合,表格和樹。Common Lisp相比于其他語言的強處之一就是,提供了很多支持這些數(shù)據(jù)結(jié)構(gòu)的內(nèi)建函數(shù)。Lisp程序員就哭了一很快的聚焦在他想解決的問題上。Pascal或者C程序員在解決實際問題之前必須先跳出來,先去實現(xiàn)一個類似于Lisp提供的系統(tǒng),例如連接列表的原語,字符數(shù)據(jù)結(jié)構(gòu),存儲的分配等等。
在本章我們會比第二章的時候處理列表更加復雜一些。不止會塔倫一些列表處理函數(shù)的行為,而且還會看到他們內(nèi)部的工作。我們先預(yù)先準備一下,復習2.17小節(jié)學習到的點對表達式。如果你還沒有閱讀先前的進階話題單元,現(xiàn)在去讀讀吧。
6.2 括號表達式VS內(nèi)存單元表達式
用括號表達式來表示列表是很方便的,但是也會產(chǎn)生誤導。使用括號表達式表示的列表是對稱的:由一個左括號開始并結(jié)束于一個右括號。因此,cons函數(shù)處理參數(shù)也是對稱的,cons函數(shù)在一個列表前加上一個元素,如下:
>(cons ’w ’(x y z)) → (w x y z)
為什么我們不能在一個列表的尾部加上一個元素呢?初學者這樣嘗試之后會被結(jié)果驚呆了:
>(cons ’(a b c) ’d) → ((a b c) . d)
如果我們在括號表達式中,訪問一個列表的左端和右端的區(qū)別沒有理由這么巨大。但是切換到內(nèi)存單元表達式來看,區(qū)別就非常巨大了。列表是由指針構(gòu)成的單向鏈條。對于列表來說我們很容易在列表前面添加一個項目是因為,我們實際上做的是創(chuàng)造一個新的單元并且白cdr指向現(xiàn)有的列表,只是這樣。如果將W和(X Y Z)輸入cons函數(shù),結(jié)果就是一個新的內(nèi)存單元,它的car指向w,cdr指向原有的列表,如下所示。雖然我們把結(jié)果寫成這樣(W X Y Z),但是實際上也可以用點對表達式來表示(W . (X Y Z))。

列表(A B C)和D輸入cons函數(shù)的時候,新內(nèi)存單元的car是指向原有列表(A B C),cdr是指向符號D。結(jié)果就是((A B C) . D),用括號表達式看上去是很奇怪。這個點的存在是絕對必須的,因為這個列表是由其他元符號而不是nil來結(jié)尾。用內(nèi)存單元表達式看起來是這樣:

由于原先的列表cdr已經(jīng)指向了nil,所以通過新建內(nèi)存單元的的方法來給列表尾部加上一個元素的方法是不可行的。更加復雜的技術(shù)將會被引用,其中一個方法在下一個小節(jié)會被介紹。
6.3 append函數(shù)
append函數(shù)接受兩個列表作為輸入,返回一個包括所有元素的列表,第一個列表的元素之后是第二個列表的元素。
> (append ’(friends romans) ’(and countrymen))
(FRIENDS ROMANS AND COUNTRYMEN)
> (append ’(l m n o) ’(p q r))
(L M N O P Q R)
如果append函數(shù)的輸入列表是空列表,那么結(jié)果就等于其他輸入,給一個列表追加空列表就相當于在一個數(shù)字上加上0。
> (append ’(april showers) nil)
(APRIL SHOWERS)
> (append nil ’(bring may flowers))
(BRING MAY FLOWERS)
> (append nil nil)
NIL
append函數(shù)對于嵌套列表也是起作用的,他只著眼于頂層括號,所以不必在意輸入是不是嵌套列表。
> (append ’((a 1) (b 2)) ’((c 3) (d 4)))
((A 1) (B 2) (C 3) (D 4))
append函數(shù)不會改變?nèi)魏巫兞炕蛘攥F(xiàn)有內(nèi)存單元的值,所以也被稱為非破壞性函數(shù)。
> (setf who ’(only the good))
(ONLY THE GOOD)
> (append who ’(die young))
(ONLY THE GOOD DIE YOUNG)
> who
(ONLY THE GOOD) The value of WHO is unchanged.
(吐槽:作者這是滿滿的惡意么?好人死得早?搜索一下才發(fā)現(xiàn)是一首77年老歌的名字)
append函數(shù)的兩個輸入看上去好像是對稱的,但這只是括號表達式帶來的一個假象。append函數(shù)對待兩個輸入是完全不同的。當往列表(D E)后面追加列表(A B C)的時候,append賦值第一個輸入但是不復制第二個。第一個輸入的最后一個內(nèi)存單元的cdr指向了第二個輸入,返回一個復制的列表的指針。如圖6-1所示。
對append函數(shù)的工作原理描述過后,就解釋了為什么在第一個輸入不是列表的時候會報錯,但是第二個輸入不是列表的時候就ok。
>(append ’a ’(b c d)) → Error! A is not a list.
>(append ’(w x y) ’z) → (W X Y . Z)
append想要復制第一個輸入的內(nèi)存單元,但由于第一個輸入A并不是一個列表,所以報錯了。但是當往列表(W X Y)上追加Z的時候,append可以復制他的第一個輸入然后最后一個內(nèi)存單元的cdr會指向第二個輸入,所以不會報錯。因為第二個輸入不是一個列表,所以結(jié)果看上去還是有點奇怪。
現(xiàn)在我們需要解決的問題就是,把一個元素加載一個列表的后面,如果我們先把元素做成一個列表,那么就可以使用append來解決這個問題了。
>(append ’(a b c) ’(d)) → (A B C D)

(defun add-to-end (x e)
"Adds element E to the end of list X."
(append x (list e)))
(add-to-end ’(a b c) ’d) → (A B C D)
比較cons,list和append
一開始Lisp程序員會在cons,list和append函數(shù)之間的區(qū)分上有所困惑,因為這三個函數(shù)都有構(gòu)造列表數(shù)據(jù)結(jié)構(gòu)的功能。這里是一個對三個函數(shù)的簡單回顧:
- cons函數(shù)創(chuàng)造一個新的內(nèi)存單元,經(jīng)常被使用在列表前面需要增加一個元素的時候。
- list函數(shù)通過接受任意數(shù)量的輸入來創(chuàng)造一個新的列表,而且在最后一個元素的內(nèi)存單元以nil結(jié)束。每一個元素的car指向?qū)?yīng)的輸入。
- append函數(shù)將列表合成在一起。通過復制第一個輸入,然后使第一個輸入的最后一個單元的cdr指向第二個輸入。如果第一個輸入不是列表的話將會出現(xiàn)錯誤。
現(xiàn)在我們拉進一步比較他們。首先考慮一種,第一個輸入是符號,第二個輸入是列表的情況:
> (cons ’rice ’(and beans))
(RICE AND BEANS)
> (list ’rice ’(and beans))
(RICE (AND BEANS))
> (append ’rice ’(and beans))
Error: RICE is not a list.
然后我們看看兩個輸入都是列表的情況:
> (cons ’(here today) ’(gone tomorrow))
((HERE TODAY) GONE TOMORROW)
> (list ’(here today) ’(gone tomorrow))
((HERE TODAY) (GONE TOMORROW))
> (append ’(here today) ’(gone tomorrow))
(HERE TODAY GONE TOMORROW)
最后我們來看看第一個輸入是列表,第二個輸入是符號的情況。這是最讓人困惑的情況,你必須用內(nèi)存單元表達式來再想想。
> (cons ’(eat at) ’joes)
((EAT AT) . JOES)
> (list ’(eat at) ’joes)
((EAT AT) JOES)
> (append ’(eat at) ’joes)
(EAT AT . JOES)
為了更多的而開發(fā)你對這三個函數(shù)的直覺,嘗試一下使用sdraw工具來驗證上面的例子,lisp toolkit中的sdraw工具是用來畫內(nèi)存單元表達式圖的。
6.5 更多關(guān)于列表的函數(shù)
lisp提供了很多用來操作列表的簡單函數(shù),我們已經(jīng)討論過的有cons,list,append和length。他們之中有些必須復制它的輸入,有些則不需要。接下來請看這樣設(shè)置的理由。
6.5.1 reverse(反轉(zhuǎn))
reverse函數(shù)提供一個列表的反轉(zhuǎn)。
> (reverse ’(one two three four five))
(FIVE FOUR THREE TWO ONE)
> (reverse ’(l i v e))
(E V I L)
> (reverse ’live)
Error: Wrong type input.
> (reverse ’((my oversight)
(your blunder)
(his negligence)))
((HIS NEGLIGENCE) (YOUR BLUNDER) (MY OVERSIGHT))
請注意reverse函數(shù)只是反轉(zhuǎn)頂層括號的元素。嵌套列表里的作為元素的列表不會被操作。關(guān)于reverse函數(shù)的另一點就是他不支持符號的操作。列表(L I V E)的反轉(zhuǎn)是列表(E V I L),但是符號live的反轉(zhuǎn)會返回一個類型輸入錯誤。
就像append一樣,reverse函數(shù)也是非破壞性的。他會復制輸入而不是改變輸入。
> (setf vow ’(to have and to hold))
(TO HAVE AND TO HOLD)
(reverse vow)
(HOLD TO AND HAVE TO)
vow
(TO HAVE AND TO HOLD)
我們可以像下面一樣使用reverse函數(shù)來給一個列表的最后加上一個元素。假設(shè),我們想要給列表(A B C)加上元素D。列表(A B C)的反轉(zhuǎn)就是(C B A)。把符號D加到這個反轉(zhuǎn)列表的前面,再一次反轉(zhuǎn),就得到了列表(A B C D)。
(defun add-to-end (x y)
(reverse (cons y (reverse x))))
(add-to-end ’(a b c) ’d) → (a b c d)
現(xiàn)在你知道了兩種在列表后面加上元素的方案。append方案和雙reverse方案,相比之下,append方案會更好一些,因為雙reverse方案會進行兩次復制,append方案會更有效率一些。在本章的結(jié)尾,我們會在進階話題中進一步討論效率問題。
6.5.2 nth和nthcdr
nthcdr函數(shù)返回的是列表的連續(xù)第n個的cdr。當然了我們輸入0個的話,就會返回原來的列表,如果要的個數(shù)多過列表的長度,就會返回NIL。
(nthcdr 0 ’(a b c)) → (a b c)
(nthcdr 1 ’(a b c)) → (b c)
(nthcdr 2 ’(a b c)) → (c)
(nthcdr 3 ’(a b c)) → nil
使用大于3的數(shù)字輸入并不會引發(fā)錯誤;我們會得到和輸入3相同的結(jié)果。的腳的結(jié)論之一就是nil的cdr就是nil。
(nthcdr 4 ’(a b c)) → nil
(nthcdr 5 ’(a b c)) → nil
但如果列表的結(jié)尾不是nil而是一個元字符的話,輸入的數(shù)字超過長度就會引發(fā)錯誤。
(nthcdr 2 ’(a b c . d)) → (c . d)
(nthcdr 3 ’(a b c . d)) → d
(nthcdr 4 ’(a b c . d)) → Error! D is not a list
函數(shù)nth是返回一個列表的第n個元素。
(defun nth (n x)
"Returns the Nth element of the list X,
counting from 0."
(car (nthcdr n x)))
既然(NTHCDR 0 x)得到的是列表,(NTH 0 x)得到的是第一個元素,以此類推,(NTH 1 x)就是得到第二個元素。
(nth 0 ’(a b c)) → a
(nth 1 ’(a b c)) → b
(nth 2 ’(a b c)) → c
(nth 3 ’(a b c)) → nil
數(shù)數(shù)字的時候是從0開始而不是從1開始是Common Lisp一直遵循的慣例。在我們討論數(shù)組的時候會在此接觸到這一點。
6.5.3 last函數(shù)
last函數(shù)返回的是一個列表的最后一個內(nèi)存單元,這個內(nèi)存單元的car是列表的最后一個元素。根據(jù)定義,這個單元的cdr是一個元字符。否則就不是列表的最后一個單元了。如果列表為空,last會返回nil。
(last ’(all is forgiven)) → (forgiven)
(last nil) → nil
(last ’(a b c . d)) → (c . d)
(last ’nevermore) → Error! NEVERMORE is not a list.
6.5.4 remove函數(shù)
remove函數(shù)會刪除列表中的一個項目。正常情況下回刪除所有適合條件的元素,也是有方法刪除其中一部分的(在本章進階話題中)。remove返回的結(jié)果是一個刪除了相關(guān)元素的新的列表。
(remove ’a ’(b a n a n a)) → (b n n)
(remove 1 ’(3 1 4 1 5 9)) → (3 4 5 9)
remove函數(shù)是一個非破壞性函數(shù),當從列表中刪除變量的時候,他不會改變?nèi)魏巫兞亢蛢?nèi)存單元。remove得到的結(jié)果是一個原來列表的部分的拷貝。
> (setf spell ’(a b r a c a d a b r a))
(A B R A C A D A B R A)
> (remove ’a spell)
(B R C D B R)
> spell
(A B R A C A D A B R A)
下面的表格會幫助你記憶,那些函數(shù)拷貝輸入進行處理,哪些不拷貝。append,reverse,remove返回一個新的不包括輸入的內(nèi)存單元鏈,所以他們是要拷貝他們的輸入的。像nthcdr,nth和last函數(shù)會返回一個指針指向輸入的部分組件。他們不需要拷貝任何對象,因為他們所需要的對象都已經(jīng)存在了。

6.6 列表構(gòu)成集合
集合是對象的無序集合。每一個對象只在該集合中出現(xiàn)一次。一些典型的集合就是,一個星期的天數(shù),整數(shù)的集合(一個無限集合),還有住在王家屯晚上吃了麻辣燙的集合。
集合毫無疑問是有列表組成的一個更加有用的數(shù)據(jù)結(jié)構(gòu)。一些基本的集合操作比如說測試一個元素是不是在這個集合當中,獲取兩個集合的并集,交集,差集(也叫做集合減法),還有測試一個集合是不是另一個集合的子集合。這些所有操作的Lisp函數(shù)都會在下面介紹。
6.6.1 成員(member)
member斷言的作用是檢查一個元素是不是列表的成員。如果是列表的成員,那么有這個元素開始的字列表將會被返回,不是的話,會返回nil。member絕不會返回T,但是傳統(tǒng)意義上這是一個斷言,因為如果元素術(shù)語這個列表就會返回非NIL。
> (setf ducks ’(huey dewey louie)) Create a set of ducks.
(HUEY DEWEY LOUIE)
> (member ’huey ducks) Is Huey a duck?
(HUEY DEWEY LOUIE) Non-NIL result: yes.
> (member ’dewey ducks) Is Dewey a duck?
(DEWEY LOUIE) Non-NIL result: yes.
> (member ’louie ducks) Is Louie a duck?
(LOUIE) Non-NIL result: yes.
> (member ’mickey ducks) Is Mickey a duck?
NIL NIL: no
在Lisp的第一個方言中,member函數(shù)值返回T或者NIL。但是熱么決定使得member函數(shù)返回這個元素開始子列表,成為了一個非常有用的功能。這個擴展一如既往的伴隨member作為一個斷言,因為沒有元素的子列表就是false。
這里有一個例子說明為什么member返回子列表是非常有用的。斷言beforep,在列表l中,x比y先出現(xiàn)的話,返回真。
(defun beforep (x y l)
"Returns true if X appears before Y in L"
(member y (member x l)))
> (beforep ’not ’whom
’(ask not for whom the bell tolls))
(WHOM THE BELL TOLLS)
> (beforep ’thee ’tolls ’(it tolls for thee))
NIL
6.6.2 交集(intersection)
intersection函數(shù)是去獲取兩個列表的交集返回一個兩個列表的共有元素的列表。在結(jié)果中,元素出現(xiàn)的順序是未定義的,可能每一個Lisp實現(xiàn)都不相同。另外在集合中順序是不重要的,只有元素本身是重要的。
> (intersection ’(fred john mary)
’(sue mary fred))
(FRED MARY)
> (intersection ’(a s d f g)
’(v w s r a))
(A S)
> (intersection ’(foo bar baz)
’(xam gorp bletch))
NIL
如果一個列表中出現(xiàn)了多個一樣的元素,那就不是一個真的集合。Common Lisp的集合函數(shù),比如intersection和union都可以處理不是集合的列表,但是結(jié)果是不是包括重復元素就沒有準確定義了,們多事情可能還是要看具體實現(xiàn)。
6.6.3 并集(union)
union函數(shù)返回兩個集合的并集,換言之,由兩個列表的所有元素組成的不重復列表,一個元素在兩個列表中都出現(xiàn)了的話,那么在結(jié)果中就只存在一份。對集合來說,結(jié)果的元素出現(xiàn)順序并不重要(也沒有定義)。
> (union ’(finger hand arm)
’(toe finger foot leg))
(FINGER HAND ARM TOE FOOT LEG)
> (union ’(fred john mary)
’(sue mary fred))
(FRED JOHN MARY SUE)
> (union ’(a s d f g)
’(v w s r a))
(A S D F G V W R)
6.6.4 差集(set-difference)
set-difference函數(shù)的功能室差集。返回的是第一個集合去除和第二個集合重復的元素的結(jié)果。再次說明,元素出現(xiàn)的順序沒有定義。
> (set-difference ’(alpha bravo charlie delta)
’(bravo charlie))
(ALPHA DELTA)
> (set-difference ’(alpha bravo charlie delta)
’(echo alpha foxtrot))
(BRAVO CHARLIE DELTA)
> (set-difference ’(alpha bravo) ’(bravo alpha))
NIL
不像union和intersection,set-difference函數(shù)不是一個對稱函數(shù)。調(diào)換第一個和第二和輸入會導致結(jié)果的不同。
(setf line1 ’(all things in moderation))
(setf line2 ’(moderation in the defense of liberty
is no virtue))
> (set-difference line1 line2)
(ALL THINGS)
> (set-difference line2 line1)
(THE DEFENSE OF LIBERTY IS NO VIRTUE)
6.6.5 substep斷言
如果一個集合包括了另一個,斷言substep返回T,換句話說,第一個集合每一個元素都是第二個集合的元素。
(subsetp ’(a i) ’(a e i o u)) → t
(subsetp ’(a x) ’(a e i o u)) → nil
6.7 用集合編程
這里的一個例子是說如何使用集合解決一個編程問題。這個問題是給一個名字的前面加上一個稱呼,吧john doe變成mr john doe或者吧jane doe變成ms jane doe。如果一個名字已經(jīng)有稱呼了,那么標題應(yīng)該被保留,但是如果沒有的話,我們來嘗試根據(jù)第一個名字來決定性別然后加上合適的稱呼。
為了解決這樣的問題,我們必須把他分解成小問題,讓我們開始在這個首先名字前面有沒有稱呼這個小問題上。這里有定義一個函數(shù)來解決這個問題。
(defun titledp (name)
(member (first name) ’(mr ms miss mrs)))
> (titledp ’(jane doe)) ‘‘Jane’’ is not a title.
NIL
> (titledp ’(ms jane doe)) ‘‘Ms.’’ is in the set of titles.
(MS MISS MRS)
下一個步驟就是找出這個單詞是一個男性名字還是女性名字。我們會使用簡化一些的樣本來是的我們的例子保持簡潔。
(setf male-first-names
’(john kim richard fred george))
(setf female-first-names
’(jane mary wanda barbara kim))
(defun malep (name)
(and (member name male-first-names)
(not (member name female-first-names))))
(defun femalep (name)
(and (member name female-first-names)
(not (member name male-first-names))))
> (malep ’richard) ‘‘Richard’’ is in the set of males.
T
> (malep ’barbara) ‘‘Barbara’’ is not a male name.
NIL
> (femalep ’barbara) ‘‘Barbara’’ is a female name.
T
> (malep ’kim) ‘‘Kim’’ can be either male or female,
NIL so it’s not exclusively male.
現(xiàn)在我們可以寫一個give-title函數(shù)來給名字加上稱呼。當然,我們只給沒有稱呼的名字加上去。如果名字既不屬于男性,也不屬于女性那就加上mr or ms。
(defun give-title (name)
"Returns a name with an appropriate title on
the front."
(cond ((titledp name) name)
((malep (first name)) (cons ’mr name))
((femalep (first name)) (cons ’ms name))
(t (append ’(mr or ms) name))))
> (give-title ’(miss jane adams)) Already has a title.
(MISS JANE ADAMS)
> (give-title ’(john q public)) Untitled male name.
(MR JOHN Q PUBLIC)
> (give-title ’(barbara smith)) Untitled female name.
(MS BARBARA SMITH)
> (give-title ’(kim johnson)) Untitled, and gender
(MR OR MS KIM JOHNSON) is ambiguous
在這個例子中比較重要的特性有,1,將問題分解為小函數(shù)。2,逐一測試寫的每一個函數(shù),一旦寫好了titlep,malep和femaiep等斷言,give-title函數(shù)就很好寫了。
講一個問題分解為子問題是一個很重要的技能。有經(jīng)驗的程序員京城可以看到正確的方式如何講一個問題分解成為邏輯上的子問題,但是初學者必須同過聯(lián)系來建立這種直覺。
Here are a few more things we can do with these lists of names. The
functions below take no inputs, so their argument list is NIL.
還有一件我們可以做的事情就是關(guān)于這些列表的名字。下面的函數(shù)沒有輸入,也就是參數(shù)列表是NIL。
(defun gender-ambiguous-names ()
(intersection male-names female-names))
(gender-ambiguous-names) → (kim)
(defun uniquely-male-names ()
(set-difference male-names female-names))
(uniquely-male-names) → (john richard fred george)
到現(xiàn)在為止,我們在本章見過的所有集合都只是由字符或者數(shù)字組成。操作列表組成的集合也是很簡單的,但是使用像member,union,intersection等等函數(shù)的話需要一個小技巧。詳情請見進階話題的討論。
6.8 列表組成的表格
表格(tables)是另一個我們可以由列表構(gòu)建的非常有用的數(shù)據(jù)結(jié)構(gòu)。一個表格,或者聯(lián)合列表,是一個列表的列表。每一個列表被稱作一個入口(entry),每一個入口的car就是入口的key、下面顯示的就是一個五個英文單詞和他們的法語翻譯組成的表格。這個二表哥包括5個入口,key就是英語單詞。
(setf words
’((one un)
(two deux)
(three trois)
(four quatre)
(five cinq)))
6.8.1 assoc函數(shù)
The ASSOC function looks up an entry in a table, given its key. Here are
some examples.
assoc函數(shù)在表格中給出一個key,找到一個入口,下面是例子:
(assoc ’three words) → (three trois)
(assoc ’four words) → (four quatre)
(assoc ’six words) → nil
assoc尋找五個之中匹配的入口,然后返回整個入口。如果assoc沒有找到入口,就返回nil。
Notice that when ASSOC does find an entry with the given key, the value
it returns is the entire entry. If we want only the French word and not the
entire entry, we can take the second element of the result of ASSOC.
請注意當assoc找到對應(yīng)key的入口的時候,返回的值是整個入口。如果我們只想要法語翻譯的話那么就對assoc的結(jié)果進行進一步處理,得到入口第二個元素。
(defun translate (x)
(second (assoc x words)))
(translate ’one) → un
(translate ’five) → cinq
(translate ’six) → nil
6.8.2 rasscoc
rassoc類似于assoc,但是是著眼于每一個表格的元素的cdr而不是car。(他的名字就是Reverse assoc的縮寫)。為了使用符號作為key的rassoc,表格必須使一個點對的列表:
(setf sounds
’((cow . moo)
(pig . oink)
(cat . meow)
(dog . woof)
(bird . tweet)))
(rassoc ’woof sounds) ? (dog . woof)
(assoc ’dog sounds) ? (dog . woof)
assoc和rassoc都是返回他們找到的第一個符合key的入口,身下的列表就不被搜索了。
6.9 表格編程
這里有assoc的另一個例子。我們將要創(chuàng)造一個對象以及他們描述的表格,這個表格會存儲在全局變量things中:
((object1 large green shiny cube)
(object2 small red dull metal cube)
(object3 red small dull plastic cube)
(object4 small dull blue metal cube)
(object5 small shiny red four-sided pyramid)
(object6 large shiny green sphere))
現(xiàn)在我們來開發(fā)函數(shù)來告訴我們,在那個對象中有兩個對象不同。我們一開始來寫一個叫做description的函數(shù)來導出一個對象的描述。
(defun description (x)
(rest (assoc x things)))
(description ’object3) → (red small dull plastic cube)
兩個對象之間的區(qū)別是有什么屬性出現(xiàn)在了第一個對象的描述中卻沒有出現(xiàn)在第二個當中,或者在第二個對象的描述中出現(xiàn)但第一個沒有。用技術(shù)術(shù)語來說就是異或。這是Common Lisp的內(nèi)建函數(shù)。
(defun differences (x y)
(set-exclusive-or (description x)
(description y)))
(differences ’object2 ’object3) → (metal plastic)
object2是金屬,但是object3是塑料,所以金屬和塑料的屬性是完全不同的。我們可以根據(jù)他們指向的類型的不停來分辨屬性。這里是一個點對列表的表格:
(setf quality-table
’((large . size)
(small . size)
(red . color)
(green . color)
(blue . color)
(shiny . luster)
(dull . luster)
(metal . material)
(plastic . material)
(cube . shape)
(sphere . shape)
(pyramid . shape)
(four-sided . shape)))
我們可以使用這個表格作為函數(shù)的一部分來給我們指出給定屬性的物質(zhì):
(defun quality (x)
(cdr (assoc x quality-table)))
(quality ’red) → color
(quality ’large) → size
Using DIFFERENCES and QUALITY, we can write a function to tell us
one quality that is different between a pair of objects.
使用differences和quality,我們可以寫一個函數(shù)來告訴我們一個物質(zhì)在兩個對象之間有什么不同。
(defun quality-difference (x y)
(quality (first (differences x y))))
(quality-difference ’object2 ’object3) → material
(quality-difference ’object1 ’object6) → shape
(quality-difference ’object2 ’object4) → color
如果我想要一個所有物質(zhì)區(qū)別的列表而不只是一個怎么辦?我們需要一個方法來從區(qū)別的列表(RED
BLUE METAL PLASTIC) 到對應(yīng)物質(zhì)的列表轉(zhuǎn)換。然后我們也不得不評價多個元素。第一部分可以依靠sublis完成,這個函數(shù)我們將在進階話題中討論。
(differences ’object3 ’object4) → (red blue metal plastic)
> (sublis quality-table
(differences ’object3 ’object4))
(COLOR COLOR MATERIAL MATERIAL)
現(xiàn)在我們不得不做的是在結(jié)果中評價多個入口。Common Lisp提供了一個函數(shù)佳作remove-duplicates來達到這個目的。
(defun contrast (x y)
(remove-duplicates
(sublis quality-table (differences x y))))
(contrast ’object3 ’object4) → (color material)
小結(jié)
列表在自己應(yīng)用范圍是一種重要的數(shù)據(jù)結(jié)構(gòu)。但是在lisp中他更重要的地方在于他能夠用來構(gòu)成其他數(shù)據(jù)結(jié)構(gòu),集合和表格。
如所見,解決任何不簡單的問題的方法及時將問題分解成小問題,一個個可控的片段。一些簡單的函數(shù)可以一個個測試并組合成主要問題的解決方案。
本章涉及函數(shù)
列表函數(shù): APPEND, REVERSE, NTH, NTHCDR, LAST, REMOVE.
集合函數(shù): UNION, INTERSECTION, SET-DIFFERENCE, SETEXCLUSIVE-OR, MEMBER, SUBSETP, REMOVE-DUPLICATES.
表格函數(shù): ASSOC, RASSOC
Lisp Toolkit:sdraw
sdarw是一個用來畫列表對應(yīng)的內(nèi)存單元表達式的工具。他并不是Common Lisp標準中的一部分;他被定義在附錄A當中。在完整的移植版本中會運行任何Common Lisp實現(xiàn),使用普通字符來畫出內(nèi)存單元表示:

還有一個圖形版本,可以從發(fā)行商那里的軟盤獲得,是使用圖形來表示內(nèi)存單元和箭頭的函數(shù)。那個圖形表示看上去更好看一些。一個圖形版本是使用CLX,Common Lisp對應(yīng)X Window System的接口。如果你的電腦不是運行Xwindow的話,會不能使用這個版本。但是要是你的Lisp支持其他圖形實例,也會很容易使用sdraw。
另一個有用的工具像函數(shù)sdraw-loop,就像一個read-eval-print循環(huán)一樣工作的畫圖函數(shù)。sdraw-loop的提示符就是字符串S>。

第六章進階話題
6.10 樹結(jié)構(gòu)(trees)
樹是一個嵌套列表結(jié)構(gòu)。到現(xiàn)在為止所有的函數(shù)都只是對于列表的頂層進行操作,他們并不進行更深入結(jié)構(gòu)的操作。Lisp也包括一些新的函數(shù)來操作整個列表。其中的兩個就是subst和sublis。在第八章中我們會寫很多這樣操作樹結(jié)構(gòu)的函數(shù)。
6.10.1 subst
subst函數(shù)經(jīng)列表中的一個項目替換(substitute)為另一個項目,不論這個元素出現(xiàn)在列表的何處。這是一個三輸入的函數(shù),輸入的順序是,用x替換z中的y。
> (subst ’fred ’bill
’(bill jones sent me an itemized
bill for the tires))
(FRED JONES SENT ME AN ITEMIZED
FRED FOR THE TIRES)
如果要被替換的字符沒有出現(xiàn)在該列表中,則返回原來的列表。
> (subst ’bill ’fred ’(keep off the grass))
(KEEP OFF THE GRASS)
> (subst ’on ’off ’(keep off the grass))
(KEEP ON THE GRASS)
subst著眼于整個列表結(jié)構(gòu),不僅僅是頂層元素。
> (subst ’the ’a
’((a hatter) (a hare) and (a dormouse)))
((THE HATTER) (THE HARE) AND (THE DORMOUSE))
6.10.2 sublis
sublis類似于subst,除了他可以同時替換多個元素。第一個輸入是一個點對作為入口形成的表格。第二個輸入是將要做出替換的列表。
> (sublis ’((roses . violets) (red . blue))
’(roses are red))
(VIOLETS ARE BLUE)
(setf dotted-words
’((one . un)
(two . deux)
(three . trois)
(four . quatre)
(five . cinq)))
> (sublis dotted-words ’(three one four one five))
(TROIS UN QUATRE UN CINQ)
6.11 列表操作的效率
在本章的一開始我們討論了在括號表達式中,列表如何表現(xiàn)出對稱性,但是實際上不是這樣的。另一個方法就是在相對速度或者制定操作的效率上表現(xiàn)出的非對稱性。例如,提取一個列表的首個元素很容易,但是提取最后一個元素卻要很花力氣。當要提取起一個元素的時候,我們從指向第一個內(nèi)存單元的指針開始。函數(shù)first僅僅是提取了那個單元的car然后返回他。想要找到列表的最后一個元素要做的工作就要多得多。因為唯一的方法就是一直找啊找知道遇到一個元素的cdr是一個元字符的時候,我們才可以看到那個單元的car。如果原來的列表很長,那么可能要好一會才能找到最后一個單元。
計算機可以循序成百上千的單元來找到想要的單元,時間還非常短,所以一般不需要注意到first和last在速度上的差別。但是如果軟件項目的規(guī)模變得很大,那么這個細微差別就會變得引人注意。
另一個會影響函數(shù)速度的事實就是有多少切實的單元存在。創(chuàng)造一個新的單元是要耗費時間的,最后也會占滿計算機的內(nèi)存。在事實上有一些會被棄用,但是所占的內(nèi)存卻還在。在一些Lisp實現(xiàn)中,內(nèi)存會被沒有用的單元給占滿,機器必須暫時停下來然后運行垃圾回收機制。一個函數(shù)要處理的單元越多,垃圾回收機制運行的就越頻繁。我們接下來來比較一下這兩個版本add-to-and函數(shù)的效率:
(defun add-to-end-1 (x y)
(append x (list y)))
(defun add-to-end-2 (x y)
(reverse (cons y (reverse x))))
假設(shè)這些函數(shù)的輸入是一個n元素的列表。add-to-end1使用append來拷貝第一個輸入,然后將第二個輸入定位到最后。因此他會創(chuàng)造一個完整的n+1列表。add-to-end2開始是將列表反轉(zhuǎn),這將會創(chuàng)造一個新的列表,然后將第二個輸入加載這個列表的前面,最后反轉(zhuǎn)結(jié)果,再次創(chuàng)造一個新的列表。所以add-toend2創(chuàng)造的是n+1+n+1個單元。其中n+1是結(jié)果,其他的n+1將會在創(chuàng)造后被拋棄,成為垃圾,很銘心啊add-to-end1是一個更加有效率的函數(shù),因為創(chuàng)造了更少的單元。
6.12 共享結(jié)構(gòu)
當兩個列表共享內(nèi)存單元的時候,就被稱為共享結(jié)構(gòu)。在顯示器上輸出的列表是不可能體現(xiàn)出共享結(jié)構(gòu)的,因為每一個列表看上去都是一個新列表。通過使用car,cdr和cons可以構(gòu)造全新的共享列表。例如我們使得兩個列表共享一些元素。
> (setf x ’(a b c))
(A B C)
> (setf y (cons ’d (cdr x)))
(D B C)
列表X的值是(A B C),Y的值是(D B C)。兩個列表共享了一部分內(nèi)存單元(B C)。共享結(jié)構(gòu)的建立是因為Y是由(CDR X)而來的。如果我們直接簡單定義Y,就不會有共享結(jié)構(gòu)。

6.13 對象的相等
在Lisp中,符號是唯一的,意味著他們在計算機內(nèi)存中只能是一個具名符號。每一個內(nèi)存中的對象命名位置,我們叫他們地址(address)。所以在列表(TIME AFTER TIME)中。是沒有兩個字符叫做time的。

列表,從另一個角度說也不是唯一的。我們可以很簡單的用獨立的內(nèi)存單元鏈條來構(gòu)造不同的列表(A B C),兩個列表中的字符是唯一的,但是列表本身不是唯一的。這意味著equal函數(shù)是不可以通過比較兩者的地址來知道他們是不是相等,因為兩個(A B C)是相等的但是他們是不同的內(nèi)存單元。equal函數(shù)因此事一個一個元素的比較兩個列表。如果兩個列表的對應(yīng)元素是相等的,那么列表本身就被認為是相等的。
> (setf x1 (list ’a ’b ’c)) Make a fresh list (A B C).
(A B C)
> (setf x2 (list ’a ’b ’c)) Make another list (A B C).
(A B C)
> (equal x1 x2) The lists are EQUAL.
T
如果我們想知道兩個指針是不是指向相同的對象,就必須比較他們的地址。EQ斷言就是做這個事情的。列表之間如果有相同的地址,那么他們就是相等的(EQ),不會一個個比較元素。
> (eq x1 x2) The two lists are not EQ.
NIL
> (setf z x1) Now Z points to the same list as X1.
(A B C)
> (eq z x1) So Z and X1 are EQ.
T
> (eq z ’(a b c)) These lists have different addresses.
NIL
> (equal z ’(a b c)) But they have the same elements.
T
EQ函數(shù)要比eqaul快,因為eq只是比較一個個地址,equal首先要測試輸入是不是列表,如果是必須每一個元素相對應(yīng)的比較。由于更有效率,程序員更喜歡使用eq而不是equal,除非是想要知道內(nèi)存單元是不是相同。
數(shù)字,在不同的Lisp系統(tǒng)中也有不同的展現(xiàn)。在一些實現(xiàn)中,每一個數(shù)字都有一個唯一的地址,然而在另一些實現(xiàn)中不是。因此eq不應(yīng)該用來比較數(shù)字。
eql斷言的行為大體上跟eq相同。他跟eql一樣比較兩個對象的地址,除開對于兩個相同類型(例如都是整形)數(shù)字的情況下。他會比較兩個數(shù)字的值。不同類型的數(shù)字在eql中不相等,即使是值相等。
(eql ’foo ’foo) → t
(eql 3 3) → t
(eql 3 3.0) → nil Different types.
eql是Common Lisp中的“默認”比較斷言。例如member函數(shù)和assoc函數(shù)都是默認使用eql作為相等的比較函數(shù),除非指定使用其他的。
對于比較兩個完全不同類型的數(shù)字,現(xiàn)在也有另一個比較斷言叫做=。這個斷言是最常用的比較連個數(shù)字的方法。給出數(shù)字外的其他輸入會報錯。
(= 3 3.0) → t
(= ’foo ’foo) → Error! FOO is not a number
最后,equalp斷言是相似于equal的斷言,但是在一些情況下更加寬容一些。一個例子就是他會忽視字符串比較的大小寫。
(equal "foo bar" "Foo BAR") → nil
(equalp "foo bar" "Foo BAR") → t
初學者經(jīng)常會對Common Lisp中的大量相等比較感到困惑,我推薦的是忘記這些所有特殊的函數(shù)。只是記住兩條建議。第一,使用equal,他就是你想要的。第二內(nèi)建函數(shù)memeber和assoc等等在內(nèi)部出于效率都是使用eql作為默認的。這意味著他們頸部會正確比較列表,除非你告訴他們用不同的相等斷言。下一小節(jié)我們解釋如何做到這一點:
概述一下:
- eq是最快的相等測試,他比較的是地址。專家一般是用這個來快速比較符號,測試他們是不是在內(nèi)存單元中是同一個物理單元。eq不應(yīng)該被用來比較數(shù)字。
- eql類似于eq,除了他可以安全的比較相同類型的數(shù)字,例如兩個整形或者兩個浮點數(shù)。在common Lisp中他是默認的相等測試斷言。
- equal是一個初學者使用的斷言。他逐個比較列表的元素,除此之外和eql的行為相同。
- equalp是更加寬容的斷言,和equal比較的話,他會忽視字符大小寫的諸如此類的問題。
- =是一個比較數(shù)字最有效的方式,也是比較不同類型數(shù)字的唯一方式,比如3和3.0.他只接受數(shù)字。
6.14 關(guān)鍵字參數(shù)(keyword argument)
很多Common Lisp函數(shù)都支持在列表中附加的,可選的參數(shù),叫做關(guān)鍵字參數(shù)。例如,remove函數(shù)可以接受一個可選的參數(shù)叫做,:count來告訴函數(shù),到底刪除多少個對象。
(setf text ’(b a n a n a - p a n d a))
> (remove ’a text) Remove all As.
(B N N - P N D)
> (remove ’a text :count 3) Remove 3 As.
(B N N - P A N D A)
remove也接受一個:from-end關(guān)鍵字。如果他的值不是nil,那么remove就會從列表的最后開始操作,而不是從開始。所以為了刪除列表中的最后兩個A,我們可以這樣寫:
> (remove ’a text :count 2 :from-end t)
(B A N A N A - P N D)
一個關(guān)鍵字是一個特殊類型的字符,他的名字總是在一個冒號的后面。字符count和:count不是相同的,他們是不同的對象,在eq中也是不同的。關(guān)鍵字總是求值為自身的,所以他們不需要被引用,嘗試改變一個關(guān)鍵字的值會引發(fā)錯誤。斷言keywordp,如果輸入是關(guān)鍵字的話就會返回T。
:count → :count
(symbolp :count) → t
(equal :count ’count) → nil
(keywordp ’count) → nil
(keywordp :count) → t
另一個可以接受關(guān)鍵字參數(shù)的函數(shù)是member,正常來說,member使用eql來測試一個項目是不是出現(xiàn)在了一個集合里。eql對符號和數(shù)字都是有效的。但是假設(shè)我們的集合包括了列表怎么辦?在那種情況下我們必須使用equal來做相等判斷,否則其他的member將不會找到我們要找的項目。
(setf cards
’((3 clubs) (5 diamonds) (ace spades)))
(member ’(5 diamonds) cards) → nil
(second cards) → (five diamonds)
(eql (second cards) ’(5 diamonds)) → nil
(equal (second cards) ’(5 diamonds)) → t
關(guān)鍵字:test可以再member函數(shù)中使用來指定一個相等判斷的函數(shù)。我們在特定的引號后面寫上函數(shù)名作為一種輸入。
> (member ’(5 diamonds) cards :test #’equal)
((5 DIAMONDS) (ACE SPADES))
所有的列表函數(shù),包括相等測試等等都接受一個:test關(guān)鍵字參數(shù)。remove函數(shù)是另一個例子,我們不能從cards中刪除(5 diamonds)除非我們告訴remove使用equal作為相等判斷。
> (remove ’(5 diamonds) cards)
((3 CLUBS) (5 DIAMONDS) (ACE SPADES))
> (remove ’(5 diamonds) cards :test #’equal)
((3 CLUBS) (ACE SPADES))
另一個接受:test關(guān)鍵字的函數(shù)是union,intersection和set-difference,assoc,rassoc,subst和sublis。為了找出函數(shù)接受哪一個關(guān)鍵字,可以使用在線文檔。給函數(shù)輸入他不支持的關(guān)鍵字的話會報錯。
> (remove ’(ace spades) cards :reason ’bad-luck)
Error! :REASON is an invalid keyword argument
to REMOVE.
進階話題涉及函數(shù)
樹函數(shù): SUBST, SUBLIS.
附加的相等函數(shù): EQ, EQL, EQUALP, =.
關(guān)鍵字斷言: KEYWORDP