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

     

  • 相关阅读:
    hibernate联合主键 注解方式
    使用Json出现java.lang.NoClassDefFoundError解决方法
    Spring 定时任务2
    Spring 定时任务1
    Javasocket1
    volatile
    Java中byte与16进制字符串的互相转换
    Spring 源码学习
    web服务器工作原理
    SpringMVC国际化
  • 原文地址:https://www.cnblogs.com/seaman-h-zhang/p/4650354.html
Copyright © 2020-2023  润新知