Prolog Recursion in lists, last but one element

前端 未结 6 656
闹比i
闹比i 2020-12-21 02:36

The question is to find the last but one character in a list, e.g.

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

My code is:

last         


        
相关标签:
6条回答
  • 2020-12-21 02:53

    Your original version is much simpler to read. In particular, the recursive rule reads - reading it right-to-left

    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
    

    In other words: If you have already an lbo-element in a given list T, then you can construct new lists with any further elements in front that also have the very same lbo-element.

    One might debate which version is preferable as to efficiency. If you are really into that, rather take:

    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).
    

    or even:

    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).
    

    Never forget general testing:

    | ?- 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] ? ...
    

    And here are some benchmarks on my 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×
    

    The reason for the big difference is that both f1 and f2 run purely determinate without any creation of a choicepoint.

    Using

    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))
       ).
    
    0 讨论(0)
  • 2020-12-21 02:59

    I agree with @false that your own version is simpler to read.

    Personally, I find using a DCG (see dcg) even easier:

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

    As interface predicate, you can use:

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

    I now would like to add some actual timings.

    We have 3 versions for comparison:

    1. the DCG version, which I call last_but_one//1
    2. your own version, which I call last_but_one_you/2
    3. their version, which I call last_but_one_they/2.

    The test case consists of finding the penultimate element of a list with ten million elements.

    We have:

    ?- length(Ls, 10_000_000), time(last_but_one(L, Ls)), false.
    9,999,999 inferences, 1.400 CPU in 1.400 seconds (100% CPU, 7141982 Lips)
    
    ?- length(Ls, 10_000_000), time(last_but_one_you(L, Ls)), false.
    9,999,998 inferences, 1.383 CPU in 1.383 seconds (100% CPU, 7229930 Lips)
    
    ?- length(Ls, 10_000_000), time(last_but_one_they(L, Ls)), false.
    9,999,998 inferences, 5.566 CPU in 5.566 seconds (100% CPU, 1796684 Lips)
    

    This shows that not only is the version that they provided much harder to read, it is also by far the slowest for this benchmark.

    Always aim for elegance and readability first. Very often, you also obtain a fast version if you follow this principle.

    0 讨论(0)
  • 2020-12-21 03:02

    I would say both answers are just as good and I would probably have written it the way you did. What they do in the second solution is that they check, before the recursive call, that the second element is not a empty list ([]). If you trace the two different solutions on the following query: last_but_one(X,[b]).

    You'll see that both give the same answer (false), but the second solution takes shorter amount of steps since it returns false before the recursive call is made.

    0 讨论(0)
  • 2020-12-21 03:02

    another solution :

    • firstly, the list must be of length >= 2,
    • also the last length element of the list = 1

    code :

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

    Test :

    | ?- 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
    

    Hope this idea help you

    0 讨论(0)
  • 2020-12-21 03:03

    Here are more ways how you could do it. I wouldn't recommend actually using any of the following methods, but IMO they are interesting as they give a different view on the other codes and on the Prolog library provided by the respective Prolog processors:

    In the first three variants, we delegate the "recursive part" to built-in / library predicates:

    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)
    

    Alternatively, we could use vanilla home-brewed myappend/3 and myreverse/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|_]).
    

    Let's run the experiments1!

    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))
       ).
    

    Here are the runtimes2 using SICStus Prolog and SWI-Prolog3,4:

                   SICStus | SICStus | SWI    |
                     4.3.2 |   4.3.3 | 7.3.20 |
        -------------------+---------+--------|
        you          0.26s |   0.10s |  0.83s |  3.1×  8.3×
        they         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×
        mat          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×
        append       1.50s |   1.13s |  1.57s |  1.0×  1.3×
        reverse      0.36s |   0.32s |  1.02s |  2.8×  3.1×
        rev          0.04s |   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×
    

    Edit: Added SICStus Prolog 4.3.3 benchmarking data

    Very impressive! In the SICStus/SWI speedup column, differences > 10% got bold-faced.


    Footnote 1: All measurements shown in this answer were obtained on an Intel Haswell processor Core i7-4700MQ.
    Footnote 2: rev/2 is offered by SICStus—but not by SWI. We compare the fastest "reverse" library predicate.
    Footnote 3: The SWI command-line option -G1G was required to prevent Out of global stack errors.
    Footnote 4: Also, the SWI command-line option -O (optimize) was tried, but did not yield any improvement.

    0 讨论(0)
  • 2020-12-21 03:11

    Here is another approach using DCGs. I think that this solution is much more "graphical", but it seems quite slow in SICStus:

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

    So we describe how a list must look like such that it has a last-but-one element. It looks like this: Anything (...) in front, and then two elements at the end.

    It gets a bit faster by expanding phrase/2. Note that the expansion itself is no longer a conforming program.

    last_but_one_dcgx(L, Ls) :-
       ...(Ls, Ls2),
       Ls2 = [L,_].
    
    0 讨论(0)
提交回复
热议问题