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


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.