【问题标题】:Are these functions tail recursive?这些函数是尾递归的吗?
【发布时间】:2012-12-19 02:33:47
【问题描述】:

我正在学习尾递归,但在确定我的函数是否为尾递归时遇到了一些困难(主要是在我使用另一个函数的函数上)。

我已经实现了以下两个函数,但我不确定它们是否是尾递归的。

第一个是连接两个列表的函数。

conca list [] = list
conca [] result = result
conca (h:t) result = conca (init (h:t)) ( last(h:t):result ) 

concatenate::[a]->[a]->[a]
concatenate list1 list2 = conca list1 list2

函数中的计算是在递归调用之前处理的,但它使用 last 和 init,它们不是尾递归(我在 http://ww2.cs.mu.oz.au/172/Haskell/tourofprelude.html 中检查了它们的定义)

第二个功能是删除给定列表中给定数字的第一次出现。

invert [] result = result
invert (h:t) result = invert t (h:result)

remov n [] aux result = invert result []
remov n (h:t) aux result
        | aux==1 = remov n t 1 (h:result)
        | n==h = remov n t 1 (result)
        | otherwise = remov n t 0 (h:result)

remove n list = remov n list 0 []

参数 aux(可以假定 0 或 1 作为值)用于标记是否已删除出现。

在 remove 函数中,当部分结果通过递归调用向下传递时,列表被倒置,最后列表没有第一次出现而是倒置,因此它必须被倒置才能作为返回结果。

【问题讨论】:

  • 出于效率原因,您可能正在学习尾递归。你读过this question 了吗?它解释了为什么尾调用优化不是您所需要的。另外,关于效率的话题,大量使用initlast 是一场灾难!
  • 请阅读。在这种情况下,我不是在寻找效率,而是在实践中使用尾递归实现一些基本功能。那么,与其使用 init 和 last ,还有什么更好的选择呢?
  • initlast 都遍历整个列表,O(n) 操作也是如此。 headtailO(1),使用起来要好得多,但您可以通过模式匹配获得它们。 (++) 的标准定义是 (x:xs) ++ ys = x:(xs ++ ys),这是一个经典的高效非尾递归惰性函数,它立即产生结果的头部。

标签: haskell recursion tail-recursion


【解决方案1】:
conca (h:t) result = conca (init (h:t)) ( last(h:t):result ) 

是尾调用,但last(h:t):result 以未评估的重击开始,所以它有点像这些嵌套函数调用仍在堆栈上。

conca 模式匹配其第一个参数,因此 init 将在递归调用中进行评估。

conca 在其第二个参数中是非严格的,因此在应用 conca 的递归调用时不会评估这些 thunk。


remov 是尾递归的,是的,但是....

使用TrueFalse 代替01 使您的代码更清晰:

remov n [] found result = invert result []
remov n (h:t) found result
        | found = remov n t True (h:result)
        | n==h = remov n t True (result)
        | otherwise = remov n t False (h:result)

remove n list = remov n list False []

最好不要传递这么多数据,减少n的复制,并使用两个函数而不是一个测试布尔参数的函数:

remove' n list = seek list [] where
   seek []     result = invert result []
   seek (h:t) result | h == n    = got t result
                     | otherwise = seek t (h:result)
   got []    result = invert result []
   got (h:t) result = got t (h:result)

但是got a result 只是计算reverse result ++ a,所以你可以写

remove'' n list = seek list [] where
   seek []     result = invert result []
   seek (h:t) result | h == n    = invert result [] ++ t
                     | otherwise = seek t (h:result)

但是,这一切似乎都相当费力,并且仍然遍历列表两次。为什么不去非尾调用:

removeFast n [] = []
removeFast n (h:t) | h == n = t
                   | otherwise = h:removeFast n t

这样做的好处是立即生成其第一个元素,而不是运行整个列表,并且在找到要删除的元素后,无需进一步计算即可返回 t 的捷径。尝试将length (removeFast 1 [1..100000])length (remove 1 [1..100000]) 比赛(根据您的处理器速度改变零的数量)。


如果您想制作更高效的尾递归conca,您可以使用remove 中的技巧:

conc this result = prepend (invert this []) result

prepend [] result = result
prepend (h:t) result = prepend t (h:result)

和以前一样,您要遍历 this 两次,一次是 inverting 它,另一个是 prepending 它,但这仍然是一种线性算法,并且比分别使用 initlast 好得多元素,它是二次元。

【讨论】:

  • 感谢您的建议!那么,为了使 conca tail 递归,我必须使第二个参数严格吗?
  • @user1493813 我的主要观点是尾递归不是很重要。查看我使用removeFast 获得的速度提升,它不是尾递归,而是执行最少的操作。
  • 你的回答现在让我质疑为什么我的老师要求我们在作业中用尾递归实现这些功能,这就是我必须实现连接的原因,因为如果我不't 我会得到一个 0。
  • @user1493813 它已经是尾递归了,但这与有效利用堆栈不同。
  • @user1493813 不要气馁;您的尾递归技能在大多数语言中都至关重要 - 严格的函数式编译器和良好的命令式编译器可以将尾递归函数优化为循环。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2015-09-08
  • 2015-12-10
  • 1970-01-01
  • 2012-12-30
  • 1970-01-01
相关资源
最近更新 更多