为什么缺点列表与函数式编程相关联?


22

我注意到,大多数功能语言都使用单链接列表(“ cons”列表)作为其最基本的列表类型。示例包括Common Lisp,Haskell和F#。这与主流语言不同,后者的本地列表类型是数组。

这是为什么?

对于Common Lisp(是动态键入的),我得到的想法是,缺点非常普遍,足以成为列表,树等的基础。这可能是一个微小的原因。

但是,对于静态类型的语言,我找不到很好的推理,甚至可以找到反参数:

  • 功能性风格会促进不变性,因此链表的插入容易程度较弱,
  • 功能风格鼓励不变性,因此也鼓励数据共享。数组比链表更容易“部分”共享,
  • 您也可以对常规数组进行模式匹配,甚至更好(例如,您可以轻松地从右向左折叠),
  • 最重要的是,您可以免费获得随机访问,
  • 并且(一个实际的优势),如果该语言是静态类型的,则可以采用常规的内存布局并从高速缓存中提高速度。

那么为什么偏爱链表?


4
来自对@ sepp2k答案的评论,我认为an array is easier to share "partially" than a linked list需要澄清您的意思。由于它们具有递归特性,据我所知,事实恰恰相反-您可以通过传递列表中的任何节点来更轻松地部分共享链接列表,而数组则需要花费时间来制作新副本。或者,就数据共享而言,两个链接列表可以指向相同的后缀,而对于数组,这只是简单的。
伊兹方(Izkata)2012年

如果一个数组将自身定义为一个offset,length,buffer三倍,那么您可以通过使用offset + 1,length-1,buffer创建一个新数组来共享一个数组。或将特殊类型的数组作为子数组。
Dobes Vandermeer 2012年

@Izkata在谈论数组时,我们很少只是指一个缓冲区,例如指向C中连续内存的起点的指针。我们通常是指某种结构,它存储长度和指向包装缓冲区的起点的指针。在这样的系统下,切片操作可以返回一个子数组,该子数组的缓冲区指针指向缓冲区的中间(在子数组的第一个元素处),并且其计数使得start + count为您提供最后一个元素。这样的切片操作在时间和空间上都是O(1)
亚历山大-恢复莫妮卡

Answers:


22

最重要的因素是您可以在O(1)时间内添加一个不可变的单链列表,这使您可以像这样在O(n)时间内递归建立n个元素列表:

// Build a list containing the numbers 1 to n:
foo(0) = []
foo(n) = cons(n, foo(n-1))

如果使用不可变数组进行此操作,则运行时间将是二次的,因为每个cons操作都需要复制整个数组,从而导致二次运行时间。

功能风格鼓励不变性,因此也鼓励数据共享。数组比链表更容易“部分”共享

我假设“部分”共享意味着您可以在O(1)的时间内从数组中提取一个子数组,而使用链表,您只能在O(1)的时间内获取尾部,其他所有内容都需要O(n)。那是真实的。

但是,在许多情况下,仅需尾巴即可。而且您必须考虑到,如果您无法廉价地创建数组,那么能够廉价地创建子数组并不能为您提供帮助。而且(没有聪明的编译器优化)没有办法廉价地逐步构建阵列。


那根本不是真的。您可以追加到摊销的O(1)中的数组。
DeadMG 2012年

10
@DeadMG是的,但不是不可变数组。
sepp2k 2012年

“部分共享”-我认为两个缺点列表都可以指向同一个后缀列表(不知道为什么要这么做),并且您可以将中点而不是列表的开头传递给另一个函数而不必复制它(我已经做过很多次了)
Izkata 2012年

@Izkata OP正在谈论部分共享数组,而不是列表。另外,我从未听过您所说的部分共享。那只是分享。
sepp2k 2012年

1
@Izkata OP精确地使用了术语“数组”三遍。曾经说过,FP语言使用链表,而其他语言则使用数组。曾经说过,数组在部分共享方面比链表更出色,并且曾经说过,数组也可以像链表一样进行模式匹配。在所有情况下,他都将数组和链表进行对比(以指出数组作为主数据结构比链表更有用,这导致了他的问题,为什么在FP中首选链表),所以我不知道他是如何做的。可以互换使用这些术语。
sepp2k 2012年

4

我认为可以归结为功能代码中很容易实现的列表。

方案:

(define (cons x y)(lambda (m) (m x y)))

Haskell:

data  [a]  =  [] | a : [a]

数组比较难,而且实现起来也不尽如人意。如果您希望它们非常快,则必须将它们写为低级。

此外,在列表上的递归比数组要好得多。考虑一下您递归使用/生成列表与索引数组的次数。


我不会说将您的方案版本称为链表的实现是正确的。除了功能之外,您将无法使用它存储任何内容。同样,在没有内置支持(或内存块)的任何语言中,数组都很难实现(实际上是不可能的),而链表仅需要实现诸如结构,类,记录或代数数据类型之类的东西。这并非特定于函数式编程语言。
sepp2k 2012年

@ sepp2k您什么意思“除了函数外什么都不要存储”
Pubby 2012年

1
我的意思是,以这种方式定义的列表无法存储不是函数的任何内容。但这不是真的。邓诺,为什么我这么想。对于那个很抱歉。
sepp2k 2012年

2

单链列表是最简单的持久数据结构

持久的数据结构对于高性能,纯功能的编程至关重要。


1
这似乎只是重复点进行,并在解释最多的回答这是4年多前发布
蚊蚋

3
@gnat:最重要的答案没有提到持久性数据结构,或者单链表是最简单的持久性数据结构,或者它们对于执行纯函数式编程至关重要。我发现与最佳答案完全没有重叠。
萧伯纳

2

仅当您具有垃圾收集语言时,才能轻松使用Cons节点。

缺点节点与递归调用和不可变值的功能编程风格非常匹配。因此,它非常适合于心理程序员模型。

并且不要忘记历史原因。为什么它们仍称为Cons Nodes,更糟糕的是仍然使用car和cdr作为访问器?人们通过课本和课程学习它,然后使用它。

没错,在现实世界中,由于缓存级别未命中,数组更易于使用,仅占用一半的内存空间,并且性能更高。没有理由将它们与命令性语言一起使用。


1

链接列表很重要,原因如下:

一旦你采取了一些像3,并将其转换为类似的继任顺序succ(succ(succ(zero))),然后用替代它与{succ=List node with some memory space},和{zero = end of list},你会(长度3)链表结束。

实际的重要部分是数字,替换以及存储空间和零。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.