【问题标题】:Tail Recursions in erlangerlang中的尾递归
【发布时间】:2015-02-18 15:48:33
【问题描述】:

我正在从非常基础的角度学习 Erlang,但遇到了尾递归函数的问题。我希望我的函数接收一个列表并返回一个新列表,其中 element = element + 1。例如,如果我发送 [1,2,3,4,5] 作为参数,它必须返回 [2,3,4 ,5,6]。问题是当我发送确切的参数时,它返回 [[[[[[]|2]|3]|4]|5]|6]。

我的代码是这样的:

-module(test).
-export([test/0]).

test()->
  List = [1,2,3,4,5],
  sum_list_2(List).

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

sum_list_2([Head|Tail], Result)->
  sum_list_2(Tail,[Result|Head +1]);
sum_list_2([], Result)->
  Result.

但是,如果我将我的功能更改为:

sum_list_2([Head|Tail], Result)->
  sum_list_2(Tail,[Head +1|Result]);
sum_list_2([], Result)->
  Result.

它输出 [6,5,4,3,2] 没问题。为什么该函数不能反过来工作([Result|Head+1] 输出 [2,3,4,5,6])?

PS:我知道这个特殊问题可以通过列表推导解决,但我想通过递归来解决。

【问题讨论】:

  • Erlang 中很常见的做法是在对列表进行操作后简单地lists:reverse/1 来解决这个问题。
  • 好的,谢谢,但这似乎不是很理想。不过,短名单不会有问题。
  • 请参阅learnyousomeerlang.com/recursion 以获得有关 Erlang 尾递归的良好指南。它实际上声称相反 - 内置的 lists:reverse/1 函数将为更长的列表提供更多好处。
  • 我实际上是通过该指南学习 Erlang,但我找不到解决问题的实际方法。我想我将不得不阅读更多关于它的信息。谢谢
  • 另外,请查看ferd.ca/erlang-s-tail-recursion-is-not-a-silver-bullet.html,了解有关主体与尾递归的一些信息。对于这个特定问题,我对主体与尾递归(加上列表反转)进行了基准测试,两者本质上是并驾齐驱的。正如其中一个答案所建议的那样,使用 ++ 的速度要慢得多。在对 10000 个元素的快速测试中,运行时间要长 1000 倍。

标签: recursion erlang tail-recursion tail


【解决方案1】:

对于这种操作,您应该使用列表推导:

1> L = [1,2,3,4,5,6].
[1,2,3,4,5,6]
2> [X+1 || X <- L].
[2,3,4,5,6,7]

这是最快和最惯用的方法。

对您的第一个版本的评论:[Result|Head +1] 构建了一个不正确的列表。构造始终是[Head|Tail],其中 Tail 是一个列表。您可以使用Result ++ [Head+1],但这会在每次递归调用时执行结果列表的副本。

你也可以看看lists:map/2的代码,它不是尾递归的,但是在这种情况下编译器的实际优化似乎效果很好:

inc([H|T]) -> [H+1|inc(T)];
inc([]) -> [].

[编辑]

列表的内部和隐藏表示看起来像一个链表。每个元素都包含一个术语和对尾部的引用。所以在头顶添加一个元素不需要修改现有列表,但是在末尾添加一些东西需要改变最后一个元素(对空列表的引用替换为对新子列表的引用)。由于变量是不可变的,它需要制作最后一个元素的修改副本,这反过来又需要改变列表的前一个元素,依此类推。据我所知,编译器的优化并没有做出改变变量的决定(从documentation中扣除)。

【讨论】:

  • 您能否详细说明“这将在每次递归调用时执行结果列表的副本”?我原以为编译器会优化该数组并有效地将新元素附加到原始列表中。
【解决方案2】:

以相反顺序产生结果的函数是您将新增加的元素添加到Result 列表的front 的自然结果。这并不少见,建议的“修复”是在返回之前简单地list:reverse/1 输出。

虽然在这种情况下,您可以简单地使用 ++ 运算符而不是 [H|T] "cons" 运算符来反过来加入您的结果,从而以正确的顺序为您提供所需的输出:

sum_list_2([Head|Tail], Result)->
  sum_list_2(Tail, Result ++ [Head + 1]);

不建议这样做,因为 ++ 运算符 always copies 它是(越来越大的)左侧操作数,导致算法在 O(n^2) 时间内运行,而不是 [Head + 1 | Tail] 版本的 O(n ) 时间。

【讨论】:

  • 这个函数的时间复杂度为O(N^2)
  • @Hynek-Pichi-Vychodil 谢谢 - 我已经调查过了,你是对的,所以我相应地更新了我的答案。
猜你喜欢
  • 1970-01-01
  • 2013-12-17
  • 1970-01-01
  • 1970-01-01
  • 2016-03-21
  • 2018-03-17
  • 1970-01-01
  • 2017-11-11
  • 1970-01-01
相关资源
最近更新 更多