効率的な双方向ハッシュテーブルを実装する方法は?


86

Pythondictは非常に便利なデータ構造です。

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

d['a'] # get 1

値でインデックスを作成したい場合もあります。

d[1] # get 'a'

このデータ構造を実装するための最も効率的な方法はどれですか?それを行うための公式の推奨方法はありますか?


必要に応じて、キーと同様に値は不変であると想定できます。
Juanjo Conti

4
このdictに対して何を返しますか:{'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

回答:


68

これは、Python辞書の値からキーを検索dictすることに触発され、次の2)と3)を許可するように変更された双方向のクラスです。

ご了承ください :

  • 1)標準辞書が変更されると、逆ディレクトリは bd.inverse自動的に更新bdされます。
  • 2)逆ディレクトリ bd.inverse[value]は、常に次のようなリストです。keybd[key] == value
  • 3)https://pypi.python.org/pypi/bidictbidictモジュールとは異なり、ここでは同じ値を持つ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']}

2
あいまいなケースの非常にきちんとした解決策!
Tobias Kienzler 2014

2
このデータ構造は、多くの実際的な問題で非常に役立つと思います。
0xc0de 2015

6
これは驚異的です。簡潔です。それは自己文書化です。それはかなり効率的です。それはうまくいきます。私の唯一の疑問は、そのようなルックアップに再利用される単一の割り当てを使用self[key]__delitem__()て、の繰り返しルックアップを最適化することvalue = self[key]です。しかし...ええ。それはごくわずかです。純粋に素晴らしい、Basjをありがとう
セシルカレー

1
Python 3バージョンはどうですか?
zelusp 2016

1
私は例としてこの答えが好きです。受け入れられた答えはまだ正しいので、受け入れられた答えは受け入れられた答えのままである必要があると思いますが、これは、辞書を逆にするために逆を配置する必要があることを明確に示しているため、自分で定義するためにもう少し明確ですディクショナリはキーと値の関係が1対多であるため、1対1のマッピングはできないため、値をリストに追加します。
searchengine27 2017

41

キーと値のペアを逆の順序で追加することで、同じdict自体を使用できます。

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

5
+1素晴らしい実用的なソリューション。それを書く別の方法:d.update( dict((d[k], k) for k in d) )
FMc 2010

4
+1 reverse()をきちんと使用するため。明示的なよりも読みやすいかどうかは未定ですdict((v, k) for (k, v) in d.items())。いずれの場合も、ペアを直接.update:に渡すことができますd.update(reversed(i) for i in d.items())
Beni Cherniavsky-Paskin 2012

22
たとえば、これが失敗することに注意してくださいd={'a':1, 'b':2, 1: 'b'}
Tobias Kienzler 2013年

3
わずかな変更:dict(map(reversed, a_dict.items()))
0xc0de 2015

13
元の辞書に逆マッピングを追加するのはひどい考えです。上記のコメントが示すように、そうすることは一般的な場合に安全ではありません。2つの別々の辞書を維持するだけです。しかし、この回答の最初の2行は末尾d.update(revd)を無視するのは素晴らしいので、私はまだ賛成票を検討しています。これについて考えてみましょう。
セシルカレー

36

貧乏人の双方向ハッシュテーブルは、2つの辞書だけを使用することです(これらはすでに高度に調整されたデータ構造です)。

インデックスにはbidictパッケージもあります。

bidictのソースはgithubにあります:


1
2 dictは、二重の挿入と削除を必要とします。
Juanjo Conti 2010

12
@Juanjo:ほぼすべての双方向/可逆ハッシュテーブルには、構造の実装の一部として、または構造の使用の一部として、「二重挿入と削除」が含まれます。2つのインデックスを保持することは、実際にそれを行う唯一の高速な方法です、AFAIK。
Walter Mundt 2010

7
もちろん; 手作業で2つのインデックスを処理することが問題であることを意味しました。
Juanjo Conti 2010

1
@Basj複数の値があるということは、それがもはや全単射ではなく、逆引き参照があいまいであることを意味するため、受け入れられないのは正しいと思います。
user193130 2014

1
@Basjええと、キーごとに複数の値を持つと便利なユースケースがあることは理解できます。したがって、このタイプのデータ構造は、bidictのサブクラスとして存在する必要があります。ただし、通常のdictは単一のオブジェクトにマップされるため、逆も同じである方がはるかに理にかなっていると思います。(明確にするために、値もコレクションにすることができますが、最初の
辞書のキー

4

以下のコードスニペットは、可逆(全単射)マップを実装しています。

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)

この実装の利点はinverse、aの属性BijectiveMapが再びaになることBijectiveMapです。したがって、次のようなことができます。

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

2

残念ながら、最高評価の回答はbidict機能しません。

3つのオプションがあります。

  1. サブクラスdict:のサブクラスを作成できますdictが、注意してください。あなたはのカスタム実装を記述する必要がありますupdatepopinitializersetdefaultdict実装は呼び出すことはありません__setitem__。これが、最高評価の回答に問題がある理由です。

  2. UserDictから継承:すべてのルーチンが正しく呼び出されることを除けば、これはdictと同じです。と呼ばれるアイテムで、内部でdictを使用しますdata。あなたは読むことができますPythonのドキュメントを、あるいはPythonの3で動作することにより、指向リストの簡単な実装を使用します。逐語的に含めなかったことをお詫びします:私はその著作権がわかりません。

  3. 抽象基本クラスから継承:collections.abcから継承すると、新しいクラスのすべての正しいプロトコルと実装を取得するのに役立ちます。これは、暗号化してデータベースにキャッシュすることもできない限り、双方向辞書にとってはやり過ぎです。

TL; DR-これをコードに使用します。詳細については、TreyHunner記事をお読み ください。


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))

複数のキーに特定の値がある場合、何を実行するかを決定する必要があります。特定のペアの双方向性は、後で挿入したペアによって簡単に破壊される可能性があります。私は1つの可能な選択肢を実装しました。


例:

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

まず、値のマッピングへのキーが1対1であることを確認する必要があります。そうしないと、双方向マップを作成できません。

次に、データセットの大きさはどれくらいですか?データが少ない場合は、2つの別々のマップを使用し、更新時に両方を更新します。または、更新/削除が組み込まれた、2つのdictの単なるラッパーであるBidictのような既存のソリューションを使用することをお勧めします。

ただし、データセットが大きく、2つのdictを維持することが望ましくない場合:

  • キーと値の両方が数値の場合は、補間を使用してマッピングを概算する可能性を検討してください。キーと値のペアの大部分をマッピング関数(およびその
    逆関数)でカバーできる場合は、外れ値をマップに記録するだけで済みます。

  • ほとんどのアクセスが単方向(キー->値)の場合、時間を
    スペースと交換するために、リバースマップを段階的に作成することはまったく問題ありません。

コード:

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]
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.