Haskell线性时间在线算法

时间:2013-06-03 14:02:23

标签: algorithm haskell text-compression

如果我误用了标题中的大字,请原谅我;我不太了解他们,但希望他们描述我的问题。我写了一个精心设计的方案来尝试根据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

1 个答案:

答案 0 :(得分:6)

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

了解输出中的每个项目如何从深层嵌套的括号中向外运行?这使得它在输入的大小上采用二次时间来获得所有输出。您还会看到前几个项目缓慢产生的行为,当您到达列表末尾时,它会变得越来越快。这种减少解释了这一点。

这里的主要性能问题是将新元素附加到列表的末尾,这会占用与要附加的列表大小成比例的时间。更好的方法是在前面进行利弊,这是一个重要的时间操作。这将导致输出反转,因此您需要反转结果。

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 - 这是(非)行动中的懒惰)。