Як реалізувати ефективну двонаправлену хеш-таблицю?


86

Python dict- дуже корисна структура даних:

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

d['a'] # get 1

Іноді вам також хочеться індексувати за значеннями.

d[1] # get 'a'

Який найефективніший спосіб реалізувати цю структуру даних? Будь-який офіційний рекомендаційний спосіб зробити це?


Якщо ви бажаєте, ми можемо припустити, що значення незмінні, як і ключі.
Хуанхо Конті

4
Що б ви повернули за цей дикт: {'a': 1, 'b': 2, 'A': 1}
PaulMcG

2
@PaulMcGuire: Я б повернувся {1: ['a', 'A'], 2: 'b'}. Подивіться мою відповідь на такий спосіб зробити це.
Бас

4
Примітка для модератора: це не дублікат stackoverflow.com/questions/1456373/two-way-reverse-map . Останній має 1) дуже розпливчасте формулювання 2) немає MCVE 3) стосується лише випадку бієктивної карти (див. Перший коментар до цього питання), який є набагато більш обмежувальним, ніж це фактичне питання, яке є більш загальним. Тому я думаю, що позначення його як дубліката тут, у цьому конкретному випадку, вводить в оману. Якщо насправді одне має бути дублікатом іншого, воно повинно бути навпаки, оскільки воно тут охоплює загальний випадок, тоді як інше (див. Відповіді) не охоплює небієктивний випадок.
Бас

Відповіді:


68

Ось клас для двонаправленості dict, натхненний пошуком ключа зі значення у словнику Python і модифікований, щоб дозволити наступні 2) та 3).

Зверніть увагу, що:

  • 1) Зворотний каталог bd.inverse автоматично оновлюється при bdзміні стандартного дикту .
  • 2) зворотний каталог bd.inverse[value] завжди список з keyтаких , що bd[key] == value.
  • 3) На відміну від bidictмодуля з https://pypi.python.org/pypi/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']}

2
Дуже акуратне вирішення неоднозначної справи!
Тобіас Кінцлер

2
Я думаю, що ця структура даних дуже корисна для багатьох практичних проблем.
0xc0de

6
Це феноменально. Це коротко; це самодокументування; це досить ефективно; це просто працює. Моїй єдиною примхою було б оптимізувати повторні пошуки self[key]в __delitem__()за допомогою одного value = self[key]призначення, повторно використаного для таких пошуків. Але ... так. Це незначно. Дякую за чудовий чудовий, Баж !
Сесіл Каррі,

1
Як щодо версії Python 3?
zelusp

1
Мені подобається ця відповідь на прикладі. Прийнята відповідь все ще є правильною, і я думаю, що прийнята відповідь повинна залишатися прийнятою відповіддю, але це трохи чіткіше для визначення її самостійно, просто тому, що чітко викладено, що для того, щоб змінити словник у зворотному напрямку, ви повинні розмістити зворотне значення у список, оскільки не може бути співвідношення "один на один", оскільки словник має відношення "один до багатьох" із ключем до значень.
searchengine

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 Для акуратного використання зворотного (). Я не визначився, чи це читабельніше, ніж явне 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'}
Тобіас Кінцлер,

3
Незначна зміна: dict(map(reversed, a_dict.items())).
0xc0de

13
Додавання зворотних відображень до оригінального словника - жахлива ідея. Як демонструють наведені вище коментарі, робити це в цілому не безпечно. Просто підтримуйте два окремі словники. Оскільки перші два рядки цієї відповіді, ігноруючи кінцевий результат, d.update(revd)є чудовими, однак, я все ще розглядаю голос проти. Давайте трохи подумаємо.
Сесіл Каррі

36

Двонаправлена ​​хеш-таблиця бідної людини мала б використовувати лише два словники (це вже високо налаштовані структури даних).

Також в індексі є пакет bidict :

Джерело для bidict можна знайти на github:


1
Для двох диктів потрібні подвійні вставки та видалення.
Хуанхо Конті

12
@Juanjo: майже будь-яка двонаправлена ​​/ оборотна хеш-таблиця буде включати "подвійні вставки та видалення", або як частина реалізації структури, або як частина її використання. Ведення двох індексів - це справді єдиний швидкий спосіб це зробити, AFAIK.
Вальтер Мундт

7
Звичайно; Я мав на увазі, що проблема в ручному догляді за індексом 2 - це проблема.
Хуанхо Конті

1
@Basj Я вважаю правильним, що воно не приймається, оскільки наявність декількох значень означає, що це вже не бієкція і неоднозначно для зворотного пошуку.
user193130

1
@Basj Ну, я розумію, що існували б випадки використання, які мали б користь мати більше одного значення на ключ, тож, можливо, такий тип структури даних повинен існувати як підклас bidict. Однак, оскільки звичайний дикт відображається на одному об'єкті, я вважаю, що має набагато більше сенсу, щоб і реверс був однаковим. (Тільки для уточнення, хоча значення також може бути колекцією, я мав на увазі, що ключ першого дикту повинен бути того самого типу, що і значення зворотного
дикту

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не працює.

Є три варіанти:

  1. Дикт підкласу : Ви можете створити підклас dict, але будьте обережні. Вам потрібно написати призначені для користувача реалізації update, pop, initializer, setdefault. У dictреалізації не називають __setitem__. Ось чому відповідь із найвищим рейтингом має проблеми.

  2. Успадковування від UserDict : Це подібно до дикту, за винятком того, що всі процедури зроблені для правильного виклику. Він використовує дикт під капотом, в елементі під назвою data. Ви можете прочитати Документацію Python або скористатися простою реалізацією списку за напрямками, який працює в Python 3 . Вибачте, що не включив його дослівно: я не впевнений у його авторських правах.

  3. Успадкування від абстрактних базових класів : успадкування від collection.abc допоможе вам отримати всі правильні протоколи та реалізації для нового класу. Це є надмірним для двонаправленого словника, якщо він також не може шифрувати та кешувати в базі даних.

TL; DR - використовуйте це для свого коду. Read Трей Hunner «s стаття для деталей.


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

1
Це не проблема для прикладу OP, але може бути гарним застереженням для включення.
tgray

Як ми можемо print bd['myvalue2']відповісти b, c(або [b, c], або (b, c), або щось інше)?
Бас

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]
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.