【问题标题】:What is under the hood when inserting an item to a specific index in a Python list?将项目插入 Python 列表中的特定索引时,幕后是什么?
【发布时间】:2020-11-19 03:52:58
【问题描述】:

我从this answer 了解到,在索引处插入项目的最快方法是这样的:

a = [1, 2, 3, 4, 5]
a[2:2] = ["12345"]

但我不知道为什么。我从this answer 了解到,复杂度永远不会是 O(1)。

我尝试了以下并验证了它应该大于O(1)的说法。

python3 -m timeit --setup="a = list(range(1000))" "a[500:500] = [3]"
50000 loops, best of 5: 3.89 usec per loop

python3 -m timeit --setup="a = list(range(100000))" "a[500:500] = [3]"
10000 loops, best of 5: 20.1 usec per loop

python3 -m timeit --setup="a = list(range(1000000))" "a[500:500] = [3]"
1000 loops, best of 5: 340 usec per loop

我认为我们可以在 O(1) 中 pinpoint the address/pointer 然后我们只需将该地址指向新项目,它会是 O(1)。我认为我应该是错的,因为这会跳过右侧项目的地址。

我试图查看a[2:2] 是什么,但结果只是一个空列表。我认为可以将索引和分配分开。我的意思是如果我们可以先获取特定索引的指针,然后让它指向一个新项目?

In [14]: a = [1, 2, 3, 4, 5]
In [15]: b = a[2:2]
In [16]: b = ["12345"]
In [17]: b
Out[17]: ['12345']
In [18]: a
Out[18]: [1, 2, 3, 4, 5]
In [19]: a[2:2] = ["12345"]
In [20]: a
Out[20]: [1, 2, '12345', 3, 4, 5]

在上面的代码中,我想通过b=a[2:2] 获取指针,然后通过b = ["12345"] 将其重定向到新项目"12345"

引擎盖下发生了什么?任何建议将不胜感激。提前致谢。

【问题讨论】:

  • 当您尝试测量不同大小列表的插入操作时间时,您发现了什么?
  • 这个答案提供了解释和链接,以进一步阅读有关幕后情况的材料:How is Python's List Implemented?
  • 您链接的第一个问题,我无法重现不同的时间。它报告 list.insert 与切片分配相比要慢 8 倍(!)。这似乎不切实际(即使对于旧版本的 Python),我在 Python 3.8 上也获得了相同的性能。此操作是否为 O(1) 取决于列表类型的实现。但对于大多数实现来说,它不会是 O(1)。
  • 平均而言,在list 中插入的时间复杂度为O(1),但在最坏的情况中可能是O(n) 的复杂度。
  • @FishingCode 请注意,它表示 在结尾

标签: python python-3.x list time-complexity


【解决方案1】:

这个a[2:2] = [123] 意味着从第二个索引到第二个索引的空列表被分配给一个非空列表,该列表将从此填充该空间。

还有其他方法可以在列表中插入元素。最常见和最直接的方式之一是list.insert(index, element)

【讨论】:

  • 如何获取那个空列表的指针?我可以用另一种方式吗?分离索引和分配?
  • 我想我的意思是为什么或如何通过填充空白空间来插入项目?什么是空白空间?有地址吗?
  • 我们可以用[] = [3, 4] 填补空白吗?我觉得和a[2:2] = [4]不一样吧?
  • 你应该这样想:b = [], b = [3,4]。在这里,您正在用另一个列表填充该空白区域。这就是你对a[2:2] = [4] 所做的事情,
【解决方案2】:

这个问题需要对 python 的工作原理有一些基本的了解。让我们从给变量赋值开始。 在 python 中,这些值被赋予了一个内存位置,而变量只是该位置的标签。当您更改变量的值时,该内存位置中的值不会更改,而是将新内存分配给新值,并且标签不指向新内存。当内存没有指向它的标签时,它会被垃圾回收。例如

a = 5
b = a
b = 6
print(a, b)

这会给你输出 5 6。a 仍然是 5,b 现在是 6。

现在我们来谈谈python中列表中的切片操作。最简单的定义是,ar[k:m] 给出从索引 k 到 m 的列表,m 不包括。

因此 ar[n:n] 将是一个空列表从 n 开始。(Python 就是这样很棒)。现在,当此语句 ar[k:m] 位于赋值的左侧时,这是对该范围内列表的引用。例如

ar = [0,1,2,3,4,5,6]
ar[1:2] = [13]

这将获取从索引 1 到 2 不包括索引 2 的列表,即 [1] 并将其替换为列表 [13]。因此,ar 现在变为 [0,13,2,3,4,5,6]

现在如果我们这样做

ar[2:5] = [3]

那么 ar 变为 [0,13,3,5,6]

现在是有趣的部分。 ar[2:2] 是一个从索引 2 开始并且没有元素的列表,因此分配不会更改任何现有条目,而是创建一个从索引 2 开始的列表:

ar[2:2] = [1,2,3]

ar 变为 [0,13,1,2,3,3,5,6]

现在,有点令人困惑。当切片操作在赋值运算符的右侧时,则创建列表的副本并将其分配给左侧的变量。

b = ar[2:4]

b 变成 [1,2]

由于 b 是 ar 的副本,因此您对 b 所做的任何更改都不会影响 ar。

【讨论】:

    【解决方案3】:

    注意:第一个引用的问题是关于生成列表的修改版本而不更改原始版本

    使用插入或切片表示法添加单个元素或多或少是一回事。切片表示法更好的是你插入一个子列表,并且子列表越长,差异越大。

    在底层,Python 列表是一个动态大小的数组(C++ 称为向量,Java 称为 ArrayList),在列表中插入某些内容需要:

    • 如果保留的(和未使用的)大小不足以容纳其他内容,则可选择分配一个新数组并复制原始数组
    • 除非您在末尾添加,否则将从插入索引开始的所有内容移至数组末尾
    • 将新内容放在空闲的地方

    这就是原因:

    • 在数组末尾添加一个元素是 O(1)
    • 在数组的开头插入一个元素是 O(n) - 在随机位置插入它的平均值为 n/2,仍然是 O(n)
    • 用一个切片操作在数组中插入 m 个元素是 O(n)
    • 使用 m 个插入操作在数组中插入 m 个元素是 O(n*m)

    如果需要新的分配,这些操作中的任何一个都会产生巨大的开销。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2013-11-28
      • 1970-01-01
      • 2021-03-25
      • 1970-01-01
      • 2015-10-09
      • 2010-10-18
      • 1970-01-01
      • 2019-10-09
      相关资源
      最近更新 更多