Làm cách nào để triển khai bảng băm hai chiều hiệu quả?


82

Python dictlà một cấu trúc dữ liệu rất hữu ích:

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

d['a'] # get 1

Đôi khi bạn cũng muốn lập chỉ mục theo các giá trị.

d[1] # get 'a'

Cách hiệu quả nhất để triển khai cấu trúc dữ liệu này là gì? Bất kỳ chính thức khuyến nghị cách để làm điều đó?


Nếu bạn muốn, chúng tôi có thể giả định rằng các giá trị là bất biến cũng như các khóa.
Juanjo Conti

3
Bạn sẽ quay trở lại để làm gì dict này: { 'a': 1, 'b': 2, 'A': 1}
PaulMcG

2
@PaulMcGuire: Tôi sẽ quay lại {1: ['a', 'A'], 2: 'b'}. Hãy xem câu trả lời của tôi để biết cách làm như vậy.
Basj

4
Lưu ý với người kiểm duyệt: đây không phải là bản sao của stackoverflow.com/questions/1456373/two-way-reverse-map . Câu hỏi thứ hai có 1) từ ngữ rất mơ hồ 2) không có MCVE 3) chỉ đề cập đến trường hợp của bản đồ sinh vật (xem nhận xét đầu tiên trong câu hỏi này), điều này hạn chế hơn rất nhiều so với câu hỏi thực tế này, có tính tổng quát hơn. Vì vậy, tôi nghĩ rằng việc đánh dấu nó là trùng lặp, trong trường hợp cụ thể này là gây hiểu lầm. Nếu thực sự một cái phải là bản sao của cái khác, thì nó phải ngược lại vì cái này ở đây bao hàm trường hợp chung trong khi cái kia (xem câu trả lời) không bao gồm trường hợp phi khách quan.
Basj

Câu trả lời:


65

Đây là một lớp cho một hai chiều dict, lấy cảm hứng từ Tìm khóa từ giá trị trong từ điển Python và được sửa đổi để cho phép 2) và 3) sau.

Lưu ý rằng:

  • 1) Thư mục nghịch đảo bd.inverse tự động cập nhật khi bdsửa đổi chính tả tiêu chuẩn .
  • 2) Các thư mục nghịch đảo bd.inverse[value] luôn là một danh sách các keyví dụ đó bd[key] == value.
  • 3) Không giống như bidictmô-đun từ https://pypi.python.org/pypi/bidict , ở đây chúng ta có thể có 2 khóa có cùng giá trị, điều này rất quan trọng .

Mã:

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)

Ví dụ sử dụng:

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
Giải pháp rất gọn gàng của trường hợp mơ hồ!
Tobias Kienzler

2
Tôi nghĩ rằng cấu trúc dữ liệu này rất hữu ích trong nhiều vấn đề thực tế.
0xc0de

5
Đây là một hiện tượng. Nó ngắn gọn; nó tự ghi lại; nó hiệu quả một cách hợp lý; nó chỉ hoạt động. Phân tích duy nhất của tôi là tối ưu hóa các tra cứu lặp đi lặp lại self[key]trong __delitem__()với một value = self[key]nhiệm vụ duy nhất được sử dụng lại cho các tra cứu như vậy. Nhưng ... vâng. Đó là không đáng kể. Cảm ơn vì sự tuyệt vời thuần khiết, Basj !
Cecil Curry,

1
Làm thế nào về một phiên bản Python 3?
zelusp 14/09/2016

1
Tôi thích câu trả lời này cho ví dụ. Câu trả lời được chấp nhận vẫn đúng và tôi nghĩ câu trả lời được chấp nhận sẽ vẫn là câu trả lời được chấp nhận, nhưng điều này rõ ràng hơn một chút để tự định nghĩa nó, chỉ đơn thuần vì nó cho thấy rõ ràng rằng để đảo ngược từ điển, bạn phải đặt câu trả lời ngược giá trị vào danh sách vì không thể có ánh xạ một-một vì từ điển có mối quan hệ một-nhiều với khóa-giá trị.
searchhengine27

41

Bạn có thể sử dụng cùng một mệnh đề bằng cách thêm cặp khóa, cặp giá trị theo thứ tự ngược lại.

d = {'a': 1, 'b': 2}
revd = dict ([đảo ngược (i) cho tôi trong d.items ()])
d. cập nhật (revd)

5
+1 Một giải pháp hay, thiết thực. Một cách khác để viết nó: d.update( dict((d[k], k) for k in d) ).
FMc

4
+1 Để sử dụng gọn gàng đảo ngược (). Tôi chưa quyết định nếu nó dễ đọc hơn là rõ ràng dict((v, k) for (k, v) in d.items()). Trong mọi trường hợp, bạn có thể chuyển các cặp trực tiếp đến .update : d.update(reversed(i) for i in d.items()).
Beni Cherniavsky-Paskin

22
Lưu ý rằng điều này không thành công, ví dụ:d={'a':1, 'b':2, 1: 'b'}
Tobias Kienzler

3
Sửa đổi chút ít: dict(map(reversed, a_dict.items())).
0xc0de

13
Thêm ánh xạ ngược vào từ điển gốc là một ý tưởng tồi. Như các ý kiến ​​trên chứng minh, làm như vậy không an toàn trong trường hợp chung. Chỉ cần duy trì hai từ điển riêng biệt. d.update(revd)Tuy nhiên, vì hai dòng đầu tiên của câu trả lời này bỏ qua phần cuối là điều tuyệt vời, tôi vẫn đang suy tính về một sự ủng hộ. Hãy suy nghĩ về điều này.
Cecil Curry

34

Bảng băm hai chiều của một người nghèo sẽ chỉ sử dụng hai từ điển (đây là những cấu trúc dữ liệu đã được tinh chỉnh cao).

Ngoài ra còn có một bidict gói trên các chỉ số:

Nguồn cho bidict có thể được tìm thấy trên github:


1
2 quân số yêu cầu chèn và xóa kép.
Juanjo Conti

12
@Juanjo: gần như bất kỳ bảng băm hai chiều / đảo ngược nào sẽ liên quan đến "chèn và xóa kép", như một phần của việc triển khai cấu trúc hoặc là một phần của việc sử dụng nó. Giữ hai chỉ mục thực sự là cách nhanh chóng duy nhất để làm điều đó, AFAIK.
Walter Mundt

7
Tất nhiên; Ý tôi là việc chăm sóc 2 chỉ số bằng tay là vấn đề.
Juanjo Conti

1
@Basj Tôi nghĩ rằng nó không được chấp nhận là chính xác vì có nhiều hơn một giá trị có nghĩa là nó không phải là một từ chối nữa và không rõ ràng cho việc tra cứu ngược lại.
user193130

1
@Basj Chà, tôi có thể hiểu rằng sẽ có những trường hợp sử dụng sẽ hữu ích khi có nhiều hơn một giá trị cho mỗi khóa, vì vậy có thể loại cấu trúc dữ liệu này nên tồn tại như một lớp con của bidict. Tuy nhiên, vì một dict bình thường ánh xạ đến một đối tượng, tôi nghĩ rằng điều ngược lại cũng có ý nghĩa hơn nhiều. (Chỉ cần làm rõ, mặc dù giá trị có thể là một bộ sưu tập quá, tôi muốn nói rằng chìa khóa của dict đầu tiên phải thuộc loại tương tự như giá trị của dict ngược)
user193130

3

Đoạn mã dưới đây triển khai một bản đồ (bijective) có thể đảo ngược:

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)

Ưu điểm của cách triển khai này là inversethuộc tính của a BijectiveMaplại là a BijectiveMap. Do đó, bạn có thể làm những việc như:

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

1

Một cái gì đó như thế này, có thể:

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

Bạn phải quyết định điều gì bạn muốn xảy ra nếu nhiều hơn một khóa có giá trị nhất định; tính hai chiều của một cặp nhất định có thể dễ dàng bị che lấp bởi một số cặp sau đó bạn đã chèn. Tôi đã thực hiện một lựa chọn khả thi.


Thí dụ :

bd = BidirDict({'a': 'myvalue1', 'b': 'myvalue2', 'c': 'myvalue2'})
print bd['myvalue1']   # a
print bd['myvalue2']   # b        

1
Tôi không chắc đây có phải là sự cố hay không, nhưng bằng cách sử dụng triển khai ở trên, sẽ không có vấn đề gì nếu các khóa và giá trị chồng chéo lên nhau? Vì vậy dict([('a', 'b'), ('b', 'c')]); dict['b']-> 'c'thay vì phím 'a'.
tgray

1
Nó không phải là một vấn đề đối với ví dụ của OP, nhưng có thể là một tuyên bố từ chối trách nhiệm tốt để đưa vào.
tgray

Làm thế nào chúng ta có thể làm print bd['myvalue2']câu trả lời đó b, c(hoặc [b, c], hoặc (b, c), hoặc bất cứ điều gì khác)?
Basj

0

Đầu tiên, bạn phải đảm bảo chìa khóa của ánh xạ giá trị là 1-1, nếu không, không thể xây dựng bản đồ hai chiều.

Thứ hai, tập dữ liệu lớn như thế nào? Nếu không có nhiều dữ liệu, chỉ cần sử dụng 2 bản đồ riêng biệt, và cập nhật cả 2 bản đồ khi cập nhật. Hoặc tốt hơn, sử dụng một giải pháp hiện có như Bidict , chỉ là một gói bao gồm 2 phần, với cập nhật / xóa được tích hợp sẵn.

Nhưng nếu tập dữ liệu lớn và việc duy trì 2 vùng là không mong muốn:

  • Nếu cả khóa và giá trị đều là số, hãy xem xét khả năng sử dụng Nội suy để ánh xạ gần đúng. Nếu phần lớn các cặp khóa-giá trị có thể được bao hàm bởi chức năng ánh xạ (và
    chức năng đảo ngược của nó ), thì bạn chỉ cần ghi lại các ngoại lệ trong bản đồ.

  • Nếu phần lớn quyền truy cập là đơn hướng (key-> value), thì bạn hoàn toàn có thể xây dựng bản đồ ngược theo từng bước, để đánh đổi thời gian lấy
    không gian.

Mã:

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

Thật không may, câu trả lời được đánh giá cao nhất, bidictkhông hoạt động.

Có ba lựa chọn:

  1. Lớp con dict : Bạn có thể tạo một lớp con của dict, nhưng hãy cẩn thận. Bạn cần phải viết triển khai tùy chỉnh của update, pop, initializer, setdefault. Các dicttriển khai không gọi __setitem__. Đây là lý do tại sao câu trả lời được đánh giá cao nhất có vấn đề.

  2. Kế thừa từ UserDict : Đây giống như một câu lệnh, ngoại trừ tất cả các quy trình được thực hiện để gọi một cách chính xác. Nó sử dụng một mệnh đề dưới mui xe, trong một mục được gọi là data. Bạn có thể đọc Tài liệu Python hoặc sử dụng cách triển khai đơn giản của danh sách theo hướng hoạt động trong Python 3 . Xin lỗi vì đã không bao gồm nguyên văn: Tôi không chắc về bản quyền của nó.

  3. Kế thừa từ các lớp cơ sở trừu tượng : Kế thừa từ collection.abc sẽ giúp bạn nhận được tất cả các giao thức và triển khai chính xác cho một lớp mới. Điều này là quá mức cần thiết đối với từ điển hai chiều, trừ khi nó cũng có thể mã hóa và lưu vào bộ nhớ cache vào cơ sở dữ liệu.

TL; DR - Sử dụng cái này cho mã của bạn. Đọc bài báo của Trey Hunner để biết thêm chi tiết.

Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.