【问题标题】:How to do list concatenation "the correct" way (using tail-recursion)如何以“正确”的方式进行列表连接(使用尾递归)
【发布时间】:2011-07-01 16:07:43
【问题描述】:

我正在做以下 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)].

【问题讨论】:

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


    【解决方案1】:

    显而易见的解决方案是使用标准库(如果功能存在),在本例中为 lists:append/1。多年来,实现的细节可能会发生变化,但今天的版本非常简单(而不是尾递归):

    %% append(L) appends the list of lists L
    
    -spec append([[T]]) -> [T].
    
    append([E]) -> E;
    append([H|T]) -> H ++ append(T);
    append([]) -> [].
    

    【讨论】:

    • append([E]) 这里不需要,因为append([H|T]) 已经处理了这种情况,即。 e. [E] = [42|[]], E => 42.
    • 我认为这是针对常见情况的优化。这是来自 lists.erl 的实现,不是我的代码。
    【解决方案2】:

    我还在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
    

    【讨论】:

    • 列表,使用CPS版本,构造如下:[1|[2|[3|[4|[5|[]]]]]]
    【解决方案3】:

    [H|concatenate([T|Tail])][H|concatenate(T,Tail)] 都不是尾递归调用,因为这两个调用都是另一个表达式的一部分,因此控制权将返回给表达式,其中包括对您的 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).
    

    这里,在conc/2中,对自身的调用是函数体中的最后一个操作,函数永远不会返回。

    编辑:如果我们忘记了非尾递归调用的优化,@Robert 提到,目前,由于调用函数的返回地址传递到堆栈(堆?)。如果您调用非尾递归函数并在系统内存大小不足以容纳如此数量的返回地址的情况下传递一个相当长的列表,则可能会发生这种情况。

    【讨论】:

    • 我会颠倒conc/2 中的参数顺序。虽然它没有实际意义,也不会影响速度,但我认为它更容易阅读。
    【解决方案4】:

    正如@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,所以最好使用看起来更清晰的样式。除非必须,否则我个人不喜欢使用蓄能器。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2020-11-06
      • 2012-03-27
      • 1970-01-01
      相关资源
      最近更新 更多