为什么Python没有本地链接列表实现?

时间:2012-12-12 07:59:29

标签: python arrays algorithm linked-list

我尝试过一些快速实验,比较原生Python列表与链接列表实现(如this)的性能。

本机python列表总是比非本机链表更快(根据理论)。

from linkedlist import *
import time
a = LinkedList()
b = []
for i in range(1000000):
    b.append(i)
    a.add_first(i)
t0 = time.clock()
a.remove(10)
t1 = time.clock()
b.remove(10)
t2 = time.clock()
print t1-t0
print t2-t1

我在上面测试中得到的结果是:

  • 原生链表= 2.00000000001e-05

  • python list = 0.005576

  • 非原生链表= 3.90000000001e-05

所以,我想知道为什么Python没有原生的Linked List数据结构。 在Python的情况下,它在我看来它在算法上可能是有用的 链接列表而不是标准列表,以加快标准库的某些方面。

我的理解是List数据结构是该语言的关键构建块,它使代码更易于维护,并且易于优化以专注于该数据结构。

还有其他原因吗?

3 个答案:

答案 0 :(得分:6)

Python有collections.deque,这是一个原生的双向链表。

答案 1 :(得分:1)

实际上,Python没有本地链接列表实现,我也很想知道为什么。

您可能要考虑的另一种方法是collections.deque(=“双端队列”),它可以在两端提供非常快速的恒定时间O(1)插入。实际上,对于您的特定示例,双端队列是比链接列表更好的选择,因为您不需要在中间插入。

但是,一般而言,请记住,双端队列与链表是不同的数据结构,这一点很重要。链表还可以在中间插入固定时间的O(1),而双端队列仅在中间插入线性时间的O(n)。换句话说,将元素插入到双端队列的中间所花费的时间与该双端队列的当前长度n成正比,并且对于足够大的n,它将比使用链接列表慢。

collections.deque和真实链接列表之间的比较

在内部,collections.deque实际上是使用链表实现的。但是,这是一个实现细节,更重要的是,它被实现为固定大小的元素块的链接列表,而不是作为单个元素的链接列表。

这就是为什么在collections.deque的中间插入为O(n)而不是O(1)的原因:您仍然需要修改整个双端队列的一半内存来容纳新元素N:

before inserting N: [ABCDE]⇄[FGHIJ]⇄[KLM__]
after inserting N:  [ABCDE]⇄[FGNHI]⇄[JKLM_]
changed memory:                ^^^   ^^^^

相反,对于一个真实的链表(=单个元素),在中间插入一个新元素N仅仅包括分配一个新节点并更新四个指针的值,该操作的性能独立于链表的当前大小:

before inserting N: [A]⇄[B]⇄[C]⇄[D]⇄[E]⇄[F]⇄[G]⇄    [H]⇄[I]⇄[J]⇄[K]⇄[L]⇄[M]
after inserting N:  [A]⇄[B]⇄[C]⇄[D]⇄[E]⇄[F]⇄[G]⇄[N]⇄[H]⇄[I]⇄[J]⇄[K]⇄[L]⇄[M]
changed memory:                                ^ ^ ^                           

需要权衡的是,双端队列具有更好的内存局部性,并且需要较少的独立内存分配。例如,在上面的双端队列中插入新元素N根本不需要任何新的内存分配。这就是为什么在实践中,尤其是如果您经常在末尾而不是在中间插入时,双端队列实际上是比链表更好的选择。

请注意,在双端队列的中间插入元素为O(n),在开始或结尾插入新元素为O(1):

before:                [ABCDE]⇄[FGNHI]⇄[JKLM_]

prepending P:  [____P]⇄[ABCDE]⇄[FGNHI]⇄[JKLM_]
                    ^ ^

prepending Q:  [___QP]⇄[ABCDE]⇄[FGNHI]⇄[JKLM_]
                   ^

appending R:   [___QP]⇄[ABCDE]⇄[FGNHI]⇄[JKLMR]
                                            ^

appending S:   [___QP]⇄[ABCDE]⇄[FGNHI]⇄[JKLMR]⇄[S____]
                                              ^ ^

注意事项

当然,要使链接列表的插入实际上为O(1),这假定您已经具有要插入节点h的句柄n,在此之前或之后您要插入新节点{{1} }。在LinkedList的假设实现中,它可能类似于:

n = linkedlist.insertbefore(h, "some value")

其中:

type(h)     # => <class 'Node'>
type(n)     # => <class 'Node'>
n.value     # => "some value"
n.next == h # => True

如果没有这样的句柄,则insert(i, x)之类的函数仍将是O(n),因为即使插入操作本身是O( 1)。这是我们假设的LinkedList上insert(i, x)的一些假设实现:

def insert(self, i, x):
    node = self.node_from_index(i)       # Find the i-th node: O(n)
    return self.insertbefore(node, x)    # Insert and return new node: O(1)

这意味着仅当您确实保留那些节点句柄时,对于某些问题才值得使用链表。这也使API不太方便,并且即使您小心一点,尽管每个操作都是O(1),但常量通常会大得多。因此,在实践中,它们并不是那么有用,这可能就是为什么它们不是Python中的内置链接列表的原因。

答案 2 :(得分:-1)

这只是因为构建列表占用了大部分时间而不是追加方法。因此,当它不是你所展示的线性时间操作时,(例如:n ^ 2操作)追加方法将比构建更重要,这将导致你想要看到的结果。