【问题标题】:Python Deque - 10 minutes worth of dataPython Deque - 10 分钟的数据
【发布时间】:2014-03-12 23:54:28
【问题描述】:

我正在尝试编写一个脚本,该脚本在执行时会附加一条新的可用信息并删除超过 10 分钟的数据。

我想知道在性能方面最有效的方法是跟踪每个信息元素的特定时间,同时删除超过 10 分钟的数据。

我的新手想法是将带有时间戳的信息 - [info, time] - 附加到双端队列,并在一段时间循环中不断评估双端队列的结尾以删除超过 10 分钟的任何内容......我怀疑这是最好的方法。

有人可以举个例子吗?谢谢。

【问题讨论】:

  • 你描述的算法是我会怎么做的。我想不出任何大的改进方法。

标签: python performance time deque


【解决方案1】:

另一个答案,有更多限制:

如果您可以使用例如一分钟的精度来存储数据,那么您只需要 10 个列表。

与我的其他受限答案不同,这并不要求您只在右侧追加;您可以在中间追加(尽管您会在同一分钟内添加任何其他值)。

缺点是您实际上无法删除超过 10 分钟的所有内容;您只能删除第 10 个存储桶中的所有内容,最多可能会关闭 1 分钟。您可以通过选择如何舍入来选择这意味着什么:

  • 以一种方式截断,不会过早丢弃任何内容,但会延迟丢弃所有内容,平均为 30 秒,最坏的情况为 60 秒。
  • 反之截断,任何东西都不会延迟丢弃,但所有东西都会提前丢弃,平均 30 秒,最坏的情况是 60 秒。
  • 半回合,事情早晚都会丢掉,但平均为 0 秒,最坏的情况是 30 秒。

您当然可以使用较小的存储桶,例如 100 个 6 秒间隔的存储桶,而不是 10 个 1 分钟间隔的存储桶,以尽可能减少错误。把那个推得太远,你会破坏效率; 600000 个 1 毫秒间隔的桶的列表几乎与 1M 条目的列表一样慢。*但是如果您需要 1 秒甚至 50 毫秒,那可能没问题。

这是一个简单的例子:

def prune_queue(self):
    now = time.time() // 60
    age = now - self.last_bucket_time
    if age:
        self.buckets = self.buckets[-(10-age):] + [[] for _ in range(age)]
        self.last_bucket_time = now

def enqueue(self, thing):
    self.prune_queue()
    self.buckets[-1].append(thing)

* 当然,您可以将其与对数数据结构相结合——600000 个桶的红黑树就可以了。

【讨论】:

    【解决方案2】:

    如果您只追加到末尾,并且值始终按排序顺序排列,那么您实际上根本不需要像树或堆这样的对数数据结构;您可以在任何排序的随机访问结构中进行对数搜索,例如listcollections.deque

    问题是删除 listdeque 中任意点的所有内容需要 O(N) 时间。它没有理由应该;您应该能够在摊销的常数时间内从双端队列中删除 N 个元素(使用del q[:pos]q.popleft(pos)),只是collections.deque 没有这样做。如果你发现或编写了一个确实具有该功能的双端队列类,你可以这样写:

    q = deque()
    now = datetime.datetime.now()
    q.append((now, 'a'))
    q.append((now - datetime.timedelta(seconds=5), 'b')
    q.append((now - datetime.timedelta(seconds=10), 'c')
    q.append((now - datetime.timedelta(seconds=15), 'd')
    
    now = datetime.datetime.now()
    pos = bisect.bisect_left(q, now - datetime.timedelta(seconds=10))
    del q[:pos]
    

    我不确定 PyPI 上是否存在这样的 deque,但 the C source to collections.deque 可用于 fork,或者 Python source from PyPy,或者您可以包装 C 或 C++ deque 类型,或者编写一个从头开始……


    或者,如果您期望双端队列中的“当前”值始终是总长度的一小部分,您可以在 O(M) 时间内完成,只需不破坏性地使用双端队列:

    q = q[pos:]
    

    其实在这种情况下,你还不如直接使用list;它在右侧有 O(1) 附加,并且从列表中切出最后 M 个项目是一种低开销的复制 M 个项目的方法,就像你要找到的一样。

    【讨论】:

    • 实际上,这些值总是按顺序排列的。我只是从未想过使用列表,因为我理解双端队列的主要目的是更有效地删除数据 - 无知。但是,“当前”值将始终是总长度的较大子集。大概是 10 倍。
    • @Rook:是的,deque 的主要目的是更有效地从左侧删除数据。后记建议使用list 的原因是,如果您这样做,您将不再从左侧删除数据。但是,如果您当前的数据很大,则可以忽略该后记。
    • @Rook:对于主要答案,您正在从左侧移除。而且,虽然从列表的开头删除 N 个元素的开销大约是 O(N) 操作可以获得的低开销,但它仍然是 O(N);对于双端队列,理论上,它是 O(B),其中 B 是那些 N 个元素使用的块数,应该是摊销常数时间,只是你必须为此编写代码;否则,你必须调用popleft N 次,也就是 O(N),而且比列表切片要慢 O(N)……
    • 不幸的是它会。然而,通过 del q[:pos] 删除 N 个元素比循环双端队列的结尾更让我感兴趣。我想我会尝试走这条路,因为我没有使用水桶的经验。 :)
    • o(n) 比列表切片慢多少?我知道 o(n) 总是取决于大小,但如果我正在处理 100 万个元素。与仅调用 popleft 一百万次相比,列表切片的效率是否大大提高?
    【解决方案3】:

    执行此操作的一种方法是使用以时间戳为键的排序树结构。然后您可以找到 >= 10 分钟前的第一个元素,并删除之前的所有内容。

    bintrees 库为例(因为它的key-slicing语法使得它非常容易读写……):

    q = bintrees.FastRBTree.Tree()
    now = datetime.datetime.now()
    q[now] = 'a'
    q[now - datetime.timedelta(seconds=5)] = 'b'
    q[now - datetime.timedelta(seconds=10)] = 'c'
    q[now - datetime.timedelta(seconds=15)] = 'd'
    
    now = datetime.datetime.now()
    del q[:now - datetime.timedelta(seconds=10)]
    

    这将删除所有内容,但不包括 now-10s,它应该是 cd

    这样,找到要删除的第一个元素需要 log N 时间,并且删除低于该时间的 N 个元素应该是平均情况摊销 log N 但最坏情况 N。因此,您的整体最坏情况时间复杂度不会提高,但您的一般情况下。

    当然,管理树而不是双端队列的开销非常高,如果您正在处理一个非常小的队列,则可能很容易超过 N/log N 步的节省。


    还有其他更适合映射的对数数据结构,例如 pqueue/heapqueue(由 stdlib 中的heapq 实现)或时钟环;我只是选择了一棵红黑树,因为(使用 PyPI 模块)它是最容易演示的。

    【讨论】:

    • 啊,谢谢你的详细解释。队列是可变的,但平均来说会很大。
    • “大”是指“数百”还是“数百万”?虽然确实,比试图弄清楚这一点更好的解决方案是使用timeit 使用真实数据对其进行测试。
    • 几千到几百万。你相信使用双端队列会占上风吗?如果没有,我肯定会在这里使用 timeit - 我不着急。
    • @Rook:这只是最不值得猜测的范围。 :) 同时,看看我的其他答案,看看那里增加的限制是否适用于您的用例。如果是这样,可能会有更好的解决方案。
    猜你喜欢
    • 1970-01-01
    • 2022-01-11
    • 2017-02-13
    • 2017-03-09
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2019-01-02
    相关资源
    最近更新 更多