【问题标题】:Why Python doesn't have a native Linked List implementation?为什么 Python 没有原生的链表实现?
【发布时间】:2012-11-29 21:24:30
【问题描述】:

我尝试了一些快速实验,比较了原生 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 列表 = 0.005576

  • 非原生链表 = 3.90000000001e-05

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

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

还有其他原因吗?

【问题讨论】:

  • 我在你的测试中看到两个prints 和三个结果——你的“原生”链表来自哪里?
  • 我已经用不同的实现运行了几次测试,而且我用 swig cl.ly/code/2A3t352q1m1Y 构建了这个快速而超级脏的代码
  • 您是在问“为什么开发人员决定从 python 中排除链表 DS?” p.s.我认为这个问题有点主观,适合 SO,也许 Programmers.SE?
  • 你是对的,但是对于标准库来说,在 Python 中实现本机链表以潜在地加速任何处理大量数据的现有应用程序是否有用。跨度>
  • 在你的循环中,你附加到b,但附加到a——这是故意的吗?

标签: python arrays algorithm linked-list


【解决方案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 之前或之后的节点。在 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),因为找到第i个元素是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】:

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

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2015-07-14
      • 2011-10-03
      • 1970-01-01
      • 2012-08-16
      • 2018-11-11
      • 1970-01-01
      • 2011-07-05
      • 1970-01-01
      相关资源
      最近更新 更多