【问题标题】:Ideal data structure with fast lookup, fast update and easy comparison/sorting具有快速查找、快速更新和易于比较/排序的理想数据结构
【发布时间】:2013-10-09 15:42:23
【问题描述】:

我正在寻找一个好的数据结构来包含具有(hash, timestamp) 值的元组列表。基本上,我想通过以下方式使用它:

  • 数据进来,检查它是否已经存在于数据结构中(哈希相等,而不是时间戳)。
  • 如果是,请将时间戳更新为“现在”
  • 如果没有,请将其添加到时间戳为“now”的集合中

定期,我希望删除并返回早于特定时间戳的元组列表(当它们“过期”时,我需要更新各种其他元素)。时间戳不必是特定的(它可以是 unix 时间戳、python datetime 对象或其他一些易于比较的哈希/字符串)。

我正在使用它来接收传入的数据,如果它已经存在则更新它并清除早于 X 秒/分钟的数据。

多个数据结构也可能是一个有效的建议(我最初使用优先级队列 + 集合,但优先级队列对于不断更新值来说不是最佳的)。

也欢迎其他实现相同目标的方法。最终目标是跟踪元素何时 a) 对系统来说是新的,b) 已经存在于系统中,c) 它们何时过期。

【问题讨论】:

    标签: python data-structures


    【解决方案1】:

    这是一个很好的空间。您需要的是 两个 结构,您需要一些东西来告诉您您的密钥(在您的情况下为hash)是否为集合所知。为此,dict 非常适合;我们只需将hash 映射到timestamp,以便您轻松查找每个项目。按时间戳顺序迭代项目是一项特别适合堆的任务,由heapq 模块提供。每次我们看到一个键,我们都会把它作为(timestamp, hash)的元组添加到我们的堆中。

    不幸的是,无法查看堆积列表并删除某些项目(例如,它们已更新为稍后过期)。我们将通过忽略堆中具有与字典中的值不同的时间戳的条目来解决这个问题。

    所以这里是一个开始的地方,你可能可以在包装类中添加方法来支持额外的操作,或者改变数据的存储方式:

    import heapq
    
    
    class ExpiringCache(object):
        def __init__(self):
            self._dict = {}
            self._heap = []
    
        def add(self, key, expiry):
            self._dict[key] = expiry
            heapq.heappush(self._heap, (expiry, key))
    
        def contains(self, key):
            return key in self._dict
    
        def collect(self, maxage):
            while self._heap and self._heap[0][0] <= maxage:
                expiry, key = heapq.heappop(self._heap)
                if self._dict.get(key) == expiry:
                    del self._dict[key]
    
        def items(self):
            return self._dict.items()
    

    创建一个缓存并添加一些项目

    >>> xc = ExpiringCache()
    >>> xc.add('apples', 1)
    >>> xc.add('bananas', 2)
    >>> xc.add('mangoes', 3)
    

    重新添加过期的项目

    >>> xc.add('apples', 4)
    

    收集所有“早于”两个时间单位的东西

    >>> xc.collect(2)    
    >>> xc.contains('apples')
    True
    >>> xc.contains('bananas')
    False
    

    【讨论】:

    • 很好地使用了 heapq - 我正在考虑使用它,但想不出一个更新过期邮票的聪明方法 - 你的解决方案肯定效果很好。我唯一要更改的是collect() 需要返回过期密钥的列表(忽略具有更新时间戳的密钥),因为我需要执行一些过期后操作。谢谢!
    • 我试图了解当使用元组作为项目时 heapq 如何保持正确排序。我发现使用元组作为项目由 heapq 作为一种特殊情况处理,如文档中所述:docs.python.org/2/library/heapq.html(堆元素可以是元组。这对于分配比较值(例如任务优先级)以及主要记录被跟踪)。
    • @OniMitch:tuple 根本不是特例; tuple 以lexicographic 顺序定义排序操作​​,heapq 使用列表中要求堆化的任何内容的自然排序顺序。你可以使用任何东西,当排序时,会以正确的顺序产生元素(heapq 只会避免在列表头部进行交换)
    • 鼓舞人心的解决方案。我在工作中遇到了同样的问题。我在想是否使用两种数据结构(堆和字典)是最好的,因为这听起来需要更多的内存。有没有办法只使用堆来完成这两项工作:检查是否知道集合的键并按时间戳顺序迭代项目?
    • 我想我上面的问题是内存和时间复杂度之间的权衡。要检查是否存在散列,字典需要 O(1),而堆需要 O(n)。要删除或插入,字典需要 O(n),而堆需要 O(log(n))。如果传入的流数据不大,可以很好地放入内存中,使用两个数据结构没什么大不了的,因此我们优先考虑时间复杂度。
    【解决方案2】:

    我能想到的最接近具有您想要的属性的单个结构的是展开树(以您的哈希作为键)。

    通过将最近访问(并因此更新)的节点旋转到根节点,您最终应该在叶子或右子树中获得最近最少访问(因此更新)的数据。

    弄清楚细节(并实现它们)留给读者作为练习......


    注意事项:

    • 最坏情况高度(因此复杂性)是线性的。这不应该发生在一个像样的哈希中
    • 任何只读操作(即不更新时间戳的查找)都会破坏展开树布局和时间戳之间的关系

    更简单的方法是将包含(hash, timestamp, prev, next) 的对象存储在常规字典中,使用prevnext 来保持最新的双向链表。那么你需要的只是 dict 旁边的 headtail 引用。

    插入和更新仍然是常数时间(哈希查找 + 链表拼接),从列表尾部向后走,收集最旧的哈希是线性的。

    【讨论】:

      【解决方案3】:

      除非我误读了您的问题,否则普通的旧 dict 应该是除清除之外的所有内容的理想选择。假设您试图避免在清除期间检查整个字典,我建议保留第二个数据结构来保存 (timestamp, hash) 对。

      此补充数据结构可以是普通的旧 listdeque(来自 collections 模块)。可能bisect 模块可以方便地将时间戳比较的数量保持在最低限度(而不是比较所有时间戳,直到达到截止值),但是因为您仍然必须在需要清除的项目,确定最快的确切细节需要一些测试。

      编辑:

      对于 Python 2.7 或 3.1+,您还可以考虑使用 OrderedDict(来自 collections 模块)。这基本上是一个dict,在类中内置了一个补充的保持顺序的数据结构,所以你不必自己实现它。唯一的问题是它保留的 only 顺序是插入顺序,因此为了您的目的,您需要将其删除(使用@987654330),而不是仅仅将现有条目重新分配给新的时间戳@) 然后分配一个带有新时间戳的新条目。尽管如此,它仍然保留了 O(1) 查找并使您不必自己维护 (timestamp, hash) 对的列表;当需要清除时,您可以直接遍历 OrderedDict,删除条目,直到找到一个时间戳晚于截止日期的条目。

      【讨论】:

      • 这可能很有用,但我不能保证条目按时间戳顺序到达(我可能会得到延迟的条目并且不需要在最后放入)。否则是个好建议!
      • @ChristianP.: 嗯......你描述你的情况的方式(如果传入的哈希存在,将其时间戳更新为“现在”;如果哈希不存在,将其插入时间戳为“现在”)那么时间戳将“到达”无序的唯一方法是如果您先前存在的数据(初始状态)是无序的。那么你不能在接受新数据之前先按时间戳对数据进行排序吗?有一个初始成本,是的,但之后一帆风顺。
      【解决方案4】:

      如果您可以解决偶尔的误报,我认为布隆过滤器可能会很好地满足您的需求(它非常非常快)

      http://en.wikipedia.org/wiki/Bloom_filter

      和一个 python 实现:https://github.com/axiak/pybloomfiltermmap

      编辑:再次阅读您的帖子,我认为这会起作用,但不是存储散列,而是让布隆过滤器为您创建散列。即,我认为您只想将bloomfilter 用作一组时间戳。我假设您的时间戳基本上可能只是一个集合,因为您正在对它们进行哈希处理。

      【讨论】:

      • 从问题中并不清楚哈希是基于时间戳的——对我来说这听起来像是两个独立的元素。
      • @Useless:实际上,我会从这个问题中说,很明显哈希是不是基于时间戳的。 (我同意你的观点,但要强得多。)
      • 你是对的,我想我应该把它称为一个字符串,因为在这种情况下,它是一个哈希并不重要——时间戳和哈希/字符串是配对的,但是哈希/字符串是标识符。
      【解决方案5】:

      检查/更新/设置操作的简单哈希表或字典将是 O(1)。您可以同时将数据存储在一个简单的按时间排序的列表中,用于清除操作。保留一个头尾指针,这样插入也是 O(1) 并且移除就像推进头直到它到达目标时间并从散列中移除你找到的所有条目一样简单。

      开销是每个存储的数据项有一个额外的指针,代码非常简单:

      insert(key,time,data):
        existing = MyDictionary.find(key)
        if existing:  
            existing.mark()
        node = MyNodeType(data,time)  #simple container holding args + 'next' pointer
        node.next = NULL
        MyDictionary.insert(key,node)
        Tail.next = node
        if Head is NULL:  Head = node
      
      clean(olderThan):
        while Head.time < olderThan:
          n = Head.next 
          if not Head.isMarked():  
              MyDictionary.remove(Head.key)
          #else it was already overwritten
          if Head == Tail: Tail = n
          Head = n
      

      【讨论】:

      • 对于单链表,如何处理就地更新要求?
      • 其实变化很小。该列表按时间顺序排列。在相同键的新时间戳上,新节点将替换字典中的旧节点。旧节点保留在列表中,到期时将被清除。替换时标记旧的,这样它们就不会从哈希中清除。
      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 2011-04-02
      • 2014-06-23
      • 1970-01-01
      • 1970-01-01
      • 2018-02-20
      • 2020-08-14
      • 2011-04-09
      相关资源
      最近更新 更多