是否实际使用了功能实现的链表?

时间:2014-10-06 16:55:55

标签: functional-programming linked-list

似乎是类型的功能列表:

节点'a *'节点|没有

在大多数情况下O(n)的运行时间比面向对象的对应物O(1)差,一个例子是排队的 - 功能实现必须非尾递归循环到结束,而面向对象的实现可以只需make last.next = new和last = new。这些列表是否有一个干净的功能实现,没有这个问题?如果没有实际使用的功能列表,为什么?

由于

3 个答案:

答案 0 :(得分:1)

单链表是功能语言中最重要的主要数据结构。 haskell,Common Lisp,Scheme等都非常依赖它。

功能链接列表比你想象的要糟糕得多 - 添加到尾部意味着复制参数链表的每个段并使最后创建的段指向具有新值的新节点,因为你不能改变参数。

在Lazy语言中,这已经完成了,但是因为它很懒,所以在你使用之前没有真正创建。它没有吹嘘堆栈,优化是疯狂的智能。 (例如,Haskell)

在急切的语言中,比如Scheme,您通常要么从头到尾构建一个列表,最后要反转列表。然后将它O(n)。通常,如果您已经在其中创建了所有节点以保存内存,则可以线性反转列表,并且它可以像您没有那样工作。只要符合预期,mapfold-right甚至可以向前连接。在Common LISP中,您可以在代码中执行相同的操作并说它具有功能,只要它从相同的参数计算相同的值并且永远不会改变实际的参数本身。一些实现,如Racket,依赖于不可变列表,因此可以存储有助于速度的信息。例如。 list?需要检查最后一个缺点,检查()是否为#t,但似乎记得结果。如果列表是可变的,就无法做到这一点。

能够改变某些东西与OO无关。您可以在Scheme和Common Lisp中实现它,因为它们是多范式语言而不是纯粹的功能。

这是一个在Scheme中使用单链表(cons)实现的队列,通过在添加时改变尾部(不起作用):

#!r6rs
(import (rnrs)
        (rnrs mutable-pairs))

(define (queue-make)
  (let ((q (list 'head)))
    (set-car! q q)
    q))

(define (queue-empty? q)
  (and (pair? q)
       (null? (cdr q))))

(define (queue-push! q v)
  (let ((tmp (car q))
        (vc (cons v '())))
    (set-cdr! tmp vc)
    (set-car! q vc)))

(define (queue-pop! q)
  (if (null? (cdr q))
      (error 'queue-empty "Empty queue")  ; empty
      (let ((v (cadr q)))
        (set-cdr! q (cddr q))        
        v)))

(define q (queue-make))
(queue-push! q 1)
(queue-push! q 2)
(queue-push! q 3)
(queue-pop! q) ; ==> 1
(queue-pop! q) ; ==> 2
(queue-pop! q) ; ==> 3
(queue-pop! q) ; ==> throws  queue-empty: Empty queue

答案 1 :(得分:0)

  

在大多数情况下O(n)的运行时间比其面向对象的对应物O(1)

更差

这与面向对象无关。除非保持指针,否则任何链接结构都将在尾部附加O(N)。是的,许多语言确实使用链接列表。

更新:此外,指针与面向对象无关。

答案 2 :(得分:0)

  

如果没有实际使用的功能列表,为什么?

鉴于您对经常修改的数据结构感兴趣,关于持久性(链接)列表的唯一用例是作为实现堆栈数据结构的基础。

  • push操作在列表的前面添加一个新值(即它创建一个新的列表元素并重用以前存在的列表的其余部分)。
  • peektop操作返回列表中的第一个值。
  • pop操作返回从第一个元素开始的列表。

您定义的列表可以很好地用作堆栈数据结构,因为所有修改都发生在理想的位置,在添加或删除新元素时,您永远不必重建现有列表的任何部分。堆栈通常不需要按照添加顺序(从前到后)迭代其所有元素/遍历。

除此之外,AFAIK列表不是最佳数据结构,因为当您想要在其前面以外的任何位置编辑它时,您必须重建所有元素,直到发生更改的位置。最坏的情况是在列表的“结尾”附加一个元素。这就是为什么基于树的结构更适合作为持久(“不可变”)数据结构的基础:它们可以重用更多现有节点,而通常只需要替换沿着通向节点的路径的节点应该发生。 (“更改”=从未更改的原始文件中获取新的,不同的数据结构。)

如果您不需要经常修改列表,并且您主要按顺序读取其元素(不是按随机顺序),那么它也可能就足够了。一个优点是,与更复杂的结构(如树)相比,它的内存开销非常小。

  

Node of 'a * 'a node | Nil

     

[...]这些列表是否有一个干净的功能实现,没有这个问题?

显然不是,如果您对“列表”的定义是您给出的那个。但是再次(这可能是一个有点天真的建议),你可以在某种自平衡二叉树的基础上实现一个“列表”:

  • 迭代“list”中的所有元素将意味着以左子节点,父节点,右子节点的顺序遍历树。
  • 在列表末尾附加意味着最右边的叶节点发生了变化;你只需要重建从根节点到最右边叶节点的路径,其他一切都可以重用。
  • 树越深,修改树时可能需要更改的元素就越少。因此,我建议使用自平衡二叉树。

但同样,我不是专家,我的建议可能是天真的。有关功能数据结构的深入处理,请查看下面引用的资源。

进一步阅读: