为什么这些反向列表功能之一比另一个更快?

时间:2021-05-15 02:54:24

标签: haskell

https://wiki.haskell.org/99_questions/Solutions/5 上有一个反转列表的解决方案:

reverse :: [a] -> [a]
reverse [] = []
reverse (x:xs) = reverse xs ++ [x]
<块引用>

然而,这个定义比 Prelude 中的定义更浪费,因为它 在累积结果时反复重新计算结果。以下 变异避免了这种情况,因此在计算上更接近 Prelude 版本。

reverse :: [a] -> [a]
reverse list = reverse' list []
  where
    reverse' [] reversed     = reversed
    reverse' (x:xs) reversed = reverse' xs (x:reversed)

我正在尝试了解两者之间的区别。 reconses 是什么意思?

我的想法是 reverse xs ++ [x] 从第一个元素到最后一个元素,然后添加 x,这需要 n 次迭代 (n= lenght of xs)。在第二个中,它将列表的其余部分附加到 x。但我不知道 Haskell 的内部结构,不知道它与其他示例有何不同。

究竟发生了什么?

1 个答案:

答案 0 :(得分:6)

列表定义如下(伪代码,因为涉及到一些语法糖)

data [a] = [] | a : [a]

也就是说,列表要么是空的,要么由第一个元素和列表的其余部分组成。

从概念上讲,列表 [1, 2, 3] 存储在内存中,类似于以下内容。

a diagram of a list

如果您更熟悉命令式语言,可以将列表视为由指向“第一个”元素的指针和指向列表其余部分的指针组成。如果您以前使用过 Lisp,这应该看起来很熟悉。

现在,让我们看看 (++) 做了什么

(++) :: [a] -> [a] -> [a]
(++) []     ys = ys
(++) (x:xs) ys = x : (xs ++ ys)

因为很容易将东西放在列表的开头,(++) 取其左侧列表并将每个元素放到右侧列表中。在此期间,右侧列表是单独保留的。也就是说,由于列表的存储方式,[1] ++ [2..10000] 很快,但 [1..9999] ++ [10000] 很慢。 (++) 的时间复杂度(忽略惰性)仅取决于第一个参数,而不取决于第二个参数。

现在,您的第一个 reverse 实现是

reverse :: [a] -> [a]
reverse [] = []
reverse (x:xs) = reverse xs ++ [x]

这会反复将一个长列表(xs 的“其余部分”)附加到一个短列表 ([x]) 上。请记住,(++) 的左侧参数决定了它运行的速度,而 reverse xs 通常会很长,所以这需要一些时间。它还会多次不必要地不断重建列表,这表明我们可以做得更好。例如,reverse [1, 2, 3, 4] 将执行以下操作

reverse [1, 2, 3, 4]
reverse [2, 3, 4] ++ [1]
... lots of recursive work ...
[4, 3, 2] ++ [1]
... lots more recursive work ...
[4, 3, 2, 1]

虽然第一个递归步骤是必要的(当然,我们必须递归来反转列表),但第二个只是将我们刚刚所做的所有艰苦工作撕开,然后将其重新构建。如果我们能避免这种情况就太好了。

reverse :: [a] -> [a]
reverse list = reverse' list []
  where
    reverse' [] reversed     = reversed
    reverse' (x:xs) reversed = reverse' xs (x:reversed)

进入累积反转功能。在这里,我们使用一个额外的参数以正确的方式“构建”反向列表。与其构建整个反向列表,然后使用 (++) 将某些内容添加到末尾(请记住,添加到大列表的末尾是低效的),我们会在列表末尾跟踪我们想要的所有内容并重复地把东西放在开头(把东西放在链表的开头是非常有效的)。

即使是在一个小列表 [1, 2, 3] 上比较两个反向函数的评估(同样,我假设我们立即强制此处的值,因此忽略了懒惰)

-- First function
reverse [1, 2, 3]
reverse [2, 3] ++ [1]
(reverse [3] ++ [2]) ++ [1]
((reverse [] ++ [3]) ++ [2]) ++ [1]
(([] ++ [3]) ++ [2]) ++ [1]
([3] ++ [2]) ++ [1]
(3 : ([] ++ [2])) ++ [1]
[3, 2] ++ [1]
3 : ([2] ++ [1])
3 : (2 : ([] ++ [1]))
[3, 2, 1]

-- Second function
reverse [1, 2, 3]
reverse' [1, 2, 3] []
reverse' [2, 3] [1]
reverse' [3] [2, 1]
reverse' [] [3, 2, 1]
[3, 2, 1]

请注意第二个函数如何更简洁,并且在不必要的数据和重建结构方面进行了更少的改组。