为什么不';tf#列表有一个尾部指针

为什么不';tf#列表有一个尾部指针,f#,linked-list,F#,Linked List,或者换一种说法,拥有一个只有头指针的基本的单链接列表有什么好处?我可以看到尾部指针的好处是: O(1)列表串联 O(1)在列表右侧追加内容 这两种方法都非常方便,而不是O(n)列表串联(其中n是左侧列表的长度?)。删除尾部指针有什么好处?F#列表是不可变的,没有“append/concat”之类的东西,而只是创建新列表(可能会重用旧列表的一些后缀)。不变性有许多优点,超出了这个问题的范围。(所有纯语言和大多数函数式语言都有这种数据结构,它不是F#ism。) 另见 它有很好的图像图来解释事情

或者换一种说法,拥有一个只有头指针的基本的单链接列表有什么好处?我可以看到尾部指针的好处是:

  • O(1)列表串联
  • O(1)在列表右侧追加内容
这两种方法都非常方便,而不是O(n)列表串联(其中n是左侧列表的长度?)。删除尾部指针有什么好处?

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

另见

它有很好的图像图来解释事情(例如,为什么对于一个不可变的列表,在前面考虑比在最后考虑要便宜)。

F#与许多其他函数式[-ish]语言一样,有一个(术语最初来自LISP,但概念是相同的)。在F#中,
运算符(或
List.Cons
)用于Cons'ing:注意签名是
'a–>'a List–>'a List–>'a List
(请参阅)

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

当以类似于函数的方式使用时,这提供了一些优势:一是所有“尾部”单元格都是共享的,而且由于每个cons单元格都是不可变的(“数据”可能是可变的,但这是一个不同的问题),因此无法更改“尾部”单元格并刷新包含该单元格的所有其他列表

由于这一特性,[新]列表可以高效地构建——也就是说,它们不需要拷贝——只需将其压缩到前端即可。此外,将列表解构为
head::tail
,这也是非常有效的—同样,没有副本—这在递归函数中通常非常有用

在[standard mutable]双链表实现中通常不存在此不可变属性,因为追加会增加副作用:内部“last”节点(该类型现在是不透明的)和一个“tail”单元格被更改。(有一些不可变的序列类型允许“有效地固定时间”的追加/更新,例如——然而,与cons列表相比,它们是很重的对象,cons列表只不过是一系列单元格组合在一起。)

如前所述,cons列表也有缺点,它不适用于所有任务——特别是,创建一个新的列表(除非通过向头部进行cons)是一个O(n)操作,fsvon,并且对于更好(或更糟)的情况,该列表是不可变的

我建议您创建自己版本的
concat
,看看这个操作是如何完成的。(本文对此进行了阐述。)

快乐编码



也看到相关的帖子:

< P>除了其他人所说的:如果你需要高效但不可变的数据结构(这应该是一个惯用的方法),你就必须考虑阅读。还有一个可用的(这本书的基础)。

除了已经说过的内容,MSDN部分有一篇文章解释了列表是如何工作的,并且还用C#实现了它们,因此这可能是了解它们如何工作的一个好方法(以及为什么添加对最后一个元素的引用将不允许高效地实现append)


如果需要在列表的末尾和前面添加内容,则需要不同的数据结构。例如,Norman Ramsey发布了
DList
的源代码,其中包含这些内容(实现不是惯用的F#,但应该很容易修复).

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

List<T> { T Value; List<T> Tail; }
这种结构将允许在引用原始列表的情况下对列表进行追加和预加,而不会对列表产生任何可见的影响,因为它仍然只能看到现在扩展的列表的“窗口”。尽管这(有时)允许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#只使用一种类型,就像我的答案开头一样。可能有一种方法可以实现您建议的仅使用一种类型,但我并不清楚
  • 连接需要改变原始的“尾部”节点实例。虽然这不应该影响程序,但这是一个变异点,大多数函数式语言都倾向于避免

如果您想要一个附加操作性能更好的列表,请查看F#PowerPack和FSharpx扩展库中的

QueueList
封装了两个列表。当您使用cons进行前置时,它会将一个元素前置到第一个列表,就像一个cons列表一样。但是,如果您想追加一个元素,可以将其推到第二个列表的顶部。当第一个列表的元素用完时,
list.rev
会在第二个列表上运行,并且这两个元素是相同的交换了将列表按顺序放回并释放第二个列表以附加新元素

JoinList
使用一个有区别的并集来更有效地附加整个列表,这有点过分
 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