【问题标题】:How come list element lookup is O(1) in Python?为什么列表元素查找在 Python 中是 O(1)?
【发布时间】:2019-03-12 02:31:12
【问题描述】:

今天在课堂上,我们了解到在 Python 中从列表中检索元素是O(1)。为什么会这样?假设我有一个包含四个项目的列表,例如:

li = ["perry", 1, 23.5, "s"]

这些项目在内存中的大小不同。所以不可能把li[0]的内存位置加上每个元素大小的三倍得到li[3]的内存位置。那么解释器如何知道li[3] 在哪里而不必遍历列表来检索元素呢?

【问题讨论】:

标签: python arrays list big-o


【解决方案1】:

Python 中的列表实现为指针数组1。那么,当您创建列表时真正发生了什么:

["perry", 1, 23.5, "s"]

你实际上是在创建一个这样的指针数组:

[0xa3d25342, 0x635423fa, 0xff243546, 0x2545fade]

每个指针“指向”内存中的各个对象,因此字符串"perry"将存储在地址0xa3d25342,数字1将存储在0x635423fa等。

由于所有指针的大小都相同,解释器可以实际上将元素大小的 3 倍添加到 li[0] 的地址以获取存储在 li[3] 处的指针。


1 获取更多详细信息:the horse's mouth (CPython source code on GitHub)

【讨论】:

  • @DmitryVerhoturov 没错,但对这个答案没有实际影响。引用是引用计数的,docs.python.org/3/c-api/structures.html#c.PyVarObject
  • 引用在我所知道的每一种语言中都被实现为指针。语义可能略有不同(例如内存管理的差异,或者 C++ 中的引用是不可变的),但最终它们仍然是指针。
  • @TLW 我以前从未见过这些。你在哪里找到的?
  • @Brian Ahh,这是有道理的。如果我可以为那些和我一样好奇的人解释一下,这些数字对于在芯片本身内部进行组合逻辑的固件设计人员很有用。 Big-oh 分析总是针对一些抽象机器进行的,当你在做固件时,建模时间为“门深度”或“线距”是合理的。对于任何做软件的人(尤其是 Python 和其他解释性语言),基于抽象机器进行 big-Oh 分析更有用,其中访问内存需要一些固定数量的周期,因此 O(1)
  • @TLW 这种简单性对于开发人员来说很重要,因为他们永远不会在关注性能的环境中操作,因为工作集接近 EB,但是 O(n) 之间存在显着的性能差异和 O(n log n) 算法相对于他们的简化计算模型。简化模型很好地将注意力集中在算法的最重要方面。
【解决方案2】:

当您说a = [...] 时,a 实际上是指向 PyObject 的指针,其中包含指向 PyObjects 的指针数组。

当您请求a[2] 时,解释器首先跟随指向列表的PyObject 的指针,然后将2 添加到其中的数组地址,然后返回该指针。如果您要求a[0]a[9999],也会发生同样的情况。

基本上,所有 Python 对象都是通过引用而不是值来访问的,即使是像 2 这样的整数字面量也是如此。指针系统中只有一些技巧可以使这一切保持高效。并且指针的大小是已知的,因此它们可以方便地存储在 C 样式的数组中。

【讨论】:

  • 什么是''terp''?
  • @hkBst 我推断它是“interpreter”的缩写。
【解决方案3】:

简答:Python 列表是数组。

长答案:计算机科学术语 list 通常表示单链表(用于函数式编程)或双向链表(用于过程式编程)。这些数据结构支持 O(1) 插入列表的头部(功能上)或不需要搜索的任何位置(程序上)。 Python“列表”没有这些特征。相反,它支持(摊销)O(1) 在列表末尾附加(如 C++ std::vector 或 Java ArrayList)。 Python 列表在 CS 术语中是真正可调整大小的数组。

以下评论 from the Python documentation 解释了 Python ``列表'' 的一些性能特征:

还可以将列表用作队列,其中添加的第一个元素是检索到的第一个元素(“先进先出”);但是,列表对此目的无效。虽然从列表末尾追加和弹出很快,但从列表开头插入或弹出很慢(因为所有其他元素都必须移动一个)。

【讨论】:

  • 我从未听说过单链表与函数式编程特别相关,或者双链表与过程式编程特别相关。这两种类型的列表都是有效的,并且对于两种编程范式(以及除此之外的其他编程范式)都有它们的用例。你能支持这个说法吗?我觉得这很可疑。
  • @KRyan 我很确定 Lisp、Haskell、Ocaml 通常都使用单链表,尤其是在语言中使用更方便的原语。特别是 Lisp 有一堆速记,比如 car/cdr 用于获取列表元素的各个部分。当然,它们在其他任何地方都可以使用,但是 Lisp 和功能性公司经常大量使用它们。比如C++的list是一个双向链表,最近才得到一个forward_list(单类型)
  • 这是一个很好的答案,但我同意关于函数式与过程式语言中的列表实现的说法似乎过于笼统。抽象列表数据类型是实现为数组还是链表并不是高级语言中语言规范的一部分,是吗?我想可以制作一个 Lisp 运行时,其中列表被实现为数组,就像在 cpython 中一样?
  • @HåkenLid:性能特征通常是数据类型规范的一部分,特别是对于那些对性能更加重视的语言。例如see this Q&A about C++。我不知道 Python 有这样一个明确的列表,但您可以从标准“列表”类型公开的接口中获得提示:有 appendextend,但没有 prepend/@ 987654327@.
  • @HåkenLid:在文档保持沉默的情况下,后备立场是 CPython 实现是 Python 的事实上的规范,尽管显然 other list implementations do get discussed
猜你喜欢
  • 2016-09-17
  • 1970-01-01
  • 1970-01-01
  • 2018-07-14
  • 2013-03-06
  • 2011-12-27
  • 1970-01-01
  • 2015-05-23
相关资源
最近更新 更多