Learn Prolog Now 翻译 - 第五章 - 数字运算 - 第二节,数字运算与列表

内容提要

 列表中的一些数字运算,累加器

 尾递归调用

 

列表中的一些数字运算,累加器

 关于数字运算最为重要的应用,可能是获取一些数据结构体的一些有用事实,比如列表。例如,知道列表的长度是很有用的。我们将会给出一些使用列表和数字运算的例子。

 一个列表的长度是多少?这里有一个递归定义:

   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)的结果后才能进行计算。简而言之,尾递归程序会有更少的中间变量回溯计算,这使得递归会更高效。

 

你可能感兴趣的:(Prolog)