Ba cách lưu trữ biểu đồ trong bộ nhớ, ưu điểm và nhược điểm


90

Có ba cách để lưu trữ một biểu đồ trong bộ nhớ:

  1. Nút dưới dạng đối tượng và cạnh dưới dạng con trỏ
  2. Một ma trận chứa tất cả các trọng số cạnh giữa nút được đánh số x và nút y
  3. Danh sách các cạnh giữa các nút được đánh số

Tôi biết cách viết cả ba, nhưng tôi không chắc mình đã nghĩ ra tất cả ưu điểm và nhược điểm của từng loại.

Ưu điểm và nhược điểm của từng cách này để lưu một biểu đồ trong bộ nhớ là gì?


3
Tôi chỉ xem xét ma trận nếu đồ thị rất liên kết hoặc rất nhỏ. Đối với các đồ thị được kết nối thưa thớt, cách tiếp cận đối tượng / con trỏ hoặc danh sách các cạnh sẽ cho phép sử dụng bộ nhớ tốt hơn nhiều. Tôi tò mò những gì ngoài bộ nhớ mà tôi đã bỏ qua. ;)
sarnold

2
Chúng cũng khác nhau về độ phức tạp về thời gian, ma trận là O (1) và các biểu diễn khác có thể khác nhau nhiều tùy thuộc vào những gì bạn đang tìm kiếm.
msw

1
Tôi nhớ lại mình đã đọc một bài báo trước đây mô tả những lợi thế phần cứng của việc triển khai một biểu đồ dưới dạng ma trận trên một danh sách các con trỏ. Tôi không thể nhớ nhiều về nó ngoại trừ điều đó, vì bạn đang xử lý một khối bộ nhớ liền kề, tại bất kỳ thời điểm nào, phần lớn bộ làm việc của bạn có thể nằm trong bộ nhớ cache L2. Mặt khác, một danh sách các nút / con trỏ có thể được bắn qua bộ nhớ và có thể sẽ yêu cầu một lần tìm nạp không trúng bộ nhớ cache. Tôi không chắc mình đồng ý nhưng đó là một suy nghĩ thú vị.
nerraga vào

1
@Dean J: chỉ là một câu hỏi về "các nút là đối tượng và các cạnh là biểu diễn con trỏ". Bạn sử dụng cấu trúc dữ liệu nào để lưu trữ con trỏ trong đối tượng? Nó là một danh sách?
Timofey

4
Các tên thông thường là: (1) tương đương với danh sách kề , (2) ma trận kề , (3) danh sách cạnh .
Evgeni Sergeev

Câu trả lời:


51

Một cách để phân tích những điều này là về độ phức tạp của bộ nhớ và thời gian (phụ thuộc vào cách bạn muốn truy cập biểu đồ).

Lưu trữ các nút dưới dạng các đối tượng với các con trỏ tới nhau

  • Độ phức tạp của bộ nhớ cho cách tiếp cận này là O (n) vì bạn có càng nhiều đối tượng cũng như bạn có nút. Số lượng con trỏ (tới các nút) được yêu cầu lên đến O (n ^ 2) vì mỗi đối tượng nút có thể chứa các con trỏ cho tối đa n nút.
  • Độ phức tạp về thời gian cho cấu trúc dữ liệu này là O (n) để truy cập vào bất kỳ nút nào đã cho.

Lưu trữ ma trận trọng số các cạnh

  • Đây sẽ là độ phức tạp bộ nhớ của O (n ^ 2) cho ma trận.
  • Ưu điểm của cấu trúc dữ liệu này là độ phức tạp về thời gian để truy cập vào bất kỳ nút nhất định nào là O (1).

Tùy thuộc vào thuật toán bạn chạy trên biểu đồ và có bao nhiêu nút, bạn sẽ phải chọn một biểu diễn phù hợp.


3
Tôi tin rằng độ phức tạp về thời gian cho các tìm kiếm trong mô hình đối tượng / con trỏ chỉ là O (n) nếu bạn cũng lưu trữ các nút trong một mảng riêng biệt. Nếu không, bạn sẽ cần duyệt qua biểu đồ để tìm kiếm nút mong muốn, phải không? Việc duyệt qua mọi nút (nhưng không nhất thiết là mọi cạnh) trong một đồ thị tùy ý không thể được thực hiện trong O (n), phải không?
Barry Fruitman,

@BarryFruitman Tôi khá chắc chắn bạn nói đúng. BFS là O (V + E). Ngoài ra, nếu bạn đang tìm kiếm một nút không được kết nối với các nút khác, bạn sẽ không bao giờ tìm thấy nó.
WilderField

10

Một vài điều nữa cần xem xét:

  1. Mô hình ma trận cho vay dễ dàng hơn với các đồ thị có các cạnh trọng số, bằng cách lưu trữ các trọng số trong ma trận. Mô hình đối tượng / con trỏ sẽ cần lưu trữ trọng số các cạnh trong một mảng song song, điều này yêu cầu đồng bộ hóa với mảng con trỏ.

  2. Mô hình đối tượng / con trỏ hoạt động tốt hơn với đồ thị có hướng so với đồ thị vô hướng vì các con trỏ cần được duy trì theo cặp, có thể trở nên không đồng bộ.


1
Ý bạn là các con trỏ sẽ cần được duy trì theo cặp với đồ thị vô hướng, đúng không? Nếu nó có hướng, bạn chỉ cần thêm một đỉnh vào danh sách kề của một đỉnh cụ thể, nhưng nếu nó vô hướng, bạn phải thêm một đỉnh vào danh sách kề của cả hai đỉnh?
FrostyStraw

@FrostyStraw Đúng, chính xác.
Barry Fruitman

8

Phương thức đối tượng và con trỏ gặp khó khăn khi tìm kiếm, như một số người đã lưu ý, nhưng khá tự nhiên để thực hiện những việc như xây dựng cây tìm kiếm nhị phân, nơi có rất nhiều cấu trúc bổ sung.

Cá nhân tôi yêu thích ma trận kề vì chúng làm cho tất cả các dạng bài toán trở nên dễ dàng hơn rất nhiều, sử dụng các công cụ từ lý thuyết đồ thị đại số. (Ví dụ: lũy thừa thứ k của ma trận kề cho số đường đi có độ dài k từ đỉnh i đến đỉnh j. Thêm ma trận nhận dạng trước khi lấy lũy thừa thứ k để nhận được số đường đi có độ dài <= k. Lấy một thứ hạng n-1 con của người Laplacian để có được số lượng cây bao trùm ... Và cứ thế tiếp tục.)

Nhưng mọi người đều nói ma trận kề rất tốn kém về bộ nhớ! Chúng chỉ ở một nửa bên phải: Bạn có thể giải quyết vấn đề này bằng cách sử dụng ma trận thưa thớt khi biểu đồ của bạn có ít cạnh. Cấu trúc dữ liệu ma trận thưa thớt thực hiện chính xác công việc chỉ giữ một danh sách liền kề, nhưng vẫn có đầy đủ các phép toán ma trận tiêu chuẩn có sẵn, mang lại cho bạn lợi ích tốt nhất của cả hai thế giới.


7

Tôi nghĩ ví dụ đầu tiên của bạn hơi mơ hồ - các nút là đối tượng và các cạnh là con trỏ. Bạn có thể theo dõi những điều này bằng cách chỉ lưu trữ một con trỏ đến một số nút gốc, trong trường hợp đó việc truy cập vào một nút nhất định có thể không hiệu quả (giả sử bạn muốn nút 4 - nếu đối tượng nút không được cung cấp, bạn có thể phải tìm kiếm nó) . Trong trường hợp này, bạn cũng sẽ mất các phần của biểu đồ không thể truy cập được từ nút gốc. Tôi nghĩ đây là trường hợp f64 Rainbow đang giả định khi anh ấy nói độ phức tạp về thời gian để truy cập một nút nhất định là O (n).

Nếu không, bạn cũng có thể giữ một mảng (hoặc bản đồ băm) chứa đầy các con trỏ đến mỗi nút. Điều này cho phép O (1) truy cập vào một nút nhất định, nhưng làm tăng mức sử dụng bộ nhớ một chút. Nếu n là số nút và e là số cạnh, thì độ phức tạp không gian của cách tiếp cận này sẽ là O (n + e).

Độ phức tạp không gian đối với cách tiếp cận ma trận sẽ nằm dọc theo các dòng của O (n ^ 2) (giả sử các cạnh là một hướng). Nếu đồ thị của bạn thưa thớt, bạn sẽ có rất nhiều ô trống trong ma trận của mình. Nhưng nếu đồ thị của bạn được kết nối đầy đủ (e = n ^ 2), điều này so sánh thuận lợi với cách tiếp cận đầu tiên. Như RG nói, bạn cũng có thể có ít lần bỏ lỡ bộ nhớ cache hơn với cách tiếp cận này nếu bạn phân bổ ma trận dưới dạng một phần bộ nhớ, điều này có thể làm cho việc theo dõi nhiều cạnh xung quanh đồ thị nhanh hơn.

Cách tiếp cận thứ ba có lẽ là hiệu quả về không gian nhất đối với hầu hết các trường hợp - O (e) - nhưng sẽ làm cho việc tìm tất cả các cạnh của một nút nhất định trở thành một việc vặt của O (e). Tôi không thể nghĩ ra một trường hợp mà điều này sẽ rất hữu ích.


Danh sách cạnh là tự nhiên đối với thuật toán của Kruskal ("đối với mỗi cạnh, thực hiện tra cứu trong union-find"). Ngoài ra, Skiena (xuất bản lần thứ 2, trang 157) nói về danh sách cạnh là cấu trúc dữ liệu cơ bản cho đồ thị trong thư viện Combinatorica của anh ấy (là một thư viện có mục đích chung gồm nhiều thuật toán). Anh ấy đề cập rằng một trong những lý do cho điều này là những ràng buộc áp đặt bởi mô hình tính toán của Mathematica, đó là môi trường mà Combinatorica sống.
Evgeni Sergeev

5

Hãy xem bảng so sánh trên wikipedia. Nó cung cấp cho bạn một sự hiểu biết khá tốt về thời điểm sử dụng mỗi biểu diễn của đồ thị.


4

Có một tùy chọn khác: các nút là đối tượng, các cạnh cũng là đối tượng, mỗi cạnh ở cùng một lúc trong hai danh sách được liên kết kép: danh sách tất cả các cạnh đi ra từ cùng một nút và danh sách tất cả các cạnh đi vào cùng một nút .

struct Node {
    ... node payload ...
    Edge *first_in;    // All incoming edges
    Edge *first_out;   // All outgoing edges
};

struct Edge {
    ... edge payload ...
    Node *from, *to;
    Edge *prev_in_from, *next_in_from; // dlist of same "from"
    Edge *prev_in_to, *next_in_to;     // dlist of same "to"
};

Chi phí bộ nhớ lớn (2 con trỏ trên mỗi nút và 6 con trỏ trên mỗi cạnh) nhưng bạn nhận được

  • Chèn nút O (1)
  • Chèn cạnh O (1) (con trỏ cho trước tới các nút "từ" và "tới")
  • O (1) xóa cạnh (cho con trỏ)
  • Xóa nút O (deg (n)) (cho con trỏ)
  • O (deg (n)) tìm hàng xóm của một nút

Cấu trúc cũng có thể biểu diễn một đồ thị khá tổng quát: đa đồ thị có định hướng với các vòng (nghĩa là bạn có thể có nhiều cạnh riêng biệt giữa hai nút giống nhau bao gồm nhiều vòng lặp khác nhau - các cạnh đi từ x đến x).

Giải thích chi tiết hơn về cách tiếp cận này có sẵn tại đây .


3

Được rồi, vì vậy nếu các cạnh không có trọng số, ma trận có thể là một mảng nhị phân và việc sử dụng các toán tử nhị phân có thể khiến mọi thứ diễn ra thực sự, rất nhanh trong trường hợp đó.

Nếu đồ thị thưa thớt, phương thức đối tượng / con trỏ có vẻ hiệu quả hơn nhiều. Giữ đối tượng / con trỏ trong một cấu trúc dữ liệu cụ thể để thu hút chúng vào một đoạn bộ nhớ duy nhất cũng có thể là một kế hoạch tốt hoặc bất kỳ phương pháp nào khác để khiến chúng ở lại với nhau.

Danh sách kề - chỉ đơn giản là danh sách các nút được kết nối - cho đến nay dường như là hiệu quả về bộ nhớ nhất, nhưng cũng có thể là chậm nhất.

Đảo ngược một đồ thị có hướng là dễ dàng với biểu diễn ma trận và dễ dàng với danh sách kề, nhưng không quá tuyệt vời với biểu diễn đối tượng / con trỏ.

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.