효율적인 양방향 해시 테이블을 구현하는 방법은 무엇입니까?


82

Python dict은 매우 유용한 데이터 구조입니다.

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

d['a'] # get 1

때로는 값으로 인덱싱하고 싶을 수도 있습니다.

d[1] # get 'a'

이 데이터 구조를 구현하는 가장 효율적인 방법은 무엇입니까? 공식적으로 권장하는 방법이 있습니까?


원하는 경우 키와 마찬가지로 값이 불변이라고 가정 할 수 있습니다.
Juanjo Conti

3
이 dict에 대해 무엇을 반환 하시겠습니까 : { 'a': 1, 'b': 2, 'A': 1}
PaulMcG

2
@PaulMcGuire : 나는 돌아올 것이다 {1: ['a', 'A'], 2: 'b'}. 그렇게하는 방법에 대한 내 대답을 참조하십시오.
Basj

4
중재자 참고 사항 : 이것은 stackoverflow.com/questions/1456373/two-way-reverse-map 의 중복 이 아닙니다 . 후자는 1) 매우 모호한 표현 2) MCVE 없음 3) bijective map (이 질문의 첫 번째 주석 참조)의 경우 만 다룹니다. 이것은보다 일반적인이 실제 질문보다 훨씬 더 제한적입니다. 그래서 나는 그것을 중복으로 표시하는 것이 여기에 있다고 생각합니다.이 특별한 경우에는 오해의 소지가 있습니다. 실제로 하나가 다른 것과 중복되어야한다면 여기에있는 일반적인 경우를 다루고 다른 하나 (답변 참조)는 비용 사적 경우를 다루지 않으므로 반대 여야합니다.
Basj

답변:


65

다음은 Python 사전의 값에서 키 찾기dict 에서 영감을 받아 다음 2) 및 3)을 허용하도록 수정 된 양방향 클래스입니다 .

참고 :

  • 1) 역 디렉토리 bd.inverse 는 표준 dict bd가 수정 되면 자동으로 업데이트됩니다 .
  • 2) 역 디렉토리 bd.inverse[value] 는 항상 다음key같은 목록 입니다 bd[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

5
이것은 경이 롭습니다. 간결합니다. 자체 문서화입니다. 합리적으로 효율적입니다. 그냥 작동합니다. 내 유일한 문제는 그러한 조회에 재사용 되는 단일 할당 으로 self[key]in 의 반복 조회를 최적화하는 것 입니다. 하지만 ... 그래. 무시해도 될 정도입니다. 순수한 굉장한 Basj 감사합니다 ! __delitem__()value = self[key]
세실 카레

1
파이썬 3 버전은 어떻습니까?
zelusp

1
예를 들어이 답변이 마음에 듭니다. 받아 들여진 대답은 여전히 ​​옳고 받아 들여진 대답은 받아 들여진 대답으로 남아 있어야한다고 생각하지만, 이것은 스스로 정의하기 위해 조금 더 명확합니다. 딕셔너리가 키-값과 일대 다 관계를 갖기 때문에 일대일 매핑이있을 수 없기 때문에 값을 목록에 추가합니다.
searchengine27

41

키, 값 쌍을 역순으로 추가하여 동일한 사전을 사용할 수 있습니다.

d = { 'a': 1, 'b': 2}
revd = dict ([reversed (i) for i in d.items ()])
d. 업데이트 (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()).
Beni Cherniavsky-Paskin

22
예를 들어 실패합니다.d={'a':1, 'b':2, 1: 'b'}
Tobias Kienzler 2013 년

3
약간의 수정 : dict(map(reversed, a_dict.items())).
0xc0de

13
원래 사전에 역 매핑을 추가하는 것은 끔찍한 생각입니다. 위의 설명에서 알 수 있듯이 일반적으로 그렇게하는 것은 안전 하지 않습니다 . 두 개의 별도 사전을 유지하십시오. 이 답변의 처음 두 줄은 후행을 무시하는 d.update(revd)것이 훌륭하기 때문에 여전히 찬성 투표를 고려하고 있습니다. 이것에 대해 생각해 봅시다.
Cecil Curry

34

가난한 사람의 양방향 해시 테이블은 두 개의 사전 만 사용하는 것입니다 (이는 이미 고도로 조정 된 데이터 구조입니다).

색인에 bidict 패키지 도 있습니다 .

bidict의 소스는 github에서 찾을 수 있습니다.


1
2 개의 dict에는 이중 삽입 및 삭제가 필요합니다.
Juanjo Conti

12
@Juanjo : 거의 모든 양방향 / 가역적 해시 테이블은 구조 구현의 일부 또는 사용의 일부로 "이중 삽입 및 삭제"를 포함합니다. 두 개의 인덱스를 유지하는 것이 실제로이를 수행하는 유일한 빠른 방법 인 AFAIK입니다.
Walter Mundt

7
물론이야; 나는 2 인덱스를 손으로 돌보는 것이 문제라는 것을 의미했습니다.
Juanjo Conti

1
@Basj 두 개 이상의 값이 있다는 것은 더 이상 bijection이 아니며 역방향 조회에 대해 모호하기 때문에 허용되지 않는다는 것이 옳다고 생각합니다.
user193130

1
@Basj 글쎄, 키당 하나 이상의 값을 갖는 데 유용한 사용 사례가 있다는 것을 이해할 수 있으므로 이러한 유형의 데이터 구조는 bidict의 하위 클래스로 존재해야합니다. 그러나 일반 딕셔너리는 단일 객체에 매핑되기 때문에 그 반대도 동일한 것이 훨씬 더 합리적이라고 생각합니다. (값이 컬렉션이 될 수도 있지만, 첫 번째 dict의 키는 reverse dict의 값과 동일한 유형이어야 함을 의미합니다.)
user193130

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이 다시 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'대신 key 'a'.
tgray 2010-07-23

1
OP의 예에 대한 문제는 아니지만 포함하기에 좋은 면책 조항이 될 수 있습니다.
tgray 2010-07-23

print bd['myvalue2']대답 b, c(또는 [b, c], 또는 (b, c)다른 것)을 어떻게 할 수 있습니까?
Basj

0

첫째, 값 매핑에 대한 키가 일대일인지 확인해야합니다. 그렇지 않으면 양방향 맵을 만들 수 없습니다.

둘째, 데이터 세트는 얼마나 큽니까? 데이터가 많지 않은 경우 2 개의 별도 맵을 사용하고 업데이트 할 때 둘 다 업데이트하십시오. 또는 업데이트 / 삭제 기능이 내장 된 2 개의 dict 의 래퍼 인 Bidict 와 같은 기존 솔루션을 사용하는 것이 좋습니다 .

그러나 데이터 세트가 크고 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있지만주의해야합니다. 당신의 사용자 정의 구현을 작성해야합니다 update, pop, initializer, setdefault. dict구현은 호출하지 않습니다 __setitem__. 이것이 가장 높은 등급의 답변에 문제가있는 이유입니다.

  2. UserDict에서 상속 : 이것은 모든 루틴이 올바르게 호출된다는 점을 제외하면 dict와 같습니다. 라는 항목에서 내부적으로 dict를 사용합니다 data. Python 문서를 읽 거나 Python 3에서 작동하는 방향 별 목록의 간단한 구현을 사용할 수 있습니다 . 그대로 포함하지 않아서 죄송합니다. 저작권이 확실하지 않습니다.

  3. 추상 기본 클래스 에서 상속 : collections.abc 에서 상속 하면 새 클래스에 대한 모든 올바른 프로토콜과 구현을 얻을 수 있습니다. 이것은 데이터베이스에 암호화하고 캐시 할 수없는 한 양방향 사전의 경우 과잉입니다.

TL; DR- 코드에 이것을 사용하십시오 . 자세한 내용은 Trey Hunner기사 를 읽어보십시오 .

당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.