根据ADT(代数数据类型)理论,两个列表的连接必须采用O(n)
,其中n
是第一个列表的长度。基本上,你必须递归迭代第一个列表,直到找到结束。
从不同的角度来看,可以说第二个列表可以简单地链接到第一个元素的最后一个元素。如果知道第一个列表的结尾,这将花费恒定的时间。
我在这里缺少什么?
答案 0 :(得分:6)
在操作上,Haskell列表通常由指向单链表的第一个单元格的指针(粗略地)表示。这样,tail
只返回指向下一个单元格的指针(它不需要复制任何东西),并且在列表前面的x :
分配一个新的单元格,使它指向旧的list,并返回新指针。旧指针访问的列表没有变化,因此无需复制它。
如果您使用++ [x]
附加值,则无法通过更改其最后一个指针来修改原始首选列表,除非您知道永远不会访问原始列表。更具体地说,考虑一下
x = [1..5]
n = length (x ++ [6]) + length x
如果您在执行x
时修改x++[6]
,则n
的值将变为12,这是错误的。最后一个x
引用了未更改的列表,其长度为5
,因此n
的结果必须为11.
实际上,您无法期望编译器对此进行优化,即使在x
不再使用的情况下也是如此,从理论上讲,它可以在适当的位置更新(a"线性& #34;使用)。会发生的事情是x++[6]
的评估必须为之后重复使用x
的最坏情况做好准备,因此必须复制整个列表x
。
正如@Ben所说,"列表被复制"是不精确的。实际发生的是具有指针的单元格被复制(所谓的"脊柱"在列表上),但元素不是。例如,
x = [[1,2],[2,3]]
y = x ++ [[3,4]]
只需要分配[1,2],[2,3],[3,4]
一次。列表x,y
将共享指向整数列表的指针,这些指针不必重复。
答案 1 :(得分:4)
您要求的内容与我在一段时间内为TCS Stackexchange编写的问题有关:支持功能列表的常量时间连接的数据结构是difference list。
Yasuhiko Minamide in the 90s制定了一种用函数式编程语言处理这种列表的方法。我有效地rediscovered it了一会儿。但是,良好的运行时保证需要在Haskell中不可用的语言级支持。
答案 2 :(得分:3)
这是因为不可改变的状态。列表是一个对象+一个指针,所以如果我们将一个列表想象成一个元组,它可能看起来像这样:
let tupleList = ("a", ("b", ("c", [])))
现在让我们使用“head”函数获取此“列表”中的第一项。这个head函数需要O(1)时间,因为我们可以使用fst:
> fst tupleList
如果我们想要将列表中的第一项替换为另一项,我们可以这样做:
let tupleList2 = ("x",snd tupleList)
也可以在O(1)中完成。为什么?因为列表中绝对没有其他元素存储对第一个条目的引用。由于不可变状态,我们现在有两个列表tupleList
和tupleList2
。当我们制作tupleList2
时,我们没有复制整个列表。因为原始指针是不可变的,所以我们可以继续引用它们,但在列表的开头使用其他东西。
现在让我们尝试获取3个项目列表的最后一个元素:
> snd . snd $ fst tupleList
发生在O(3)中,它等于我们列表的长度,即O(n)。
但是我们不能存储指向列表中最后一个元素的指针并访问O(1)中的那个元素吗?要做到这一点,我们需要一个数组,而不是一个列表。数组允许任何元素的O(1)查找时间,因为它是在寄存器级别上实现的原始数据结构。
(ASIDE:如果你不确定为什么我们会使用链接列表而不是数组,那么你应该多做一些关于数据结构,数据结构算法和各种操作的Big-O时间复杂度的阅读,比如get,轮询,插入,删除,排序等)。
现在我们已经建立了这个,让我们来看看连接。让我们使用新列表tupleList
连结("e", ("f", []))
。为此,我们必须遍历整个列表,就像获取最后一个元素一样:
tupleList3 = (fst tupleList, (snd $ fst tupleList, (snd . snd $ fst tupleList, ("e", ("f", [])))
上述操作实际上比O(n)时间更差,因为对于列表中的每个元素,我们必须重新读取列表到该索引。但是如果我们暂时忽略它并关注关键方面:为了到达列表中的最后一个元素,我们必须遍历整个结构。
您可能会问,为什么我们不在内存中存储最后一个列表项?附加到列表末尾的那种方式将在O(1)中完成。但不是那么快,我们无法在不更改整个列表的情况下更改最后一个列表项。为什么呢?
让我们来看看它的外观:
data Queue a = Queue { last :: Queue a, head :: a, next :: Queue a} | Empty
appendEnd :: a -> Queue a -> Queue a
appendEnd a2 (Queue l, h, n) = ????
如果我修改了“last”,这是一个不可变的变量,我实际上不会修改队列中最后一项的指针。我将创建最后一项的副本。引用该原始项目的所有其他内容将继续引用原始项目。
因此,为了更新队列中的最后一项,我必须更新所有引用它的内容。这只能在最佳O(n)时间内完成。
因此,在我们的传统列表中,我们有最终项目:
List a []
但是如果我们想要改变它,我们会复制它。现在,倒数第二个项目引用了旧版本。所以我们需要更新该项目。
List a (List a [])
但如果我们更新第二个项目,我们会复制它。现在第三个最后一项有一个旧的参考。所以我们需要更新它。重复,直到我们到达列表的头部。我们走了一圈。没有任何东西保留对列表头部的引用,因此编辑需要O(1)。
这就是Haskell没有双重链接列表的原因。这也是无法以传统方式实现“队列”(或至少FIFO队列)的原因。在Haskell中创建队列需要对传统数据结构进行一些认真的重新思考。
如果您对所有这些工作方式更加好奇,请考虑获取该书Purely Funtional Data Structures。
编辑:如果你曾经见过这个:http://visualgo.net/list.html你可能会注意到可视化“插入尾部”发生在O(1)中。但是为了做到这一点,我们需要修改列表中的最后一个条目以给它一个新的指针。更新指针会改变纯功能语言中不允许的状态。希望我的帖子能够清楚地表明这一点。
答案 3 :(得分:0)
为了连接两个列表(称为xs
和ys
),我们需要修改xs
中的最终节点,以便将其链接到(即指向) ys
的第一个节点。
但是Haskell列表是不可变的,所以我们必须首先创建xs
的副本。此操作为O(n)
(其中n
是xs
的长度。)
示例:
xs
|
v
1 -> 2 -> 3
1 -> 2 -> 3 -> 4 -> 5 -> 6 -> 7
^ ^
| |
xs ++ ys ys