【问题标题】:Prolog Recursion in lists, last but one element列表中的 Prolog 递归,最后一个元素
【发布时间】:2016-08-09 16:49:36
【问题描述】:

问题是在列表中找到最后一个字符,例如

?- last_but_one(X, [a,b,c,d]).
X = c.

我的代码是:

last_but_one(X, [X,_]).
last_but_one(X, [_|T]) :- last_but_one(X, T).

他们给出的代码是:

last_but_one(X, [X,_]).
last_but_one(X, [_,Y|Ys]) :- last_but_one(X, [Y|Ys]).

当我学习 Haskell 时,我记得当问题询问某个列表中的第 2、3 或第 n 个字符时,结构与提供的答案相同,所以我知道按照他们的方式编写它'我写了它有一些意义。但我似乎仍然可以通过我编写它的方式得到正确的答案。

是不是我写错了?做出答案的人写的代码是否更好?如果是,如何写?

【问题讨论】:

  • 第二个子句的第三个选项:last_but_one(X, [_,Y,Z|T]) :- last_but_ont(X, [Y,Z|T]).,它强制第二个参数是至少 3 个元素的列表,使约束更加精确。 :)
  • 我不同意至少 3 - 2 个元素是可以的,在这种情况下应该还有最后一个元素。为什么您认为至少 3 个元素的约束特别重要?
  • 我只是说它的效率略高。您已经有了last_but_one(X, [X,_]).,因此将另一个与双元素列表匹配的子句作为第二个参数是多余的。
  • @false。很细心。它一定是逃过我的。谢谢!
  • @repeat:它使答案看起来与主题无关。

标签: list recursion prolog


【解决方案1】:

我想说两个答案都一样好,我可能会按照你的方式写。他们在第二个解决方案中所做的是在递归调用之前检查第二个元素不是空列表([])。如果您在以下查询中跟踪两种不同的解决方案:last_but_one(X,[b]).

您会看到两者都给出了相同的答案 (false),但第二种解决方案所需的步骤更少,因为它在进行递归调用之前返回 false。

【讨论】:

  • 啊,明白了。谢谢你的解释。
【解决方案2】:

您的原始版本更易于阅读。特别是,递归规则读取 - 从右到左读取它

last_but_one(X, [_|T]) :- last_but_one(X, T).
                          ^^^^^^^^^^
                              provided X is the lbo-element in T

                       ^^  then, it follows, that (that's an arrow!)
^^^^^^^^^^^^^^^^^^^^^^
      X is also the lbo-element of T with one more element

换句话说:如果您在给定列表T 中已经有一个 lbo 元素,那么您可以构造新列表,其中包含前面也具有相同 lbo 元素的任何其他元素。

人们可能会争论哪个版本在效率方面更可取。如果你真的很喜欢,那就去吧:

last_but_one_f1(E, Es) :-
   Es = [_,_|Xs],
   xs_es_lbo(Xs, Es, E).

xs_es_lbo([], [E|_], E).
xs_es_lbo([_|Xs], [_|Es], E) :-
   xs_es_lbo(Xs, Es, E).

甚至:

last_but_one_f2(E, [F,G|Es]) :-
    es_f_g(Es, F, G, E).

es_f_g([], E, _, E).
es_f_g([G|Es], _, F, E) :-
   es_f_g(Es, F, G, E).

永远不要忘记一般测试:

| ?- last_but_one(X, Es).
Es = [X,_A] ? ;
Es = [_A,X,_B] ? ;
Es = [_A,_B,X,_C] ? ;
Es = [_A,_B,_C,X,_D] ? ;
Es = [_A,_B,_C,_D,X,_E] ? ;
Es = [_A,_B,_C,_D,_E,X,_F] ? ...

以下是我的 olde labtop 上的一些基准测试:

          SICStus     SWI
          4.3.2     7.3.20-1
    --------------+----------+--------
    you   0.850s  |   3.616s |  4.25×
    they  0.900s  |  16.481s | 18.31×
    f1    0.160s  |   1.625s | 10.16×
    f2    0.090s  |   1.449s | 16.10×
    mat   0.880s  |   4.390s |  4.99×
    dcg   3.670s  |   7.896s |  2.15×
    dcgx  1.000s  |   7.885s |  7.89×
    ap    1.200s  |   4.669s |  3.89×

差异很大的原因是f1f2 运行纯粹是确定的,没有任何选择点的创建。

使用

bench_last :-
   \+ ( length(Ls, 10000000),
        member(M, [you,they,f1,f2,mat,dcg,dcgx,ap]), write(M), write(' '),
        atom_concat(last_but_one_,M,P), \+ time(call(P,L,Ls))
   ).

【讨论】:

  • 非常感谢,这真的很有帮助:)
  • 嗯,回头看看,这真的很有趣,为什么 SWI 这么慢。我很久以前就用过它,不记得它这么慢。他们的查询需要 16.481 秒?这太疯狂了。
  • 好吧they 读取两个元素,然后重新创建一个元素。至少,在 SWI 中会发生这种情况。
【解决方案3】:

我同意 @false 的观点,即您自己的版本更易于阅读。

就我个人而言,我发现使用 DCG(参见 )更容易:

last_but_one(X) --> [X,_].
last_but_one(X) -->
    [_],
    last_but_one(X).

作为接口谓词,你可以使用:

last_but_one(L, Ls) :-
    phrase(last_but_one(L), Ls).

我现在想添加一些实际时间。

我们有 3 个版本供比较:

  1. DCG 版本,我称之为last_but_one//1
  2. 你自己的版本,我称之为last_but_one_you/2
  3. 他们的版本,我称之为last_but_one_they/2

测试用例包括查找包含一千万个元素的列表的倒数第二个元素。

我们有:

?-长度(Ls,10_000_000),时间(last_but_one(L,Ls)),假。 9,999,999 次推理,1.400 CPU 在 1.400 秒内(100% CPU,7141982 唇) ?-长度(Ls,10_000_000),时间(last_but_one_you(L,Ls)),错误。 9,999,998 次推理,1.383 CPU 在 1.383 秒内(100% CPU,7229930 唇) ?-长度(Ls,10_000_000),时间(last_but_one_they(L,Ls)),错误。 9,999,998 次推理,5.566 CPU 在 5.566 秒内(100% CPU,1796684 唇)

这表明,他们提供的版本不仅更难阅读,而且对于此基准测试而言,它也是迄今为止最慢的

始终以优雅和可读性为先。很多时候,如果你遵循这个原则,你也可以获得一个快速版本。

【讨论】:

  • 感谢您的回复和时间信息,这很有帮助:)
【解决方案4】:

另一种解决方案:

  • 首先,列表的长度必须 >= 2,
  • 也是列表的最后一个长度元素 = 1

代码:

last_but_one(R,[X|Rest]):-
   (  Rest=[_], R=X
   ;  last_but_one(R,Rest)
   ). 

测试:

| ?- last_but_one(Elem,List).
List = [Elem,_A] ? ;
List = [_A,Elem,_B] ? ;
List = [_A,_B,Elem,_C] ? ;
List = [_A,_B,_C,Elem,_D] ? ;
List = [_A,_B,_C,_D,Elem,_E] ? ;
List = [_A,_B,_C,_D,_E,Elem,_F] ? ;
List = [_A,_B,_C,_D,_E,_F,Elem,_G] ? ;
List = [_A,_B,_C,_D,_E,_F,_G,Elem,_H] ? 
yes

希望这个想法对你有所帮助

【讨论】:

  • 您的定义只产生一个答案! - 还要注意不同的参数顺序。
  • 查询last_but_one(Elem, List) 只产生一个答案,而不是无限多。您定义中的 -> 对此负责。
  • @false 谢谢,我用 ',' 测试它,但是当我在这里复制代码时,我将它更改为 `->'
  • 这取决于你想要什么。可读性或速度。
  • 并且:两者的速度完全一样。请参阅我的答案,其中也包括您的版本 (ap)
【解决方案5】:

这是使用 DCG 的另一种方法。我认为这个解决方案更加“图形化”,但在 SICStus 中似乎相当慢:

last_but_one_dcg(L, Ls) :-
   phrase( ( ..., [L,_] ), Ls).

... --> [].
... --> [_], ... .

所以我们描述了一个列表必须看起来像这样,它有一个 last-but-one 元素。它看起来像这样:前面是任何 (...),然后是末尾的两个元素。

通过扩展phrase/2 会更快一些。请注意,扩展本身不再是符合标准的程序。

last_but_one_dcgx(L, Ls) :-
   ...(Ls, Ls2),
   Ls2 = [L,_].

【讨论】:

    【解决方案6】:

    这里有更多方法可以做到这一点。 我不建议实际使用以下任何方法,但 IMO 它们很有趣,因为它们对其他代码和各个 Prolog 处理器提供的 Prolog 库给出了不同的看法:

    在前三个变体中,我们将“递归部分”委托给内置/库谓词:

    last_but_one_append(X,Es) :-
       append(_, [X,_], Es).
    
    :- use_module(library(lists)).
    last_but_one_reverse(X, Es) :-
       reverse(Es, [_,X|_]).
    
    last_but_one_rev(X, Es) :-  
       rev(Es, [_,X|_]).           % (SICStus only)
    

    或者,我们可以使用香草自制的myappend/3myreverse/2

    myappend([], Bs, Bs).
    myappend([A|As], Bs, [A|Cs]) :-
       myappend(As, Bs, Cs).
    
    last_but_one_myappend(X, Es) :-
       myappend(_, [X,_], Es).
    
    myreverse(Es, Fs) :-
       same_length(Es, Fs),        % for universal termination in mode (-,+)
       myreverse_(Es, Fs, []).
    
    myreverse_([], Fs, Fs).
    myreverse_([E|Es], Fs, Fs0) :-
       myreverse_(Es, Fs, [E|Fs0]).
    
    last_but_one_myreverse(X, Es) :-
       myreverse(Es, [_,X|_]).
    

    让我们进行实验1

    bench_last :-
       \+ ( length(Ls, 10000000),
            member(M, [you,they,f1,f2,mat,dcg,dcgx,ap,
                       append,reverse,rev,
                       myappend,myreverse]),
            write(M), write(' '),
            atom_concat(last_but_one_,M,P),
            \+ time(call(P,_L,Ls))
       ).
    

    以下是使用 SICStus Prolog 和 SWI-Prolog3,4 的运行时2

    SICStus | SICStus | SWI | 4.3.2 | 4.3.3 | 7.3.20 | -------------------+----------+--------| 你0.26s | 0.10s | 0.83s | 3.1× 8.3× 他们0.27s | 0.12s | 1.03s | 3.8× 8.5× f1 0.04s | 0.02s | 0.43s | 10.8× 21.5× f2 0.02s | 0.02s | 0.37s | 18.5× 18.5× 垫0.26s | 0.11s | 1.02s | 3.9× 9.0× dcg 1.06s | 0.77s | 1.47s | 1.3× 1.9× dcgx 0.31s | 0.17s | 0.97s | 3.1× 5.7× ap 0.23s | 0.11s | 0.42s | 1.8× 3.8× 追加 1.50s | 1.13s | 1.57s | 1.0× 1.3× 反向 0.36s | 0.32s | 1.02s | 2.8× 3.1× 转 0.04 秒 | 0.04s | --"-- | 25.6× 25.6× myappend 0.48s | 0.33s | 1.56s | 3.2× 4.7× myreverse 0.27s | 0.26s | 1.11s | 4.1× 4.2×

    编辑:添加了 SICStus Prolog 4.3.3 基准测试数据

    非常令人印象深刻!在 SICStus/SWI 加速列中,> 10% 的差异得到粗体


    脚注 1:此答案中显示的所有测量值均在 Intel 上获得 Haswell处理器 Core i7-4700MQ
    脚注 2rev/2 由 SICStus 提供,但不是由 SWI 提供。我们比较了最快的“反向”库谓词。
    脚注 3:需要 SWI 命令行选项 -G1G 以防止出现 Out of global stack 错误。
    脚注 4:此外,尝试了 SWI 命令行选项 -O(优化),但没有产生任何改进。

    【讨论】:

    • 4.3.3 的加速效果令人印象深刻。这里的模式是什么?看起来创造选择点的程序利润很大。