【问题标题】:How to implement an efficient bidirectional hash table?如何实现高效的双向哈希表?
【发布时间】:2011-03-20 02:49:55
【问题描述】:

Python dict 是一个非常有用的数据结构:

d = {'a': 1, 'b': 2}

d['a'] # get 1

有时您还希望按值索引。

d[1] # get 'a'

实现这种数据结构的最有效方法是什么?有什么官方推荐的方法吗?

【问题讨论】:

  • 如果您愿意,我们可以假设值和键一样是不可变的。
  • 你会为这个字典返回什么:{'a' : 1, 'b': 2, 'A' : 1 }
  • @PaulMcGuire:我会返回{1: ['a', 'A'], 2: 'b'}。请参阅我的答案以了解这种方法。
  • 版主注意:这不是stackoverflow.com/questions/1456373/two-way-reverse-map 的重复。后者有 1) 非常模糊的措辞 2) 没有 MCVE 3) 只处理双射映射的情况(参见这个问题中的第一条评论),这比这个更笼统的实际问题更具限制性。所以我认为将其标记为重复在这里,在这种特殊情况下,会产生误导。如果真的一个应该是另一个的副本,那应该是相反的,因为这里的这个涵盖了一般情况,而另一个(见答案)不涵盖非双射的情况。
  • 这个问题十多年了,但我现在是第一次阅读。您可能会在 Java 库 Google Guava 中找到灵感。他们有一门课程HashBiMap 值得一读。 (我假设可以在 Python 中实现类似的结构。)文档清楚地概述了边缘情况以及如何处理它们。参考:github.com/google/guava/blob/master/guava/src/com/google/common/…

标签: python hashtable bidirectional


【解决方案1】:

这是一个双向 dict 的类,受 Finding key from value in Python dictionary 的启发并修改为允许以下 2) 和 3)。

请注意:

  • 1) 反向目录 bd.inverse 在标准字典bd 被修改时自动更新。
  • 2) 反向目录 bd.inverse[value] 始终是key列表,因此bd[key] == value
  • 3) 与 https://pypi.python.org/pypi/bidict 中的 bidict 模块不同,这里我们可以有 2 个具有相同值的键,这非常重要

代码:

class bidict(dict):
    def __init__(self, *args, **kwargs):
        super(bidict, self).__init__(*args, **kwargs)
        self.inverse = {}
        for key, value in self.items():
            self.inverse.setdefault(value,[]).append(key) 

    def __setitem__(self, key, value):
        if key in self:
            self.inverse[self[key]].remove(key) 
        super(bidict, self).__setitem__(key, value)
        self.inverse.setdefault(value,[]).append(key)        

    def __delitem__(self, key):
        self.inverse.setdefault(self[key],[]).remove(key)
        if self[key] in self.inverse and not self.inverse[self[key]]: 
            del self.inverse[self[key]]
        super(bidict, self).__delitem__(key)

用法示例:

bd = bidict({'a': 1, 'b': 2})  
print(bd)                     # {'a': 1, 'b': 2}                 
print(bd.inverse)             # {1: ['a'], 2: ['b']}
bd['c'] = 1                   # Now two keys have the same value (= 1)
print(bd)                     # {'a': 1, 'c': 1, 'b': 2}
print(bd.inverse)             # {1: ['a', 'c'], 2: ['b']}
del bd['c']
print(bd)                     # {'a': 1, 'b': 2}
print(bd.inverse)             # {1: ['a'], 2: ['b']}
del bd['a']
print(bd)                     # {'b': 2}
print(bd.inverse)             # {2: ['b']}
bd['b'] = 3
print(bd)                     # {'b': 3}
print(bd.inverse)             # {2: [], 3: ['b']}

【讨论】:

  • 非常巧妙地解决了模棱两可的情况!
  • 我认为这种数据结构在很多实际问题中非常有用。
  • 这太棒了。很简洁;它是自我记录的;它相当有效;它只是工作。我唯一的问题是优化self[key]__delitem__() 中的重复查找,并将单个value = self[key] 分配重用于此类查找。但是... 是的。 这可以忽略不计。感谢您的精彩,Basj
  • Python 3 版本怎么样?
  • 啊。正确的。尝试不使用“iter”,它应该可以工作。
【解决方案2】:

你可以通过逆序添加键值对来使用相同的字典。

d={'a':1,'b':2} revd=dict([reversed(i) for i in d.items()]) d.update(revd)

【讨论】:

  • +1 一个不错的实用解决方案。另一种写法:d.update( dict((d[k], k) for k in d) ).
  • +1 用于巧妙地使用 reversed()。我不确定它是否比明确的dict((v, k) for (k, v) in d.items()) 更具可读性。在任何情况下,您都可以将对直接传递给 .update:d.update(reversed(i) for i in d.items())
  • 请注意,这会失败,例如对于d={'a':1, 'b':2, 1: 'b'}
  • 轻微修改:dict(map(reversed, a_dict.items())).
  • 向原始字典添加反向映射是一个糟糕的想法。 正如上面的 cmets 所展示的,在一般情况下这样做是安全的。只需维护两个单独的字典。由于忽略尾随d.update(revd) 的此答案的前两行很棒,但是,我仍在考虑投票。 让我们考虑一下。
【解决方案3】:

一个穷人的双向哈希表将只使用两个字典(这些已经是高度优化的数据结构)。

索引上还有一个bidict包:

bidict的源码可以在github上找到:

【讨论】:

  • 2 dicts 需要双重插入和删除。
  • @Juanjo:几乎任何双向/可逆哈希表都将涉及“双重插入和删除”,无论是作为实现结构的一部分,还是作为使用它的一部分。 AFAIK,保持两个索引确实是唯一快速的方法。
  • 当然;我的意思是手动处理 2 索引是问题所在。
  • @Basj 我认为它不被接受是正确的,因为具有多个值意味着它不再是双射并且对于反向查找来说是模棱两可的。
  • @Basj 好吧,我可以理解每个键有多个值的用例会很有用,所以这种类型的数据结构可能应该作为 bidict 的子类存在。但是,由于普通的 dict 映射到单个对象,我认为反过来也更有意义。 (澄清一下,虽然 value 也可以是一个集合,但我的意思是第一个 dict 的 key 应该与 reverse dict 的 value 的类型相同)
【解决方案4】:

下面的 sn-p 代码实现了一个可逆(双射)映射:

class BijectionError(Exception):
    """Must set a unique value in a BijectiveMap."""

    def __init__(self, value):
        self.value = value
        msg = 'The value "{}" is already in the mapping.'
        super().__init__(msg.format(value))


class BijectiveMap(dict):
    """Invertible map."""

    def __init__(self, inverse=None):
        if inverse is None:
            inverse = self.__class__(inverse=self)
        self.inverse = inverse

    def __setitem__(self, key, value):
        if value in self.inverse:
            raise BijectionError(value)

        self.inverse._set_item(value, key)
        self._set_item(key, value)

    def __delitem__(self, key):
        self.inverse._del_item(self[key])
        self._del_item(key)

    def _del_item(self, key):
        super().__delitem__(key)

    def _set_item(self, key, value):
        super().__setitem__(key, value)

这种实现的优点是BijectiveMapinverse 属性又是BijectiveMap。因此,您可以执行以下操作:

>>> foo = BijectiveMap()
>>> foo['steve'] = 42
>>> foo.inverse
{42: 'steve'}
>>> foo.inverse.inverse
{'steve': 42}
>>> foo.inverse.inverse is foo
True

【讨论】:

    【解决方案5】:

    很遗憾,评分最高的答案bidict 不起作用。

    共有三个选项:

    1. Subclass dict:您可以创建dict 的子类,但要小心。您需要编写updatepopinitializersetdefault 的自定义实现。 dict 实现不调用 __setitem__。这就是评分最高的答案有问题的原因。

    2. 从 UserDict 继承:这就像一个 dict,除了所有的例程都被正确调用。它在名为data 的项目中使用了引擎盖下的字典。您可以阅读Python Documentationuse a simple implementation of a by directional list that works in Python 3。很抱歉没有逐字包含它:我不确定它的版权。

    3. 从抽象基类继承:从collections.abc 继承将帮助您获得新类的所有正确协议和实现。这对于双向字典来说是多余的,除非它也可以加密和缓存到数据库。

    TL;DR -- 使用 this 作为您的代码。阅读Trey Hunnerarticle了解详情。

    【讨论】:

    • 好文章。不过,bidict 到底有什么不适用的?
    • 两年前不起作用的东西现在可能起作用,也可能不起作用。
    【解决方案6】:

    可能是这样的:

    import itertools
    
    class BidirDict(dict):
        def __init__(self, iterable=(), **kwargs):
            self.update(iterable, **kwargs)
        def update(self, iterable=(), **kwargs):
            if hasattr(iterable, 'iteritems'):
                iterable = iterable.iteritems()
            for (key, value) in itertools.chain(iterable, kwargs.iteritems()):
                self[key] = value
        def __setitem__(self, key, value):
            if key in self:
                del self[key]
            if value in self:
                del self[value]
            dict.__setitem__(self, key, value)
            dict.__setitem__(self, value, key)
        def __delitem__(self, key):
            value = self[key]
            dict.__delitem__(self, key)
            dict.__delitem__(self, value)
        def __repr__(self):
            return '%s(%s)' % (type(self).__name__, dict.__repr__(self))
    

    如果多个键具有给定值,您必须决定要发生什么;给定对的双向性很容易被您插入的一些后来的对破坏。我实施了一种可能的选择。


    示例:

    bd = BidirDict({'a': 'myvalue1', 'b': 'myvalue2', 'c': 'myvalue2'})
    print bd['myvalue1']   # a
    print bd['myvalue2']   # b        
    

    【讨论】:

    • 我不确定这是否是一个问题,但是使用上面的实现,如果键和值重叠会不会有问题?所以dict([('a', 'b'), ('b', 'c')]); dict['b'] -> 'c' 而不是密钥'a'
    • 这不是 OP 示例的问题,但可能是一个很好的免责声明。
    • 我们如何做到这一点print bd['myvalue2'] 回答b, c(或[b, c],或(b, c),或其他任何东西)?
    【解决方案7】:

    首先,您必须确保键值映射是一对一的,否则无法构建双向映射。

    第二,数据集有多大?如果数据不多,就用2个单独的map,更新的时候同时更新。或者更好的是,使用像 Bidict 这样的现有解决方案,它只是 2 个 dicts 的包装器,内置更新/删除。

    但是如果数据集很大,维护2个dicts是不可取的:

    • 如果键和值都是数字,请考虑使用的可能性 插值以近似映射。如果绝大多数 映射函数可以覆盖键值对(及其
      reverse 函数),那么您只需要在地图中记录异常值即可。

    • 如果大部分访问是单向的(key->value),那么它完全是 可以逐步构建反向地图,以换取时间
      空间。

    代码:

    d = {1: "one", 2: "two" }
    reverse = {}
    
    def get_key_by_value(v):
        if v not in reverse:
            for _k, _v in d.items():
               if _v == v:
                   reverse[_v] = _k
                   break
        return reverse[v]
    

    【讨论】:

      【解决方案8】:

      更好的方法是将字典转换为元组列表,然后对特定的元组字段进行排序

      def convert_to_list(dictionary):
          list_of_tuples = []
          for key, value in dictionary.items():
              list_of_tuples.append((key, value))
          return list_of_tuples
      
      def sort_list(list_of_tuples, field):
           return sorted(list_of_tuples, key=lambda x: x[field])
      
      dictionary = {'a': 9, 'b': 2, 'c': 3, 'd': 4, 'e': 5}
      list_of_tuples = convert_to_list(dictionary)
      print(sort_list(list_of_tuples, 1))
      

      输出

      [('b', 2), ('c', 3), ('d', 4), ('e', 5), ('a', 9)]
      

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 2014-11-21
        • 1970-01-01
        • 1970-01-01
        • 2012-06-03
        • 2016-03-25
        相关资源
        最近更新 更多