標籤:
內容提要
列表中的一些數字運算,累加器
尾遞迴調用
列表中的一些數字運算,累加器
關於數字運算最為重要的應用,可能是擷取一些資料結構體的一些有用事實,比如列表。例如,知道列表的長度是很有用的。我們將會給出一些使用列表和數字運算的例子。
一個列表的長度是多少?這裡有一個遞迴定義:
1. 空列表的長度為0.
2. 非空列表的長度為 1 + len(T),其中len(T)是非空列表的尾部。
這個定義在Prolog中很容易實現,以下是實現代碼:
len([], 0).
len([_|T], N) :- len(T, X), N is X + 1.
這個謂詞會如期望的運行,比如:
?- len([a, b, c, d, e, [a, b], g], X).
X = 7
這是一個不錯的程式:很容易理解,並且很書寫很高效。但是還有其他一些求列表長度的方式。我們將會學習這種替代方式,因為它會引入累加器的概念。如果你有使用其他程式設計語言的經驗,
你可能已經知道使用變數保持中間結果的概念,累加器就是Prolog中對應的思路。
如下是一個使用累加器計算資料行表長度的例子,我們將定義一個謂詞,accLen/3,有如下的參數:
accLen(List, Acc, Length)
這裡的List就是我們想要求解的列表,Length就是列表的長度(是一個整數)。Acc是什嗎?就是我們用於儲存長度中間值的累加器(所以也是一個整數)。我們的思路是,如果我們調用這個
謂詞,將Acct初始化為0;當遞迴對列表進行操作時,每當找到一個頭元素,就將Acct加1,直到列表為空白;當列表為空白時,Acc就會儲存列表的長度,下面是代碼:
accLen([_|T], A, L) :- Anew is A+1, accLen(T, Anew, L).
accLen([], A, A).
關於基礎子句的定義,將第二個參數和第三個參數進行了合一。為什嗎?因為這個簡單的合一是返回結果的良好方式。當達到了列表的底部,累加器(第二個參數)持有了列表的長度值,所以
所以將這個值通過合一賦予長度變數(第三個參數)。下面是一個例子的追蹤,可以清晰地看到當Prolog到達列表底部,長度變數通過合一進行了賦值:
?- accLen([a, b, c], 0, L).
Call: (6) accLen([a, b, c], 0, _G499) ?
Call : (7) _G518 is 0 + 1 ?
Exit: (7) 1 is 0 + 1 ?
Call: (7) accLen([b, c], 1, _G499) ?
Call: (8) _G521 is 1+1 ?
Exit: (8) 2 is 1+1 ?
Call: (8) accLen([a], 2, _G499) ?
Call: (9) _G524 is 2+1 ?
Exit: (9) 3 is 2+1?
Call: (9) accLen([], 3, _G499) ?
Exit: (9) accLen([], 3, 3) ?
Exit: (8) accLen([c], 2, 3) ?
Exit: (7) accLen([b, c], 1, 3) ?
Exit: (6) accLen([a, b, c], 0 ,3)?
最後,我們可以定義一個謂詞調用accLen,並且給出累加器的初始值為0:
leng(List, Length) :- accLen(List, 0, Length).
所以,我們可以進行如下的查詢:
?- leng([a, b, c, d, e, [a, b], g], X).
X = 7
尾遞迴調用
累加器在Prolog中是很常用的(後面的章節會看到更多使用累加器的例子),但是為什麼會這樣?accLen在哪個方面比len更好呢?畢竟,accLen看上去更加複雜。答案就是因為accLen是
尾遞迴調用,但len不是。在一個尾遞迴調用的程式裡,當遞迴到底底部的時候,結果已經計算得出,剩下所需要做的,就是逐層返回。在一個不是尾遞迴調用的遞迴中,一層的目標會等待
更裡層的結果返回後,再進行計算。為了更清楚地理解,可以對比查詢accLen([a, b, c], 0, L)的追蹤,和查詢len([a, b, c], L) (如下所示):
?- len([a, b, c], L).
Call: (6) len([a, b, c], _G418) ?
Call: (7) len([b, c], _G481) ?
Call: (8) len([c], _G486) ?
Call: (9) len([], _G489) ?
Exit: (9) len([], 0) ?
Call: (9) _G486 is 0+1 ?
Exit: (9) 1 is 0+1 ?
Exit: (8) len([c], 1) ?
Call: (8) _G481 is 1+1 ?
Exit: (8) 2 is 1+1?
Exit: (7) len([b, c], 2) ?
Call: (7) _G418 is 2+1 ?
Exit: (7) 3 is 2+1 ?
Exit: (6) len([a, b, c], 3) ?
在accLen的查詢追蹤裡,當遞迴到底底部,accLen([], 3, _G449),結果就已經計算完畢,剩下只是回傳上去。在len的查詢追蹤裡面,結果的計算依賴遞迴,比如,len([b, c], _G481)的結果,
只能在完成len([c], _G489)的結果後才能進行計算。簡而言之,尾遞迴程式會有更少的中間變數回溯計算,這使得遞迴會更高效。
Learn Prolog Now 翻譯 - 第五章 - 數字運算 - 第二節,數字運算與列表