【问题标题】:Efficiently merge two lists高效合并两个列表
【发布时间】:2019-04-20 12:29:42
【问题描述】:

我目前正在学习 Haskell,并使用 List

根据HaskellWiki,如果我想将两个列表合并在一起,我会写:

list1 ++ list2 

但是,根据this 的回答,在大列表上使用 ++ 会效率低下。

通过研究,我遇到了this SO page,但这个问题需要对List 的输出有特定要求。

我尝试了什么

假设如果我有两个数字列表(对于这个例子,假设两个列表都足够大,以至于使用++ 将是低效的,如 SO 答案中所述):

 oldNumbers = [1,5,14,22,37]
 newNumbers = [3,10,17,27,34,69]
 allNumbers = oldNumbers:newNumbers

如您所见,我试图将oldNumbers 添加到newNumbers 的头部,以便之后将其反转(忽略allNumbers 现在是无序的,这是另一天的练习)。

正如你可能猜到的那样,它产生了一个错误

error:
    * Non type-variable argument in the constraint: Num [a]
      (Use FlexibleContexts to permit this)
    * When checking the inferred type
        allNumbers :: forall a. (Num a, Num [a]) => [[a]]

那么,如标题所述,如何有效地合并两个列表?

【问题讨论】:

  • (++) 在线性时间工作的意义上并不是低效的。这个想法是 (++) 但是有时被“滥用”以将单个元素附加到列表中,从而将算法转换为 O(n^2) 而不是 O(n).
  • 正确性第一,效率第二。真的。 -- 那么你是在合并两个排序的列表,合并的列表也必须排序吗?如果没有,一次性++ 绝对没有问题。你引用的答案谈到 repeated 附加 one-element 列表,++ [x] 是“坏的”。
  • @WillNess - 合并后的列表不需要排序。我正在采取一些小步骤,所以现在的任务是合并这两个列表。当我更好地理解 Haskell 中的 List 时,我会 sort 他们。
  • 然后++ 完成这项工作。你想重新实现它,作为一个练习吗?你的问题是关于那个,还是关于你得到的错误?
  • 嘿,这就是我的回答。我从未说过++ 效率低下。

标签: list haskell merge compiler-errors


【解决方案1】:

根据HaskellWiki,如果我想将两个列表合并在一起, 我会写:

list1 ++ list2 

但是,根据this answer,在大列表上使用++ 将是 效率低下。

我认为您需要在此答案中考虑上下文。如果我们用(++) 附加两个列表ab,那么它会在O(m) 中附加两个列表(其中m 是列表的长度) .因此,就时间复杂度而言,这是有效的。没有比这更有效的构造这样的单链表了。

@melpomene 实际上指出了 Haskell 新手常犯的一个错误:他们在列表末尾附加了一个 single 元素。同样,如果您只想将 single 元素附加到列表中,那不是问题。如果您想以这种方式将 n 元素附加到列表,这是一个问题,因此,如果您每次都将单个元素附加到列表中,并且您这样做 n 次,那么算法将是O(n2)

所以简而言之,从时间复杂度的角度来看,(++) 在将两个列表附加在一起时并不慢,在向其附加单例列表时也不慢。然而,就渐近行为而言,通常比重复将带有一个元素的列表附加到列表中更好的方法,因为这样第一个操作数通常会变得更大,并且每次迭代需要 O(n) 仅将单个元素附加到列表的时间。在这种情况下,通常可以在列表的尾部元素上使用递归,或者例如反向构建列表。

(++) 因此并非“本质上”低效,通过以非设计方式使用它,您可以获得低效行为。

++ 会非常慢的一个示例是以下执行“映射”的函数:

badmap :: (a -> b) -> [a] -> [b]
badmap f = go []
    where go temp [] = temp
          go temp (x:xs) = go (temp ++ [f x]) xs

这里有两个问题:

  1. 它不是懒惰的,它需要评估整个列表才能发出第一个元素;和
  2. 它将以二次方时间运行,因为对于输入列表中的每个元素,附加该元素将花费越来越多的时间。

更有效的实现地图的方法是:

goodmap :: (a -> b) -> [a] -> [b]
goodmap f = go
    where go (x:xs) = f x : go xs
          go [] = []

【讨论】:

  • 报告中哪里说列表是“单链接的”?我在那里找不到类似的东西。 ++ 的复杂度似乎也没有指定。
  • @WillNess:从概念上讲,它是一个单链表,因为数据定义中没有直接链接到“父”。此外,这也很奇怪,因为可以重复使用尾巴,所以没有“以前”。时间复杂度来自于标准前奏的 Haskell 报告中的实现:haskell.org/onlinereport/standard-prelude.html
  • 从概念上讲,它也可以在附加 O(1) 的数组上实现。该实现为类型/函数提供值语义,而不是操作语义/性能。
  • @WillNess: 确实,然后还是O(n), O(n!),... 不知何故这种讨论感觉有点像我和一些朋友关于“计算复杂性”第一章的那个:“为什么计算模型不重要”。
  • 我真的很喜欢这个答案,但您能否提供以这种方式将 n 个元素附加到列表的示例代码?这将有助于更好地了解该做什么。
【解决方案2】:

one-time ++ 附加两个列表绝对没有错,不管它们有多长。

您引用的答案(正确地)谈到 重复 附加 one-element 列表,++ [x] 是“坏的”。

【讨论】:

    【解决方案3】:

    如果您想有效地附加任意列表,请使用不同的数据结构!例如,Data.Seq 针对高效追加进行了优化。

    https://www.stackage.org/haddock/lts-12.19/containers-0.5.11.0/Data-Sequence.html#v:-62--60-

    【讨论】:

      猜你喜欢
      • 2019-09-17
      • 2021-03-21
      • 2011-05-27
      • 1970-01-01
      • 2014-03-19
      • 2012-02-23
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多