内容提要
列表中的一些数字运算,累加器
尾递归调用
列表中的一些数字运算,累加器
关于数字运算最为重要的应用,可能是获取一些数据结构体的一些有用事实,比如列表。例如,知道列表的长度是很有用的。我们将会给出一些使用列表和数字运算的例子。
一个列表的长度是多少?这里有一个递归定义:
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)的结果后才能进行计算。简而言之,尾递归程序会有更少的中间变量回溯计算,这使得递归会更高效。