如何实现高效的双向哈希表?


82

Pythondict是非常有用的数据结构:

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

d['a'] # get 1

有时,您还想按值编制索引。

d[1] # get 'a'

哪个是实现此数据结构的最有效方法?有官方推荐的方法吗?


如果您愿意,我们可以假定值和键都是不可变的。
Juanjo Conti 2010年

3
您将为此命令返回什么:{'a':1,'b':2,'A':1}
PaulMcG 2010年

2
@PaulMcGuire:我会回来的{1: ['a', 'A'], 2: 'b'}。请参阅我的答案以了解这种方法。
Basj 2014年

4
主持人注意:这不是stackoverflow.com/questions/1456373/two-way-reverse-map的副本。后者具有1)非常模糊的措辞2)没有MCVE 3)仅处理双射映象的情况(请参阅此问题的第一条评论),比实际问题更严格,后者更为笼统。因此,在这种情况下,我认为将其标记为重复项会产生误导。如果真的一个应该是另一个的重复,那应该是相反的,因为这里的这个覆盖了一般情况,而另一个(请参阅答案)却不覆盖非双射的情况。
Basj

Answers:


65

这是一个双向类,其dict灵感来自于从Python字典中的值中查找键,并进行了修改以允许以下2)和3)。

注意 :

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

码:

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']}

2
歧义案例的解决方案非常简洁!
Tobias Kienzler 2014年

2
我认为这种数据结构在许多实际问题中非常有用。
2015年

5
这是惊人的。简洁;它是自我记录;它相当有效;它只是工作。我唯一的疑问是优化self[key]in的重复查找,__delitem__()value = self[key]为此类查找重新使用单个分配。但是...是的。可以忽略不计。感谢您的真棒,Basj
Cecil Curry

1
Python 3版本怎么样?
zelusp

1
我喜欢这个例子的答案。接受的答案仍然是正确的,我认为接受的答案应保留为接受的答案,但这对于您自己定义它来说更为明确,只是因为它清楚地列出了要反转字典,您必须放置反转​​的因为字典与键对值之间存在一对多的关系,所以不能存在一对一的映射关系,因此列表中的值不存在。
searchengine27

41

您可以通过相反的顺序添加键值对来使用相同的字典本身。

d = {'a':1,'b':2}
revd = dict([d.items()中i的反向(i)])
d.update(revd)

5
+1一个不错的实用解决方案。另一种方式来写它:d.update( dict((d[k], k) for k in d) )
FMc

4
+1巧妙地使用了reversed()。我不确定它是否比显式的更具可读性dict((v, k) for (k, v) in d.items())。在任何情况下,你可以直接传递对来.update: d.update(reversed(i) for i in d.items())
贝尼·切尔尼亚夫斯基-帕斯金

22
请注意,此操作失败,例如d={'a':1, 'b':2, 1: 'b'}
Tobias Kienzler 2013年

3
轻微修改:dict(map(reversed, a_dict.items()))
2015年

13
向原始字典添加反向映射是一个糟糕的主意。如以上评论所示,在一般情况下这样做并不安全。只需维护两个单独的字典即可。但是,由于此答案的前两行忽略了结尾,d.update(revd)因此效果很好,因此我仍在考虑投票。让我们考虑一下。
Cecil Curry

34

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

索引上还有一个bidict包:

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


1
2 dicts需要两次插入和删除。
Juanjo Conti 2010年

12
@Juanjo:几乎任何双向/可逆哈希表都将涉及“双重插入和删除”,无论是实现该结构的一部分还是使用该结构的一部分。保持两个索引确实是做到这一点的唯一快速方法,即AFAIK。
沃尔特·蒙德

7
当然; 我的意思是手动处理2索引是个问题。
Juanjo Conti 2010年

1
@Basj我认为不接受它是正确的,因为具有多个值意味着它不再是双射对象,并且对于反向查找是模棱两可的。
user193130 2014年

1
@Basj好吧,我可以理解会有一些用例,每个键具有多个值将很有用,因此也许这种数据结构应该作为bidict的子类存在。但是,由于普通字典会映射到单个对象,所以我认为反向相同也是有意义的。(只是澄清一下,尽管该值也可以是一个集合,但我的意思是第一个字典的键应与反向字典的值具有相同的类型)
user193130 2014年

3

下面的代码片段实现了一个可逆(双射)映射:

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)

此实现的优点是inversea的属性BijectiveMap再次是a BijectiveMap。因此,您可以执行以下操作:

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

1

可能是这样的:

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        

1
我不确定这是否有问题,但是使用上述实现,如果键和值重叠,就不会有问题吗?所以dict([('a', 'b'), ('b', 'c')]); dict['b']->'c'而不是键'a'
tgray,2010年

1
对于OP的示例而言,这不是问题,但包括在内可能是一个很好的免责声明。
tgray,2010年

我们该如何print bd['myvalue2']回答b, c(或[b, c],或(b, c),或其他任何问题)?
Basj 2014年

0

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

第二,数据集有多大?如果没有太多数据,则仅使用2个单独的地图,并在更新时同时更新两个地图。或者更好的方法是使用现有的解决方案(例如Bidict),该解决方案仅包含2个字典,并内置更新/删除功能。

但是,如果数据集很大,则不希望保留2个字典:

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

  • 如果大多数访问是单向的(键-值),则完全可以逐步构建反向映射,以时间换取
    空间。

码:

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]

0

不幸的是,最高评分的答案bidict无效。

共有三个选项:

  1. 子类字典:您可以创建的子类dict,但要小心。你需要写的自定义实现updatepopinitializersetdefault。该dict实现不叫__setitem__。这就是为什么评分最高的答案存在问题。

  2. 从UserDict继承:就像dict一样,不同之处在于所有例程都可以正确调用。它在幕后使用了一个dict,称为data。您可以阅读Python文档,或使用在Python 3中可以使用的按方向列表的简单实现。很抱歉没有一字不漏地包含它:我不确定它的版权。

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

TL; DR-将用于您的代码。阅读 Trey Hunner文章以了解详细信息。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.