怎么做列表串联“正确”的方式(使用尾递归)

时间:2011-02-22 00:49:23

标签: list recursion functional-programming erlang tail-recursion

我正在进行以下Erlang练习:

  

编写一个函数,给出一个列表   列表,将连接它们。例如:

concatenate([[1,2,3], [], [4,five]]) ⇒ [1,2,3,4,five].

我想出了这个:

concatenate([]) ->
    [];
concatenate([[H]|Tail]) ->
    [H|concatenate(Tail)];
concatenate([[]|Tail]) ->
    concatenate(Tail);
concatenate([[H|T]|Tail]) ->
    [H|concatenate([T|Tail])].

哪个有效,但我注意到我正在做[T|Tail]这件事。

第一个问题

这仍然被认为是直接递归吗?

之后,我摆脱了[T|Tail]并使用了累加器(如下所示)。

第二个问题

现在第二个代码是否被认为是尾递归?

book hints使用辅助函数(第二个代码正在做),但看起来相当冗长。是因为我错过了什么吗?

concatenate([]) ->
    [];
concatenate([[H]|Tail]) ->
    [H|concatenate(Tail)];
concatenate([[]|Tail]) ->
    concatenate(Tail);
concatenate([[H|T]|Tail]) ->
    [H|concatenate(T,Tail)].

concatenate([],Tail) ->
    concatenate(Tail);
concatenate([H],Tail) ->
    [H|concatenate(Tail)];
concatenate([H|T],Tail) ->
    [H|concatenate(T,Tail)].

4 个答案:

答案 0 :(得分:9)

正如@Yasir解释的那样,它们都不是尾递归的,但我不会担心它(见下文)。使用辅助函数可以通过消除输入列表的部分重建来改进代码。您的代码有点冗长,可以通过删除conc/1中的一些不必要的子句并始终调用conc/2来简化:

conc([H|T]) -> conc(H, T);
conc([]) -> [].

conc([H|T], Rest) -> [H|conc(T, Rest)];
conc([], Rest) -> conc(rest).

使用累加器拆分尾递归版本将成为:

conc(List) -> conc(List, []).

conc([H|T], Acc) -> conc(H, T, Acc);
conc([], Acc) -> lists:reverse(Acc).

conc([H|T], Rest, Acc) -> conc(T, Rest, [H|Acc]);
conc([], Rest, Acc) -> conc(Rest, Acc).

现在,这些天的速度差异比以前少得多,请参阅Myth: Tail-recursive functions are MUCH faster than recursive functions,所以最好使用看起来更清晰的风格。我个人不想使用累加器,除非我必须这样做。

答案 1 :(得分:5)

[H|concatenate([T|Tail])][H|concatenate(T,Tail)]都不是尾递归调用,因为任一调用都是另一个表达式的一部分,因此控制将返回到表达式,其中包括对{{{ 1}}。

正确的尾递归concatenate/1,2可能类似于:

conc

这里,在-module(concat). -export([concatenate/1]). concatenate(L) -> conc(L, []). conc([], Acc) -> lists:reverse(Acc); conc([[H|T] | L1], Acc)-> conc([T|L1], [H|Acc]); conc([[] | L1], Acc) -> conc(L1, Acc). 中,对自身的调用是函数体中的最后一个操作,函数永远不会返回。

编辑:如果我们忘记了非尾递归调用的优化,@ Robert说,目前,由于调用函数的返回地址传递给了堆栈(堆?)。如果你调用非尾递归函数在一个内存大小不足的系统上传递一个相当长的列表来保存这么多的返回地址,就会发生这种情况。

答案 2 :(得分:1)

我还在Continuation-passing style中实现了列表连接:

-module(concat).
-export([concat_cps/2]).

concat_cps([], F) ->
     F([]);
concat_cps([H|T], F) ->
     concat_cps_1(H, T, F).

concat_cps_1([H|T], Rest, F) ->
     concat_cps_1(T, Rest, fun(X) -> F([H|X]) end);
concat_cps_1([], Rest, F) ->
     concat_cps(Rest, F).

所以,如果某人有足够的递归,他可以使用ClosuresContinuations来增强流量控制,如上所示。

测试:

1> concat:concat_cps([[1,2,3], [], [4,5]], fun(X) -> X end).
[1,2,3,4,5]

concat_cps/2的第二个参数是延续,当concat_cps/2完成时,它接受后者的结果。然后永远不会返回对concat_cps/2的控制。

在上面的示例中,我们只使用了identity morphism,但我们可以传入任何其他有效的延续,它接受一个平面列表,例如: G:

2> concat:concat_cps([[1,2,3], [], [4,5]], fun(X) -> length(X) end).
5

答案 3 :(得分:0)

显而易见的解决方案是使用标准库,如果有功能,在这种情况下它是列表:append / 1。多年来,实现的具体细节可能会发生变化,但今天的版本非常简单(而不是尾递归):

%% append(L) appends the list of lists L

-spec append([[T]]) -> [T].

append([E]) -> E;
append([H|T]) -> H ++ append(T);
append([]) -> [].