【问题标题】:Key-ordered dict in PythonPython中的键序字典
【发布时间】:2010-11-22 03:06:39
【问题描述】:

我正在寻找有序关联数组(即有序字典)的可靠实现。我希望按键排序,而不是按插入顺序。

更准确地说,我正在寻找一种节省空间的 int-to-float(或另一个用例的 string-to-float)映射结构的实现:

  • 有序迭代是 O(n)
  • 随机访问是 O(1)

我想出的最好的方法是粘合一个字典和一个键列表,用 bisect 和 insert 保持最后一个排序。

有更好的想法吗?

【问题讨论】:

  • 你不能只是按要求进行转换吗?您需要缓存结果以备后用?并且需要性能要求,因为这被发现是一个瓶颈?
  • 是否需要动态更改内容?
  • 好吧,我没有给出上下文,抱歉。我在内存中有 10 000 个类似的结构。它们中的每一个都包含 10 到 5000 个键/值对。根据模式,我要么为每个映射寻找一个精确的键,要么在其上应用一些函数,这取决于顺序。我无法预先计算结果,因为它们取决于用户输入。分析表明,对字典进行排序确实是一个瓶颈。
  • hughdbrown:他没有缓存整数到浮点数的转换,他正在实现从整数到浮点数的映射。
  • 我必须根据用户输入更改内容。大约 70% 的时间只是附加一个键(按顺序排列的最后一个键)的问题,但有时我必须插入/删除一个中等大小的键。

标签: python data-structures collections dictionary


【解决方案1】:

“随机访问 O(1)”是一个非常严格的要求,它基本上强加了一个底层哈希表——我希望你的意思只是随机读取,因为我认为它可以在数学上得到证明,而不是在一般情况下是不可能的进行 O(1) 次写入以及 O(N) 次有序迭代。

我认为您不会找到适合您需求的预打包容器,因为它们是如此极端—— O(log N) 访问当然会在世界上产生巨大的影响。要获得读取和迭代所需的 big-O 行为,您需要粘合两个数据结构,本质上是一个 dict 和一个堆(或排序列表或树),并使它们保持同步。尽管您没有指定,但我认为您只会得到您想要的那种 amortized 行为 - 除非您真的愿意为插入和删除支付任何性能损失,这就是字面意思您表达的规格,但似乎不太可能是现实生活中的要求。

对于 O(1) 读取和 amortized O(N) 有序迭代,只需将所有键的列表保留在 dict 的一侧。例如:

class Crazy(object):
  def __init__(self):
    self.d = {}
    self.L = []
    self.sorted = True
  def __getitem__(self, k):
    return self.d[k]
  def __setitem__(self, k, v):
    if k not in self.d:
      self.L.append(k)
      self.sorted = False
    self.d[k] = v
  def __delitem__(self, k):
    del self.d[k]
    self.L.remove(k)
  def __iter__(self):
    if not self.sorted:
      self.L.sort()
      self.sorted = True
    return iter(self.L)

如果您不喜欢“摊销 O(N) 订单”,您可以删除 self.sorted 并在 __setitem__ 中重复 self.L.sort()。当然,这使得写入 O(N log N)(而我仍然在 O(1) 处写入)。任何一种方法都是可行的,很难认为其中一种在本质上优于另一种。如果您倾向于进行大量写入然后进行大量迭代,那么上面代码中的方法是最好的;如果它通常是一次写入,一次迭代,另一次写入,另一次迭代,那么它只是一次清洗。

顺便说一句,这无耻地利用了 Python 排序(又名“timsort”)的不寻常(和美妙的;-)性能特征:其中,排序一个大部分已排序但最后附加了一些额外项目的列表基本上是 O(N) (如果与排序的前缀部分相比,附加的项目足够少)。我听说 Java 很快就会获得这种类型,因为 Josh Block 对 Python 类型的技术演讲印象深刻,以至于他当时就开始在笔记本电脑上为 JVM 编写代码。大多数系统(包括我相信今天的 Jython 和 IronPython)基本上都将排序作为 O(N log N) 操作,而不是利用“大部分有序”的输入; “自然归并排序”,由 Tim Peters 塑造成今天 Python 的 timsort,在这方面是一个奇迹。

【讨论】:

  • 我有几乎完全相同的东西,但使用 __setitem__(self, k, v): if not k in self.d: idx = bisect.bisect(self.l, k) # log( n) self.l.insert(idx, k) # 保持列表排序 self.d[k] = v 这可以防止“脏”标志的事情。是否有一篇论文详细描述了 python 排序(如果没有,我会阅读源代码)。但我的问题实际上是关于防止重复键列表的技巧(例如,如果散列很短,最好只复制散列而不是长列)。不过,感谢您对 python 排序的见解。
  • 如果 (1) 消除 self.sorted (2) self.L == None 如果未排序 (3) self.L 是从 iter__() 中的键生成的 (4) __setitem 使 self.L 无效,将其设置为 None?
  • 好主意!如果我有概率 p 在两次迭代之间进行插入,那将减少迭代前的常数(仍然是 O (n log n))相同的因子。作为一个额外的好处,如果我能证明我的更新在我的系列中充分均匀分布(并且鉴于垃圾收集器收集的频率足够高,平均额外的内存负载将仅为 (1-p) * 一个系列的负载(但我有点担心垃圾收集成本)。需要更仔细地研究它的模式。谢谢!
  • python 源代码中有一个 .txt 文件,这是 Tim Peters 关于他的排序算法的一篇非常好的文章;抱歉,现在找不到网址,因为我在手机上,没有 wifi。这里的关键是,将一个项目附加到排序列表并再次排序是 o(n),就像 bisect 然后插入,并且如果在排序之间附加的 m 远小于 n 项目,则渐近更好,我希望这能解释为什么我提出的代码shd 比 cmets 中提到的替代品更好。
  • 找到了一个 wifi 点——python.org 目前没有回复,但在 webpages.cs.luc.edu/~anh/363/handouts/listsort.txt 有一份相关文章的副本。
【解决方案2】:

sortedcontainers 模块提供了满足您要求的SortedDict 类型。它基本上将 SortedList 和 dict 类型粘合在一起。 dict 提供 O(1) 查找,而 SortedList 提供 O(N) 迭代(非常快)。整个模块是纯 Python 的,并且有 benchmark graphs 来支持性能声明(fast-as-C 实现)。 SortedDict 还经过 100% 覆盖率和数小时压力的全面测试。它与 Python 2.6 到 3.4 兼容。

【讨论】:

    【解决方案3】:

    这是我自己的实现:

    import bisect
    class KeyOrderedDict(object):
       __slots__ = ['d', 'l']
       def __init__(self, *args, **kwargs):
          self.l = sorted(kwargs)
          self.d = kwargs
    
       def __setitem__(self, k, v):
          if not k in self.d:
             idx = bisect.bisect(self.l, k)
             self.l.insert(idx, k)
           self.d[k] = v
    
       def __getitem__(self, k):
          return self.d[k]
    
       def __delitem__(self, k):
          idx = bisect.bisect_left(self.l, k)
          del self.l[idx]
          del self.d[k]
    
       def __iter__(self):
          return iter(self.l)
    
       def __contains__(self, k):
          return k in self.d
    

    使用 bisect 保持 self.l 有序,插入是 O(n) (因为插入,但在我的情况下不是杀手,因为我追加的频率远高于真正的插入,所以通常情况是摊销的O(1))。访问是 O(1),迭代是 O(n)。但也许有人发明了(用 C 语言)结构更聪明的东西?

    【讨论】:

      【解决方案4】:

      在这种情况下,有序树通常更好,但随机访问将是 log(n)。您还应该考虑插入和移除成本...

      【讨论】:

      • 您是否有建议的实现(最好有良好的文档,尤其是针对极端情况)?
      • 这似乎是一个有趣的 AVL 排序树实现:pyavl.sourceforge.net
      • 感谢您的链接。我的第一个想法是,将它用作关联数组会进行很多修改(如果我放弃了具有 O(1) 随机访问时间的要求)。
      【解决方案5】:

      您可以通过在每个位置存储一对(value, next_key) 来构建一个允许遍历的字典。

      随机访问:

      my_dict[k][0]   # for a key k
      

      遍历:

      k = start_key   # stored somewhere
      while k is not None:     # next_key is None at the end of the list
          v, k = my_dict[k]
          yield v
      

      保留指向startend 的指针,对于那些只需要添加到列表末尾的情况,您将获得有效的更新。

      中间插入显然是O(n)。如果您需要更快的速度,您可以在其上构建一个skip list

      【讨论】:

      • 显然,您也可以存储指向前一个元素的指针,这样可以更轻松地从中间插入/删除,特别是如果您在顶部放置一个跳过列表。
      【解决方案6】:

      我不确定您使用的是哪个 Python 版本,但如果您想尝试一下,Python 3.1 包含有序字典的官方实现: http://www.python.org/dev/peps/pep-0372/ http://docs.python.org/3.1/whatsnew/3.1.html#pep-372-ordered-dictionaries

      【讨论】:

      • 有序字典是根据键插入排序的,而不是键排序顺序,所以我认为 LeMiz 不能使用这个解决方案。
      【解决方案7】:

      我在 2007 年实现的 ordereddict 包 (http://anthon.home.xs4all.nl/Python/ordereddict/) 包括 sorteddict。 sorteddict 是一个 KSO(Key Sorted Order)字典。它是用 C 语言实现的,非常节省空间,比纯 Python 实现快几倍。缺点是仅适用于 CPython。

      >>> from _ordereddict import sorteddict
      >>> x = sorteddict()
      >>> x[1] = 1.0
      >>> x[3] = 3.3
      >>> x[2] = 2.2
      >>> print x
      sorteddict([(1, 1.0), (2, 2.2), (3, 3.3)])
      >>> for i in x:
      ...    print i, x[i]
      ... 
      1 1.0
      2 2.2
      3 3.3
      >>> 
      

      抱歉回复晚了,也许这个答案可以帮助其他人找到那个图书馆。

      【讨论】:

        【解决方案8】:

        这是一个馅饼:我需要类似的东西。但是请注意,此特定实现是不可变的,一旦创建实例就没有插入:但是,确切的性能与您所要求的并不完全匹配。查找是 O(log n),全扫描是 O(n)。这可以在键/值(元组)对的元组上使用 bisect 模块。即使您不能精确地使用它,您也可能会成功地根据您的需要调整它。

        import bisect
        
        class dictuple(object):
            """
                >>> h0 = dictuple()
                >>> h1 = dictuple({"apples": 1, "bananas":2})
                >>> h2 = dictuple({"bananas": 3, "mangoes": 5})
                >>> h1+h2
                ('apples':1, 'bananas':3, 'mangoes':5)
                >>> h1 > h2
                False
                >>> h1 > 6
                False
                >>> 'apples' in h1
                True
                >>> 'apples' in h2
                False
                >>> d1 = {}
                >>> d1[h1] = "salad"
                >>> d1[h1]
                'salad'
                >>> d1[h2]
                Traceback (most recent call last):
                ...
                KeyError: ('bananas':3, 'mangoes':5)
           """
        
        
            def __new__(cls, *args, **kwargs):
                initial = {}
                args = [] if args is None else args
                for arg in args:
                    initial.update(arg)
                initial.update(kwargs)
        
                instance = object.__new__(cls)
                instance.__items = tuple(sorted(initial.items(),key=lambda i:i[0]))
                return instance
        
            def __init__(self,*args, **kwargs):
                pass
        
            def __find(self,key):
                return bisect.bisect(self.__items, (key,))
        
        
            def __getitem__(self, key):
                ind = self.__find(key)
                if self.__items[ind][0] == key:
                    return self.__items[ind][1]
                raise KeyError(key)
            def __repr__(self):
                return "({0})".format(", ".join(
                                "{0}:{1}".format(repr(item[0]),repr(item[1]))
                                  for item in self.__items))
            def __contains__(self,key):
                ind = self.__find(key)
                return self.__items[ind][0] == key
            def __cmp__(self,other):
        
                return cmp(self.__class__.__name__, other.__class__.__name__
                          ) or cmp(self.__items, other.__items)
            def __eq__(self,other):
                return self.__items == other.__items
            def __format__(self,key):
                pass
            #def __ge__(self,key):
            #    pass
            #def __getattribute__(self,key):
            #    pass
            #def __gt__(self,key):
            #    pass
            __seed = hash("dictuple")
            def __hash__(self):
                return dictuple.__seed^hash(self.__items)
            def __iter__(self):
                return self.iterkeys()
            def __len__(self):
                return len(self.__items)
            #def __reduce__(self,key):
            #    pass
            #def __reduce_ex__(self,key):
            #    pass
            #def __sizeof__(self,key):
            #    pass
        
            @classmethod
            def fromkeys(cls,key,v=None):
                cls(dict.fromkeys(key,v))
        
            def get(self,key, default):
                ind = self.__find(key)
                return self.__items[ind][1] if self.__items[ind][0] == key else default
        
            def has_key(self,key):
                ind = self.__find(key)
                return self.__items[ind][0] == key
        
            def items(self):
                return list(self.iteritems())
        
            def iteritems(self):
                return iter(self.__items)
        
            def iterkeys(self):
                return (i[0] for i in self.__items)
        
            def itervalues(self):
                return (i[1] for i in self.__items)
        
            def keys(self):
                return list(self.iterkeys())
        
            def values(self):
                return list(self.itervalues())
            def __add__(self, other):
                _sum = dict(self.__items)
                _sum.update(other.__items)
                return self.__class__(_sum)
        
        if __name__ == "__main__":
            import doctest
            doctest.testmod()
        

        【讨论】:

          【解决方案9】:

          对于“字符串到浮动”问题,您可以使用 Trie - 它提供 O(1) 访问时间和 O(n) 排序迭代。我所说的“排序”是指“按字母顺序排序”——似乎这个问题的含义是一样的。

          一些实现(每个都有自己的优点和缺点):

          【讨论】:

            【解决方案10】:

            我认为这是其他答案中未提及的一个选项:

            • 使用二叉搜索树 (Treap/AVL/RB) 保留映射。
            • 使用 hashmap(又名字典)来保持相同的映射(再次)。

            这将提供 O(n) 有序遍历(通过树)、O(1) 随机访问(通过哈希图)和 O(log n) 插入/删除(因为您需要同时更新树和哈希)。

            缺点是需要将所有数据保存两次,但是建议将键列表与哈希图一起保存的替代方案在这个意义上并没有好多少。

            【讨论】:

              猜你喜欢
              • 2021-03-28
              • 2011-01-25
              • 1970-01-01
              • 2021-03-22
              • 2022-11-12
              • 1970-01-01
              • 2013-04-13
              • 2010-09-14
              • 1970-01-01
              相关资源
              最近更新 更多