【问题标题】:What is the most efficient way to add an element to a list only if isn't there yet?仅当还没有元素时才将元素添加到列表中的最有效方法是什么?
【发布时间】:2009-08-23 18:50:41
【问题描述】:

我在 Python 中有以下代码:

def point_to_index(point):
    if point not in points:
        points.append(point)
    return points.index(point)

这段代码效率极低,尤其是因为我预计points 会增长到容纳几百万个元素。

如果点不在列表中,我遍历列表3次:

  1. 查找它并确定它不存在
  2. 转到列表末尾并添加一个新元素
  3. 转到列表末尾直到找到索引

如果它在列表中,我遍历它两次: 1. 寻找它并确定它在那里 2. 几乎走到列表的末尾,直到找到索引

有没有更有效的方法来做到这一点?例如,我知道:

  • 我更有可能使用不在列表中的点来调用此函数。
  • 如果该点在列表中,则它更可能位于末尾而不是开头。

所以,如果我可以的话:

if point not in points:

从尾到头搜索列表,当点已经在列表中时,会提高性能。

但是,我不想这样做:

if point not in reversed(points):

因为我认为reversed(points) 本身会付出巨大的代价。

我也不想在列表的开头添加新点(假设我知道如何在 Python 中这样做),因为这会改变索引,而索引必须保持不变才能使算法起作用。

我能想到的唯一改进是,如果可能的话,从头到尾只通过一次来实现该功能。底线是:

  • 有什么好的方法吗?
  • 有没有更好的方法来优化功能?

编辑:我收到了一些建议,只需通过一次即可实现这一点。 index()有没有办法从头走到尾?

编辑:人们问为什么索引很重要。我正在尝试使用OFF file format 描述一个 3D 表面。此格式使用其顶点和面来描述曲面。首先列出顶点,然后使用顶点索引列表来描述面。这就是为什么一旦我将一个漩涡添加到列表中,它的索引就不能改变。

编辑:有一些建议(例如igor's)使用字典。这是扫描列表的一个很好的解决方案。但是,当我完成后,我需要按照创建的顺序打印出列表。如果我使用字典,我需要打印出按值排序的键。有什么好办法吗?

编辑:我实现了www.brool.comsuggestion。这是最简单和最快的。它本质上是一个有序的字典,但没有开销。表演很棒!

【问题讨论】:

  • 你使用列表而不是集合有什么原因吗?
  • 为什么需要列表中点的索引?如果您可以一次将所有元素添加到一个集合中,您可以在 5 到 10 秒内运行此代码。

标签: python list optimization


【解决方案1】:

你想使用set

>>> x = set()
>>> x
set([])
>>> x.add(1)
>>> x
set([1])
>>> x.add(1)
>>> x
set([1])

一个集合只包含您添加的任何项目的一个实例,这将比手动迭代列表更有效。

如果您以前没有在 Python 中使用过集合,This wikibooks page 看起来是一本不错的入门书。

【讨论】:

  • 正如 OP 所说,需要常量索引怎么样?没有成套顺序。
  • 如果你问 OP 为什么他需要索引,你可能会发现它是依赖于实现的。我打赌一组是正确的答案,尽管 OP 要求了什么。
  • @hugh wat?仅仅因为他需要测试成员资格并不意味着一个集合自动成为正确的数据结构。
  • @Triptych 你是对的,它不会自动成为正确的答案,但 OP 在评论中说他正在发出“[至少] 350,000 次”要求会员资格,这就是集合和字典精通。我们真的不知道 OP 的实际问题的最佳答案是什么,但是,如果没有进一步的信息来说明为什么列表及其索引是关键的,所以在他有限的问题中,这里的解决方案将起作用。与hughdbrown 一样,我怀疑如果有更多信息,涉及集合的解决方案会表现得更好,但目前还只是猜测。
【解决方案2】:

这将最多遍历一次:

def point_to_index(point):
    try: 
        return points.index(point)
    except ValueError:
        points.append(point)
        return len(points)-1

您可能还想试试这个版本,它考虑到匹配项可能在列表末尾附近。请注意,reversed() 即使在非常大的列表上也几乎没有成本 - 它不会创建副本并且不会多次遍历列表。

def point_to_index(point):
    for index, this_point in enumerate(reversed(points)):
        if point == this_point:
            return len(points) - (index+1)
    else:
        points.append(point)
        return len(points)-1

您还可以考虑保持平行的dictset 点来检查成员资格,因为这两种类型都可以在 O(1) 中进行成员资格测试。当然,这会消耗大量内存。

显然,如果以某种方式对点进行排序,您将有许多其他选项来加快此代码的速度,特别是使用二进制搜索来进行成员资格测试。

【讨论】:

  • 这是个好主意,只是我遇到了内存问题,所以我不想添加另一个副本。 dict 有顺序的概念吗?
  • 是的,append() 和 len() 对于列表来说都是 O(1)。
  • 除非你使用 OrderedDict。
  • 我不得不提到许多受人尊敬的程序员认为对程序的逻辑流使用异常处理是不好的做法。异常处理应该保留用于处理异常情况,而不是正常情况。我知道它在这里提供了所需的行为,但尽量不要让它成为一种习惯。
  • @gotgenes - list.index 称其为错误,因此我将其视为错误。我的猜测是语言设计者想阻止人们对 list.index 的结果进行真实测试,并在这种情况下选择使用异常
【解决方案3】:

如果您担心内存使用,但想优化常见情况,请保留一个包含最后 n 个点及其索引的字典。 points_dict = 字典,max_cache = 缓存大小。

def point_to_index(point):
    try:
        return points_dict.get(point, points.index(point))
    except:
        if len(points) >= max_cache:
            del points_dict[points[len(points)-max_cache]]
        points.append(point)
        points_dict[points] = len(points)-1
        return len(points)-1

【讨论】:

【解决方案4】:
def point_to_index(point):
    try:
        return points.index(point)
    except:
        points.append(point)
        return len(points)-1

更新:在 Nathan 的异常代码中添加。

【讨论】:

  • 当点不存在的情况下,这不是一样吗?在异常情况下如何处理? return len(points) - 1? len() 在 Python 中是 O(1) 吗?
  • @Nathan - 我就是这么做的。看我的回答。是的,对于列表,len 是 O(1)。
  • 这是一个巨大的改进!在我跑了一个小时并调用了大约 250000 次之前。现在已经运行了半个小时,函数被调用了350000次,只有这个变化!
【解决方案5】:

正如其他人所说,考虑使用 set 或 dict。您没有解释为什么需要索引。如果只需要为这些点分配唯一的 id(而且我不能轻易找到使用它们的另一个原因),那么 dict 确实会更好,例如,

points = {}
def point_to_index(point):
    if point in points:
        return points[point]
    else:
       points[point] = len(points)
       return len(points) - 1

【讨论】:

  • 有什么办法让我得到按索引排序的点列表吗?
【解决方案6】:

你真正想要的是一个有序的字典(键插入决定了顺序):

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2015-12-26
    • 2012-10-21
    • 2019-05-27
    • 2019-08-24
    • 2017-01-22
    • 1970-01-01
    • 2015-06-04
    相关资源
    最近更新 更多