Biểu diễn đồ thị (cấu trúc dữ liệu) bằng Python


105

Làm cách nào để có thể biểu diễn một biểu đồ trong Python một cách gọn gàng ? (Bắt đầu từ đầu tức là không có thư viện!)
Cấu trúc dữ liệu nào (ví dụ: dicts / tuples / dict (tuples)) sẽ nhanh nhưng cũng hiệu quả về bộ nhớ?
Người ta phải có thể thực hiện các hoạt động đồ thị khác nhau trên nó.

Như đã chỉ ra, các biểu diễn đồ thị khác nhau có thể hữu ích. Làm cách nào để triển khai chúng bằng Python?

Đối với các thư viện, câu hỏi này có câu trả lời khá tốt.


1
Có rất nhiều các thư viện ngoài kia đã: graph-tool.skewed.de/performance , code.google.com/p/python-graph , networkx.github.io
Kassym Dorsel

1
Đối với việc thực hiện một cái nhìn Graph tại bài viết Wikipedia trong đó liệt kê triển khai phổ biến và hiệu quả của họ trong cả hai bộ nhớ và tốc độ: en.wikipedia.org/wiki/...
Kassym Dorsel

Bạn có thể thử GitHub.com/thePastor/pangaia. Nó cần phải viết lại một chút để sử dụng lệnh mặc định của thư viện tiêu chuẩn (không xuất hiện khi mã được viết). Nó sử dụng cấu trúc dữ liệu đệ quy để làm cho nó thanh lịch hơn so với các triển khai khác.
theDoctor

1
Đối với đồ thị có hướng , bài luận này từ python.org đề xuất một dicttrong số lists. Về cơ bản là một cái gì đó như thế nào {<parent>: [<child>, ...], ...}.
djvg

Bạn có thể triển khai bằng cách sử dụng từ điển dưới dạng danh sách kề với các khóa là nút và giá trị dưới dạng danh sách các nút liền kề cho mỗi khóa.
Shahrukh khan

Câu trả lời:


140

Mặc dù đây là một câu hỏi hơi cũ, nhưng tôi nghĩ tôi sẽ đưa ra một câu trả lời thực tế cho bất kỳ ai tình cờ gặp phải vấn đề này.

Giả sử bạn nhận được dữ liệu đầu vào cho các kết nối của mình dưới dạng danh sách các bộ giá trị như vậy:

[('A', 'B'), ('B', 'C'), ('B', 'D'), ('C', 'D'), ('E', 'F'), ('F', 'C')]

Cấu trúc dữ liệu mà tôi thấy là hữu ích và hiệu quả nhất cho đồ thị trong Python là một chính tả của các tập hợp . Đây sẽ là cấu trúc cơ bản cho Graphlớp của chúng ta . Bạn cũng phải biết những kết nối này là vòng cung (có hướng, kết nối một chiều) hay các cạnh (vô hướng, kết nối cả hai chiều). Chúng tôi sẽ xử lý điều đó bằng cách thêm một directedtham số vào Graph.__init__phương thức. Chúng tôi cũng sẽ thêm một số phương pháp hữu ích khác.

import pprint
from collections import defaultdict


class Graph(object):
    """ Graph data structure, undirected by default. """

    def __init__(self, connections, directed=False):
        self._graph = defaultdict(set)
        self._directed = directed
        self.add_connections(connections)

    def add_connections(self, connections):
        """ Add connections (list of tuple pairs) to graph """

        for node1, node2 in connections:
            self.add(node1, node2)

    def add(self, node1, node2):
        """ Add connection between node1 and node2 """

        self._graph[node1].add(node2)
        if not self._directed:
            self._graph[node2].add(node1)

    def remove(self, node):
        """ Remove all references to node """

        for n, cxns in self._graph.items():  # python3: items(); python2: iteritems()
            try:
                cxns.remove(node)
            except KeyError:
                pass
        try:
            del self._graph[node]
        except KeyError:
            pass

    def is_connected(self, node1, node2):
        """ Is node1 directly connected to node2 """

        return node1 in self._graph and node2 in self._graph[node1]

    def find_path(self, node1, node2, path=[]):
        """ Find any path between node1 and node2 (may not be shortest) """

        path = path + [node1]
        if node1 == node2:
            return path
        if node1 not in self._graph:
            return None
        for node in self._graph[node1]:
            if node not in path:
                new_path = self.find_path(node, node2, path)
                if new_path:
                    return new_path
        return None

    def __str__(self):
        return '{}({})'.format(self.__class__.__name__, dict(self._graph))

Tôi sẽ để nó như một "bài tập cho người đọc" để tạo ra một find_shortest_pathvà các phương pháp khác.

Hãy xem điều này trong hành động mặc dù ...

>>> connections = [('A', 'B'), ('B', 'C'), ('B', 'D'),
                   ('C', 'D'), ('E', 'F'), ('F', 'C')]
>>> g = Graph(connections, directed=True)
>>> pretty_print = pprint.PrettyPrinter()
>>> pretty_print.pprint(g._graph)
{'A': {'B'},
 'B': {'D', 'C'},
 'C': {'D'},
 'E': {'F'},
 'F': {'C'}}

>>> g = Graph(connections)  # undirected
>>> pretty_print = pprint.PrettyPrinter()
>>> pretty_print.pprint(g._graph)
{'A': {'B'},
 'B': {'D', 'A', 'C'},
 'C': {'D', 'F', 'B'},
 'D': {'C', 'B'},
 'E': {'F'},
 'F': {'E', 'C'}}

>>> g.add('E', 'D')
>>> pretty_print.pprint(g._graph)
{'A': {'B'},
 'B': {'D', 'A', 'C'},
 'C': {'D', 'F', 'B'},
 'D': {'C', 'E', 'B'},
 'E': {'D', 'F'},
 'F': {'E', 'C'}}

>>> g.remove('A')
>>> pretty_print.pprint(g._graph)
{'B': {'D', 'C'},
 'C': {'D', 'F', 'B'},
 'D': {'C', 'E', 'B'},
 'E': {'D', 'F'},
 'F': {'E', 'C'}}

>>> g.add('G', 'B')
>>> pretty_print.pprint(g._graph)
{'B': {'D', 'G', 'C'},
 'C': {'D', 'F', 'B'},
 'D': {'C', 'E', 'B'},
 'E': {'D', 'F'},
 'F': {'E', 'C'},
 'G': {'B'}}

>>> g.find_path('G', 'E')
['G', 'B', 'D', 'C', 'F', 'E']

6
Mặc dù câu hỏi này rất cũ, nhưng tôi nghĩ đây chính xác là câu trả lời mà tôi mong đợi vào thời điểm đó. Ví dụ thực sự giúp giải thích cách người ta có thể thực hiện đồng thời giữ cho nó thực sự đơn giản. Người ta có thể tìm thấy các triển khai từ các thư viện mã nguồn mở khác nhau, nhưng lời giải thích sẽ không ngang bằng. Cảm ơn!
shad0w_wa1k3r

2
loại sửa đổi nào được yêu cầu để thêm trọng lượng cho các cạnh?
pshirishreddy

3
@pshirishreddy Câu hỏi thú vị! Tôi chưa nghĩ đến điều đó, nhưng bản năng của tôi là sử dụng heapqlib để xếp đống danh sách các bộ giá trị thay vì các bộ. Ví dụ: đồ thị sẽ là một lệnh của đống như: _graph = {'A': heapify([(0.3, 'D'), (0.5, 'B'), (0.75, 'A'), (0.9, 'C')])}(lưu ý: bạn sẽ không thực sự sử dụng heapifynhư thế này, hãy đọc phần trợ giúp cho lib), sau đó bạn có thể sử dụng các heapqhàm để chèn và lấy các cạnh có trọng số.
mVChr

@mVChr điều đó có nghĩa là logtruy cập thời gian. Nhưng làm thế nào để mở rộng từ điển mà bạn đã sử dụng để ánh xạ cả nodeID và weight?
orezvani,

Đẹp ! Hàm được gọi một cách đệ quy, đây dường như là một DFS vì nó tiếp tục mở rộng các nút. Đối với đường đi ngắn nhất, chúng ta có thể so sánh độ dài của đường đi và chỉ trả về đường ngắn nhất ở cuối.
Jwalant Bhatt

36

NetworkX là một thư viện đồ thị Python tuyệt vời. Bạn sẽ khó tìm thấy thứ bạn cần mà nó chưa làm được.

Và nó là mã nguồn mở để bạn có thể xem cách họ triển khai các thuật toán của mình. Bạn cũng có thể thêm các thuật toán bổ sung.

https://github.com/networkx/networkx/tree/master/networkx/algorithm


7
Đó là lý do tại sao NetworkX là một nguồn tài nguyên tuyệt vời. Đó là mã nguồn mở để bạn có thể xem cách họ triển khai các thuật toán của mình. Bạn cũng có thể thêm các thuật toán bổ sung.
jterrace

2
Khoảng 2000 dòng mã cho graph.py --> class Graph. Và tất cả những gì tôi muốn xem là cách họ sử dụng __iter__.
T.Woody

8

Đầu tiên, việc lựa chọn biểu diễn danh sách cổ điển so với biểu diễn ma trận phụ thuộc vào mục đích (bạn muốn làm gì với biểu diễn). Các vấn đề và thuật toán nổi tiếng có liên quan đến sự lựa chọn. Việc lựa chọn kiểu biểu diễn trừu tượng quyết định cách thực hiện nó.

Thứ hai, câu hỏi đặt ra là liệu các đỉnh và cạnh chỉ nên được thể hiện dưới dạng tồn tại, hay liệu chúng có mang thêm một số thông tin hay không.

Từ góc nhìn của các kiểu dữ liệu tích hợp trong Python, bất kỳ giá trị nào chứa ở nơi khác đều được thể hiện dưới dạng tham chiếu (ẩn) đến đối tượng đích. Nếu nó là một biến (tức là tham chiếu được đặt tên), thì tên và tham chiếu luôn được lưu trong từ điển (nội bộ). Nếu bạn không cần tên, thì tham chiếu có thể được lưu trữ trong vùng chứa của riêng bạn - ở đây có thể danh sách Python sẽ luôn được sử dụng cho danh sách dưới dạng trừu tượng.

Danh sách Python được triển khai dưới dạng một mảng tham chiếu động, tuple Python được triển khai dưới dạng mảng tham chiếu tĩnh với nội dung không đổi (giá trị của tham chiếu không thể thay đổi). Do đó chúng có thể được lập chỉ mục dễ dàng. Bằng cách này, danh sách cũng có thể được sử dụng để triển khai các ma trận.

Một cách khác để biểu diễn ma trận là các mảng được thực thi bởi mô-đun chuẩn array- bị hạn chế hơn đối với kiểu được lưu trữ, giá trị đồng nhất. Các phần tử lưu trữ giá trị trực tiếp. (Danh sách lưu trữ các tham chiếu đến các đối tượng giá trị thay thế). Bằng cách này, bộ nhớ hiệu quả hơn và truy cập vào giá trị cũng nhanh hơn.

Đôi khi, bạn có thể thấy hữu ích thậm chí còn hạn chế đại diện hơn như bytearray.


7

Có hai thư viện đồ thị tuyệt vời NetworkXigraph . Bạn có thể tìm thấy cả hai mã nguồn thư viện trên GitHub. Bạn luôn có thể xem các hàm được viết như thế nào. Nhưng tôi thích NetworkX hơn vì nó dễ hiểu.
Xem mã của họ để biết cách họ thực hiện các chức năng. Bạn sẽ nhận được nhiều ý tưởng và sau đó có thể chọn cách bạn muốn tạo biểu đồ bằng cách sử dụng cấu trúc dữ liệu.

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.