一个递归算法比另一个好,为什么?

时间:2015-06-25 02:10:16

标签: optimization recursion prolog

以下是last/2的两种类似算法。为什么last_element0/2的效果优于last_element1/2?额外代码last_element0/2的效果几乎与last/2相同,但不如last_element1/2那么优雅。此外,last_element1/2会为太大的列表引发堆栈溢出错误,但last_element0/2不会,为什么?最后,last_element1/2有一个额外的选择点,所以需要削减,不确定这是一个巨大的问题。

last_element0(X,Last) :-
    last_element0(X,_,Last).

last_element0([X|Xs],_,Last) :-
    last_element0(Xs,X,Last).
last_element0([],Last,Last).
last_element1([_|Xs],Last) :-
    last_element1(Xs,Last),
    !.
last_element1([Last],Last).

以下是测试结果:

last_element0/2

1 ?- length(Cs, 1000000), maplist(=(c), Cs),time(last_element0(Cs,X)).
% 1,000,003 inferences, 0.031 CPU in 0.023 seconds (136% CPU, 32000096 Lips)
Cs = [c, c, c, c, c, c, c, c, c|...],
X = c.

last_element1/2

2 ?- length(Cs, 1000000), maplist(=(c), Cs),time(last_element1(Cs,X)).
% 1,000,001 inferences, 0.250 CPU in 0.452 seconds (55% CPU, 4000004 Lips)
Cs = [c, c, c, c, c, c, c, c, c|...],
X = c.

last/2

3 ?- length(Cs, 1000000), maplist(=(c), Cs),time(last(Cs,X)).
% 1,000,001 inferences, 0.031 CPU in 0.022 seconds (142% CPU, 32000032 Lips)
Cs = [c, c, c, c, c, c, c, c, c|...],
X = c.

1 个答案:

答案 0 :(得分:2)

您的last_element0/2 几乎library implementation of last/2相同,至少对于SWI-Prolog而言。它(更快)更快,因为由于底层实现,last_element0/3(辅助谓词)的两个子句被识别为互斥,因此不会创建任何选择点,也不需要丢弃任何选择点。

但是,看看这个:

?- last_element0(Xs, X).
ERROR: Out of local stack

?- last_element1(Xs, X).
ERROR: Out of local stack

?- last(Xs, X).
Xs = [X] ;
Xs = [_G1570, X] ;
Xs = [_G1570, _G1573, X] ;
% ... and so on

罪魁祸首:辅助谓词的子句的顺序

顺便说一句,还有其他方法来定义last/2

last_reverse(List, Last) :- reverse(List, [Last|_]).
last_append(List, Last) :- append(_, [Last], List).

(无耻的自我推销)请参阅this question以及两个答案,以获得有关终止行为,选择点和效率的更多讨论(还有更多链接可供使用)。在回答a question on programmers.stackexchange并在此过程中意识到我一无所知后,我问了这个问题。