为什么F#列表没有尾指针

时间:2011-10-10 07:22:33

标签: f# linked-list

或者用另一种方式说明,如果只有一个头指针,你会得到一个基本的,单链接列表会带来什么样的好处?我能看到的尾指针的好处是:

  • O(1)list concatenation
  • O(1)将内容附加到列表的右侧

与O(n)列表连接(其中n是左侧列表的长度?)相比,这两者都是相当方便的东西。丢弃尾指针有什么好处?

7 个答案:

答案 0 :(得分:11)

与许多其他功能性[-ish]语言一样,F#有一个cons-list(术语最初来自LISP,但概念是相同的)。在F#中,::运算符(或List.Cons)用于保证:请注意签名为‘a –> ‘a list –> ‘a list(请参阅Mastering F# Lists)。

不要将cons-list与不透明的Linked List实现混淆,后者包含一个离散的第一个[/ last]节点 - cons-list中的每个单元都是[不同]列表的开头!也就是说,“列表”只是从给定的缺点单元格开始的单元格链。

当以类似功能的方式使用时,这提供了一些优点:一个是所有“尾部”单元都是共享的,因为每个cons-cell都是不可变的(“数据”可能是可变的,但这是一个不同的问题)没有办法改变“尾巴”单元格,并且包含所有其他包含所述单元格的列表。

由于这个属性,[new]列表可以高效构建 - 也就是说,它们不需要复制 - 只需通过 cons 到前面。此外,将列表解构为head :: tail也是非常有效的 - 再次,没有副本 - 这在递归函数中通常非常有用。

这个不可变属性通常不存在于[标准可变]双链接列表实现中,因为附加会增加副作用:内部'last'节点(类型现在是不透明的)和一个“tail”单元格改变了。 (不可变序列类型,允许“有效恒定时间”追加/更新,例如immutable.Vector in Scala - 然而,这些是重量级对象,而不是缺点不仅仅是一系列细胞聚集在一起。)

如上所述,缺点是缺点不适用于所有任务 - 特别是除了缺点之外,创建一个新列表是O(n)操作,fsvo n,为了更好(或更糟),列表是不可变的。

我建议您创建自己的concat版本,以了解此操作是如何完成的。 (文章Why I love F#: Lists - The Basics涵盖了这一点。)

快乐的编码。


另见相关帖子:Why can you only prepend to lists in functional languages?

答案 1 :(得分:6)

F#列表是不可变的,没有“append / concat”这样的东西,而只是创建新的列表(可能会重用旧列表的一些后缀)。不可变性有许多优点,超出了这个问题的范围。 (所有纯语言,大多数函数式语言都有这种数据结构,它不是F#主义。)

另见

http://diditwith.net/2008/03/03/WhyILoveFListsTheBasics.aspx

它有很好的图片来解释事物(例如,为什么前面的事情比不可变列表的最后便宜)。

答案 2 :(得分:4)

除了其他人所说的:如果你需要高效但不可改变的数据结构(应该是惯用的F#方式),你必须考虑阅读Chris Okasaki, Purely Functional Data Structures。还有thesis可用(本书所基于的)。

答案 3 :(得分:3)

除了已经说过的内容之外,MSDN上的Introducing Functional Programming部分还有一篇关于Working with Functional Lists的文章解释了列表如何工作并在C#中实现它们,所以它可能是一个很好的方法了解它们是如何工作的(以及为什么添加对最后一个元素的引用不允许有效实现append)。

如果您需要将内容附加到列表的末尾以及前面,那么您需要一个不同的数据结构。例如,Norman Ramsey发布了DList的源代码,其中包含properties here(实现不是惯用的F#,但应该很容易修复)。

答案 4 :(得分:2)

如果您希望列表具有更好的追加操作性能,请查看F#PowerPack中的QueueList和FSharpx扩展库中的JoinList

QueueList封装了两个列表。当您使用缺点前置时,它会将元素添加到第一个列表中,就像一个缺点列表。但是,如果要追加单个元素,可以将其推送到第二个列表的顶部。当第一个列表用完元素时,List.rev将在第二个列表上运行,并且交换两个列表并按顺序放回列表并释放第二个列表以附加新元素。

JoinList使用受歧视的联合来更有效地追加整个列表,并且更多地参与其中。

两者显然不太适合标准的cons-list操作,但为其他场景提供了更好的性能。

您可以在文章Refactoring Pattern Matching中详细了解这些结构。

答案 5 :(得分:1)

正如其他人所指出的,F#列表可以用数据结构表示:

List<T> { T Value; List<T> Tail; }

从这里开始,惯例是列表从List引用,直到Tail为空。根据该定义,其他答案中的好处/特征/限制自然而然。

但是,您的原始问题似乎是列表未定义更多的原因:

List<T> { Node<T> Head; Node<T> Tail; }
Node<T> { T Value; Node<T> Next; }

这样的结构允许在列表中附加和前置,而对原始列表的引用没有任何可见的影响,因为它仍然只看到现在扩展列表的“窗口”。虽然这会(有时)允许O(1)连接,但是有一些问题会出现这样的问题:

  • 连接只能使用一次。这可能导致意外的性能行为,其中一个串联是O(1),但下一个是O(n)。比如说:

     listA = makeList1 ()
     listB = makeList2 ()
     listC = makeList3 ()
     listD = listA + listB //modified Node at tail of A for O(1)
     listE = listA + listC //must now make copy of A to concat with C
    

    你可能会争辩说,尽可能节省时间的时间是值得的,但是不知道什么时候会是O(1)以及当O(n)是反对这个特征的强烈论据时会感到惊讶。

  • 所有列表现在占用的空间是原来的两倍,即使您从未打算连接它们。
  • 您现在拥有单独的列表和节点类型。在当前的实现中,我相信F#只使用单一类型,就像我的答案的开头一样。可能有一种方法可以用一种类型做你想要的东西,但这对我来说并不明显。
  • 连接需要改变原始的“尾部”节点实例。虽然这个不应该影响程序,但它是一个突变点,大多数函数式语言都倾向于避免。

答案 6 :(得分:1)

  

或者用另一种方式说明,如果只有一个头指针,你会得到一个基本的,单链接列表会带来什么样的好处?我能看到的尾指针的好处是:

     
      
  • O(1)list concatenation
  •   
  • O(1)将内容附加到列表的右侧
  •   
     

这两个都是相当方便的东西,而不是O(n)列表连接(其中n是左侧列表的长度?)。

如果通过“尾部指针”表示从每个列表到列表中最后一个元素的指针,那么单独使用它不能用于提供您引用的任何一个好处。虽然你可以快速得到列表中的最后一个元素,但你不能对它做任何事情,因为它是不可变的。

你可以写一个可变的双向链表,但是可变性会使使用它的程序更难以推理,因为你用它调用的每个函数都可能改变它。

正如Brian所说,有纯粹功能性的可挂接列表。但是,它们在常见操作上的速度比F#使用的简单单链表慢很多。

  

丢弃尾指针有什么好处?

几乎所有列表操作的空间使用量减少了30%,性能也提高了。