【问题标题】:Haskell Linear-Time Online AlgorithmHaskell 线性时间在线算法
【发布时间】:2013-05-29 17:09:09
【问题描述】:

如果我用错了标题中的大词,请原谅我;我对他们不太了解,但希望他们能描述我的问题。我编写了一个精心设计的方案来尝试根据these 的要求对字符串进行编码。对于长度为 10^4 或更高的字符串,我编写的代码非常慢,我想知道 - 因为它一次处理 200 个块(尽管有时只向前移动一个字符来获取下一个块),可以吗?被修改以更快或更线性的方式输出结果(例如,每处理 200 个字符立即输出结果)。任何有关该优化或其他明显优化的帮助将不胜感激。

根据电话的建议,我简化了我的示例:

encode xs = encode' xs [] where
  encode' []     result = result
  encode' (z:zs) result
    | null test = encode' zs (result ++ [z])
    | otherwise = encode' (drop numZsProcessed zs) (result ++ processed)
   where test = ..some test
         toProcess = take 200 (z:zs)
         processed = ..do something complicated with toProcess
         numZsProcessed = ..number of z's processed

【问题讨论】:

  • 在 Haskell 中编写常数时间、常数空间在线算法是非常有可能的。不过,用一个更简单的例子来解释这些机制会容易得多。你可以为你的问题创建一个简单的实例吗?要么,要么将问题移至代码审查。
  • @tel 谢谢你的建议。我试图简化我的例子。现在怎么样了?
  • 值得注意的是,上面的代码 (a) 是尾递归的,建立了一个累加器,只有在所有输入都被消耗后才会交付,并且 (b) 计算一个左嵌套的塔++ 应用程序,因为 ++ 在其第一个参数的长度上花费时间线性,因此速度会变慢。尝试使您的代码尽快提供尽可能大的输入前缀,将递归调用放在 ++ 的右侧。
  • @pigworker 感谢您的建议-您能否举一个修改示例来为我指明正确的方向?由于输出确实只是非常缓慢地传递,我假设代码在开始输出之前没有消耗所有输入,不是吗?
  • 尝试使用“朴素”递归而不是尾递归。

标签: algorithm haskell text-compression


【解决方案1】:

Haskell 和尾递归不像其他函数式语言和尾递归那样相处融洽。让我们对一些非常简单的代码进行手动缩减,看看尾递归发生了什么。这是map (1+)的尾递归实现。

go [] r = r
go (x:xs) r = go xs (r ++ [1+x])

另外我们必须牢记(++)的定义:

[] ++ ys = ys
(x:xs) ++ ys = x : (xs ++ ys)

现在让我们减少go [1,2,3,4,5] []。请记住,[x,y,z]x:(y:(z:[])) 的符号,或简称为 x:y:z:[]

go [1,2,3,4,5] []
go [2,3,4,5] ([] ++ [2])   -- 2 here is actually the thunk 1+1, but
                           -- for compactness I am reducing earlier
go [3,4,5] (([] ++ [2]) ++ [3])
go [4,5] ((([] ++ [2]) ++ [3]) ++ [4])
go [5] (((([] ++ [2]) ++ [3]) ++ [4]) ++ [5])
go [] ((((([] ++ [2]) ++ [3]) ++ [4]) ++ [5]) ++ [6])
(((([] ++ [2]) ++ [3]) ++ [4]) ++ [5]) ++ [6]
((([2] ++ [3]) ++ [4]) ++ [5]) ++ [6]
(((2:([] ++ [3]) ++ [4]) ++ [5]) ++ [6]
((2:(([] ++ [3]) ++ [4]) ++ [5]) ++ [6]
(2:((([] ++ [3]) ++ [4]) ++ [5]) ++ [6]
2:(((([] ++ [3]) ++ [4]) ++ [5]) ++ [6])    -- first observable output
2:((([3] ++ [4]) ++ [5]) ++ [6])
2:((3:([] ++ [4]) ++ [5]) ++ [6])
2:(3:(([] ++ [4]) ++ [5]) ++ [6])
2:3:((([] ++ [4]) ++ [5]) ++ [6])           -- second observable output
2:3:(([4] ++ [5]) ++ [6])
2:3:(4:([] ++ [5]) ++ [6])
2:3:4:(([] ++ [5]) ++ [6])                  -- third observable output
2:3:4:([5] ++ [6])
2:3:4:5:([] ++ [6])                         -- fourth observable output
2:3:4:5:6:[]                                -- final output

看看输出中的每个项目需要如何从一系列嵌套的括号向外工作?这导致它需要输入大小的二次时间才能获得所有输出。您还将看到前几个项目产生缓慢的行为,并且当您到达列表末尾时它变得越来越快。这种减少说明了这一点。

这里的主要性能问题是将新元素附加到列表的末尾,这所花费的时间与您要附加到的列表的大小成正比。更好的方法是 cons 在前面,这是一个 constant-time 操作。这将导致输出反转,因此您需要反转结果。

go [] r = reverse r
go (x:xs) r = go xs ((1+x):r)

reverse xs = rev xs []      -- roughly from the report prelude
rev [] r = r
rev (x:xs) r = rev xs (x:r)

还有,让我们减少:

go [1,2,3,4,5] []
go [2,3,4,5] [2]
go [3,4,5] [3,2]
go [4,5] [4,3,2]
go [5] [5,4,3,2]
go [] [6,5,4,3,2]
reverse [6,5,4,3,2]
rev [6,5,4,3,2] []
rev [5,4,3,2] [6]
rev [4,3,2] [5,6]
rev [3,2] [4,5,6]
rev [2] [3,4,5,6]
rev [] [2,3,4,5,6]
[2,3,4,5,6]          -- first and all observable output!

所以这显然比第一个版本要少。这是 Scheme 和 ML 等严格语言中使用的样式,因为它具有良好的内存性能。但是,它也有一些缺点:

  • 必须先消耗所有输入,然后才能产生任何输出。事实上,整个计算是在产生任何结果之前执行的。
  • 因此,当给定一个无限列表时,它永远不会产生任何输出。
  • 它涉及reverse,它需要额外的O(n)时间,与我们正在做的事情无关(反转与为每个元素添加一个并保持顺序有什么关系?)。李>

在像 Haskell 这样的惰性语言中,我们可以做得更好。奇怪而美妙的是,我们的做法是写得更天真。

go [] = []
go (x:xs) = (1+x):go xs

并减少:

go [1,2,3,4,5]
2:(go [2,3,4,5])     -- first observable output
2:3:(go [3,4,5])     -- second observable output
2:3:4:(go [4,5])     -- third observable output
2:3:4:5:(go [6])     -- fourth observable output
2:3:4:5:6:(go [])    -- fifth observable output
2:3:4:5:6:[]         -- final output

它需要更少的工作,甚至在查看列表的其余部分之前就开始产生输出,因此它在流计算中具有良好的性能并且可以处理无限输入。并且实现与您希望的一样简单明了。

我希望这能让您对 Haskell 中尾递归的工作原理有所了解。对于您的示例,我建议删除尾递归并以类似于我们最终的go 的朴素风格重写,使用我希望我从这篇文章中建议的直觉来产生“尽可能大的输入前缀,尽快可能”(请注意,返回 x:xs 会立即产生 x,即使还有更多工作要做来计算 xs——这是(非)行动中的懒惰)。

【讨论】:

  • 您忘记在最终的go 函数中添加递归部分。 go (x:xs) = (1+x): go xs
  • @DiegoNolan 谢谢,已修复。也许它毕竟不像我声称的那么简单;-)
  • 这是一个非常好的答案,如果可以的话,我会投票多次:-)
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2016-02-05
  • 2012-08-09
  • 2011-05-02
  • 2018-08-29
  • 1970-01-01
  • 1970-01-01
  • 2014-11-16
相关资源
最近更新 更多