我注意到在Haskell和OCaml等函数式语言中,您可以使用列表执行2个操作。首先你可以x:xs
,其中x是一个元素,而xs是一个列表,结果是我们得到一个新的列表,其中x在常量时间附加到xs的开头。第二个是x++y
,其中x和y都是列表,结果是我们得到一个新的列表,其中y在线性时间内相对于x中元素的数量附加到x的末尾。
现在我不是语言设计和编译器的专家,但在我看来,这很像一个链接列表的简单实现,其中一个指针指向第一个项目。如果我用C ++这样的语言实现这个数据结构,我会发现添加一个指向最后一个元素的指针通常是微不足道的。在这种情况下,如果以这种方式实现这些语言(假设它们确实使用了所描述的链接列表),则向最后一项添加“指针”将使得将项添加到列表末尾并允许模式匹配更有效。最后一个元素。
我的问题是这些数据结构是否真的实现为链表,如果是这样,为什么不添加对最后一个元素的引用呢?
答案 0 :(得分:11)
是的,它们确实是链接列表。但它们是不可改变的。不变性的优点是您不必担心还有谁有指向同一列表的指针。 您可能会选择编写x++y
,但程序中的其他位置可能依赖于x
保持不变。
使用这些语言的编译器(我是其中之一)的人不担心这个成本,因为有很多其他数据结构可以提供高效的访问:
表示为两个列表的功能队列提供对两端的持续时间访问,并为put
和get
操作分摊常量时间。
像 finger tree 这样更复杂的数据结构可以非常低的成本提供多种列表访问。
如果你只想要恒定时间附加,John Hughes开发了一个优秀,简单的列表表示为函数,它提供了这一点。 (在Haskell库中,它们被称为DList
。)
如果您对这些问题感兴趣,可以从Chris Okasaki的书 Purely Functional Data Structures 以及Ralf Hinze的一些不那么令人生畏的论文中获得很好的信息。
答案 1 :(得分:4)
你说:
第二个是
x++y
,其中x和y都是 列表和结果操作是y 被附加到x的末尾 相对于数字的线性时间 x中的元素。
在像Haskell这样的函数式语言中,这并不是真的。 y被附加到x的副本,因为任何保留在x上的东西都取决于它没有改变。
如果你要复制所有的x,那么抓住它的最后一个节点并没有真正获得任何东西。
答案 2 :(得分:4)
是的,它们是链接列表。在像Haskell和OCaml这样的语言中,您不会在列表末尾添加项目。列表是不可变的。有一个操作可以创建新列表 - 缺点,您之前引用的:
运算符。它需要一个元素和一个列表,并创建一个新的列表,其中元素为head,列表为tail。 x++y
占用线性时间的原因是因为它必须包含x
的{{1}}的最后一个元素,然后使用y的倒数第二个元素> 列表,依此类推x
的每个元素。 x
中的任何cons单元都不能被重用,因为这会导致原始列表也发生变化。指向x
的最后一个元素的指针在这里不会有用 - 我们仍然需要遍历整个列表。
答案 3 :(得分:1)
++只是众多“你可以用列表做的事情”中的一个。现实情况是,列表非常通用,很少使用其他集合。此外,我们的函数式程序员几乎从不觉得需要查看列表的最后一个元素 - 如果需要,最后有一个函数。
但是,仅仅因为列表很方便,这并不意味着我们没有其他数据结构。如果您真的感兴趣,请查看本书http://www.cs.cmu.edu/~rwh/theses/okasaki.pdf(纯功能数据结构)。您将找到树,队列,列表,其中O(1)在尾部追加元素,依此类推。
答案 4 :(得分:0)
Here's a bit of an explanation on how things are done in Clojure:
避免变异状态的最简单方法是使用不可变数据结构。 Clojure提供了一组不可变列表,向量,集合和映射。由于它们无法更改,因此从不可变集合中“添加”或“删除”某些内容意味着创建一个新集合,就像旧集合一样,但需要进行必要的更改。持久性是用于描述属性的术语,其中旧版本的集合在“更改”之后仍然可用,并且该集合保持其对大多数操作的性能保证。具体来说,这意味着无法使用完整副本创建新版本,因为这需要线性时间。不可避免地,使用链接数据结构实现持久性集合,以便新版本可以与先前版本共享结构。 单链接列表和树是基本的功能数据结构,Clojure根据数组映射的哈希尝试添加哈希映射,设置和向量。
(强调我的)
所以基本上它看起来你大多是正确的,至少就Clojure而言。