Điều gì là tốt hơn, danh sách kề hoặc ma trận kề cho các vấn đề đồ thị trong C ++?


129

Điều gì là tốt hơn, danh sách kề hoặc ma trận kề, cho các vấn đề đồ thị trong C ++? Những lợi thế và bất lợi của mỗi là gì?


21
Cấu trúc bạn sử dụng không phụ thuộc vào ngôn ngữ mà phụ thuộc vào vấn đề bạn đang cố gắng giải quyết.
avakar

1
Tôi có nghĩa là để sử dụng chung như thuật toán djikstra, tôi đã hỏi câu hỏi này vì tôi không biết việc thực hiện danh sách được liên kết đáng để thử vì nó khó mã hóa hơn so với ma trận kề.
magiix

Danh sách trong C ++ dễ như gõ std::list(hoặc tốt hơn, std::vector).
avakar

1
@avakar: std::dequehoặc std::set. Nó phụ thuộc vào cách biểu đồ sẽ thay đổi theo thời gian và thuật toán bạn định chạy trên chúng.
Alexandre C.

Câu trả lời:


125

Nó phụ thuộc vào vấn đề.

Ma trận kề

  • Sử dụng bộ nhớ O (n ^ 2)
  • Thật nhanh chóng để tra cứu và kiểm tra sự hiện diện hay vắng mặt của một cạnh cụ thể
    giữa bất kỳ hai nút O (1) nào
  • Nó chậm để lặp lại trên tất cả các cạnh
  • Thật chậm để thêm / xóa một nút; một hoạt động phức tạp O (n ^ 2)
  • Thật nhanh chóng để thêm một cạnh mới O (1)

Danh sách điều chỉnh

  • Việc sử dụng bộ nhớ phụ thuộc vào số cạnh (không phải số nút),
    có thể tiết kiệm rất nhiều bộ nhớ nếu ma trận kề là thưa thớt
  • Tìm sự hiện diện hoặc vắng mặt của cạnh cụ thể giữa hai nút bất kỳ
    chậm hơn một chút so với ma trận O (k); Trong đó k là số nút lân cận
  • Nó nhanh chóng lặp đi lặp lại trên tất cả các cạnh vì bạn có thể truy cập trực tiếp vào bất kỳ nút lân cận nút nào
  • Thật nhanh chóng để thêm / xóa một nút; dễ dàng hơn so với biểu diễn ma trận
  • Thật nhanh chóng để thêm một cạnh mới O (1)

danh sách liên kết khó mã hóa hơn, bạn có nghĩ rằng việc triển khai đáng để dành thời gian tìm hiểu nó không?
magiix

11
@magiix: Có Tôi nghĩ bạn nên hiểu cách mã hóa các danh sách được liên kết nếu cần, nhưng điều quan trọng là không phát minh lại bánh xe: cplusplus.com/reference/stl/list
Đánh dấu vào

bất cứ ai cũng có thể cung cấp một liên kết với một mã sạch để nói tìm kiếm đầu tiên Breadth ở định dạng danh sách được liên kết ??
magiix


78

Câu trả lời này không chỉ dành cho C ++ vì mọi thứ được đề cập là về chính cấu trúc dữ liệu, bất kể ngôn ngữ. Và, câu trả lời của tôi là giả sử rằng bạn biết cấu trúc cơ bản của danh sách kề và ma trận.

Ký ức

Nếu bộ nhớ là mối quan tâm chính của bạn, bạn có thể làm theo công thức này cho một biểu đồ đơn giản cho phép các vòng lặp:

Một ma trận kề là chiếm không gian 2/8 byte (một bit cho mỗi mục nhập).

Một danh sách kề chiếm không gian 8e, trong đó e là số cạnh (máy tính 32 bit).

Nếu chúng ta xác định mật độ của biểu đồ là d = e / n 2 (số cạnh chia cho số cạnh tối đa), chúng ta có thể tìm thấy "điểm dừng" trong đó danh sách chiếm nhiều bộ nhớ hơn ma trận:

8e> n 2 /8 khi d> 1/64

Vì vậy, với những con số này (vẫn là 32 bit cụ thể), điểm dừng ở 1/64 . Nếu mật độ (e / n 2 ) lớn hơn 1/64, thì nên sử dụng ma trận nếu bạn muốn tiết kiệm bộ nhớ.

Bạn có thể đọc về điều này tại wikipedia (bài viết về ma trận kề) và rất nhiều trang web khác.

Lưu ý bên lề : Người ta có thể cải thiện hiệu quả không gian của ma trận kề bằng cách sử dụng bảng băm trong đó các khóa là các cặp đỉnh (chỉ vô hướng).

Lặp lại và tra cứu

Danh sách điều chỉnh là một cách nhỏ gọn chỉ đại diện cho các cạnh hiện có. Tuy nhiên, điều này đi kèm với chi phí có thể tìm kiếm chậm các cạnh cụ thể. Vì mỗi danh sách miễn là mức độ của một đỉnh, thời gian tra cứu trường hợp xấu nhất để kiểm tra một cạnh cụ thể có thể trở thành O (n), nếu danh sách không có thứ tự. Tuy nhiên, việc tìm kiếm các lân cận của một đỉnh trở nên tầm thường và đối với một biểu đồ thưa thớt hoặc nhỏ, chi phí lặp lại qua các danh sách kề có thể không đáng kể.

Mặt khác, ma trận điều chỉnh sử dụng nhiều không gian hơn để cung cấp thời gian tra cứu liên tục. Vì mỗi mục có thể tồn tại, bạn có thể kiểm tra sự tồn tại của một cạnh trong thời gian không đổi bằng cách sử dụng các chỉ mục. Tuy nhiên, tra cứu hàng xóm mất O (n) vì bạn cần kiểm tra tất cả các hàng xóm có thể. Hạn chế không gian rõ ràng là đối với các biểu đồ thưa thớt, rất nhiều phần đệm được thêm vào. Xem các cuộc thảo luận về bộ nhớ ở trên để biết thêm thông tin về điều này.

Nếu bạn vẫn không chắc chắn nên sử dụng cái gì : Hầu hết các vấn đề trong thế giới thực tạo ra các biểu đồ thưa thớt và / hoặc lớn, phù hợp hơn cho các biểu diễn danh sách kề. Chúng có vẻ khó thực hiện hơn nhưng tôi đảm bảo với bạn rằng chúng không phải và khi bạn viết BFS hoặc DFS và muốn tìm nạp tất cả hàng xóm của một nút thì chúng chỉ cách một dòng mã. Tuy nhiên, lưu ý rằng tôi không quảng bá danh sách kề.


9
+1 cho cái nhìn sâu sắc, nhưng điều này phải được sửa chữa bởi cấu trúc dữ liệu thực tế được sử dụng để lưu trữ danh sách kề. Bạn có thể muốn lưu trữ cho mỗi đỉnh danh sách kề của nó dưới dạng bản đồ hoặc vectơ, trong trường hợp đó, các số thực trong công thức của bạn phải được cập nhật. Ngoài ra, các tính toán tương tự có thể được sử dụng để đánh giá các điểm hòa vốn về độ phức tạp thời gian của các thuật toán cụ thể.
Alexandre C.

3
Vâng, công thức này là cho một kịch bản cụ thể. Nếu bạn muốn có câu trả lời sơ bộ, hãy tiếp tục và sử dụng công thức này hoặc sửa đổi nó theo thông số kỹ thuật của bạn khi cần (ví dụ: hầu hết mọi người hiện nay có máy tính 64 bit :))
keyer

1
Đối với những người quan tâm, công thức cho điểm phá vỡ (số cạnh trung bình tối đa trong biểu đồ của n nút) là e = n / s, trong đó skích thước con trỏ.
giảm tốc

33

Được rồi, tôi đã biên soạn độ phức tạp Thời gian và Không gian của các hoạt động cơ bản trên biểu đồ.
Hình ảnh dưới đây nên tự giải thích.
Lưu ý cách Ma trận điều chỉnh thích hợp hơn khi chúng ta kỳ vọng biểu đồ sẽ dày đặc và cách Danh sách điều chỉnh thích hợp hơn khi chúng ta mong đợi biểu đồ sẽ thưa thớt.
Tôi đã đưa ra một số giả định. Hỏi tôi nếu một sự phức tạp (Thời gian hoặc Không gian) cần làm rõ. (Ví dụ: Đối với một biểu đồ thưa thớt, tôi đã coi En là một hằng số nhỏ, vì tôi đã giả sử rằng việc thêm một đỉnh mới sẽ chỉ thêm một vài cạnh, bởi vì chúng tôi hy vọng biểu đồ vẫn còn thưa thớt ngay cả sau khi thêm đỉnh.)

Xin vui lòng cho tôi biết nếu có bất kỳ sai lầm.

nhập mô tả hình ảnh ở đây


Trong trường hợp không biết là đồ thị dày đặc hay thưa thớt, liệu có đúng không khi nói rằng độ phức tạp không gian cho danh sách kề sẽ là O (v + e)?

Đối với hầu hết các thuật toán thực tế, một trong những hoạt động quan trọng nhất là lặp qua tất cả các cạnh đi ra khỏi một đỉnh đã cho. Bạn có thể muốn thêm nó vào danh sách của mình - đó là O (độ) cho AL và O (V) cho AM.
tối đa

@johnred không tốt hơn khi nói rằng Thêm một đỉnh (thời gian) cho AL là O (1) vì thay vì O (en) vì chúng ta không thực sự thêm các cạnh khi thêm một đỉnh. Thêm một cạnh có thể được xử lý như là một hoạt động riêng biệt. Đối với AM, nó có ý nghĩa đối với tài khoản nhưng ngay cả ở đó chúng ta chỉ cần khởi tạo các hàng và cột có liên quan của đỉnh mới bằng không. Ngoài ra các cạnh thậm chí cho AM có thể được tính riêng.
Usman

Làm thế nào là thêm một đỉnh vào AL O (V)? Chúng ta phải tạo một ma trận mới, sao chép các giá trị trước đó vào nó. Nó phải là O (v ^ 2).
Alex_ban

19

Nó phụ thuộc vào những gì bạn đang tìm kiếm.

Với ma trận kề, bạn có thể trả lời nhanh các câu hỏi liên quan đến việc nếu một cạnh cụ thể giữa hai đỉnh thuộc về biểu đồ và bạn cũng có thể có các phần chèn và xóa nhanh các cạnh. Các nhược điểm là bạn phải sử dụng không gian quá nhiều, đặc biệt đối với đồ thị với nhiều đỉnh, đó là rất không hiệu quả đặc biệt là nếu đồ thị của bạn là thưa thớt.

Mặt khác, với các danh sách kề , khó kiểm tra xem một cạnh đã cho có nằm trong biểu đồ hay không, bởi vì bạn phải tìm kiếm qua danh sách thích hợp để tìm cạnh, nhưng chúng hiệu quả hơn về không gian.

Nói chung, mặc dù, danh sách kề là cấu trúc dữ liệu phù hợp cho hầu hết các ứng dụng của đồ thị.


Điều gì xảy ra nếu bạn sử dụng từ điển để lưu trữ danh sách kề, điều đó sẽ cho bạn sự hiện diện của một cạnh trong thời gian khấu hao O (1).
Rohith Yeravothula

10

Giả sử chúng ta có một đồ thị có n số nút và m số cạnh,

Biểu đồ ví dụ
nhập mô tả hình ảnh ở đây

Ma trận điều chỉnh: Chúng tôi đang tạo một ma trận có n số hàng và cột để trong bộ nhớ, nó sẽ chiếm không gian tỷ lệ với n 2 . Kiểm tra nếu hai nút có tên là uv có cạnh giữa chúng sẽ mất (1) thời gian. Ví dụ: kiểm tra (1, 2) là một cạnh sẽ trông như sau trong mã:

if(matrix[1][2] == 1)

Nếu bạn muốn xác định tất cả các cạnh, bạn phải lặp lại ma trận tại đây sẽ yêu cầu hai vòng lặp lồng nhau và sẽ mất (n 2 ). (Bạn chỉ có thể sử dụng phần tam giác trên của ma trận để xác định tất cả các cạnh nhưng nó sẽ lại (n 2 ))

Danh sách điều chỉnh: Chúng tôi đang tạo một danh sách mà mỗi nút cũng trỏ đến một danh sách khác. Danh sách của bạn sẽ có n phần tử và mỗi phần tử sẽ trỏ đến một danh sách có số mục bằng với số hàng xóm của nút này (nhìn hình ảnh để hiển thị rõ hơn). Vì vậy, nó sẽ chiếm không gian trong bộ nhớ tỷ lệ với n + m . Kiểm tra xem (u, v) là một cạnh sẽ mất thời gian O (deg (u)) trong đó deg (u) bằng số hàng xóm của u. Bởi vì nhiều nhất, bạn phải lặp đi lặp lại danh sách được chỉ bởi u. Xác định tất cả các cạnh sẽ mất Θ (n + m).

Danh sách điều chỉnh của đồ thị ví dụ

nhập mô tả hình ảnh ở đây
Bạn nên lựa chọn theo nhu cầu của bạn. Vì danh tiếng của tôi, tôi không thể đặt hình ảnh của ma trận, xin lỗi vì điều đó


7

Nếu bạn đang xem phân tích biểu đồ trong C ++, có lẽ nơi đầu tiên để bắt đầu sẽ là thư viện biểu đồ boost , nơi thực hiện một số thuật toán bao gồm BFS.

BIÊN TẬP

Câu hỏi trước đây về SO có thể sẽ giúp:

how-to-created-ac-boost-undirected-graph-and-traverse-it-in-deep-First-searc h


Cảm ơn bạn tôi sẽ kiểm tra thư viện này
magiix

+1 cho biểu đồ tăng. Đây là con đường để đi (tất nhiên trừ khi đó là vì mục đích giáo dục)
Tristram Gräbener

5

Điều này được trả lời tốt nhất với các ví dụ.

Hãy nghĩ về Floyd-Warshall chẳng hạn. Chúng ta phải sử dụng ma trận kề, nếu không thuật toán sẽ chậm hơn.

Hoặc nếu đó là một biểu đồ dày đặc trên 30.000 đỉnh thì sao? Sau đó, một ma trận kề có thể có ý nghĩa, vì bạn sẽ lưu trữ 1 bit cho mỗi cặp đỉnh, thay vì 16 bit trên mỗi cạnh (mức tối thiểu mà bạn cần cho danh sách kề): đó là 107 MB, thay vì 1,7 GB.

Nhưng đối với các thuật toán như DFS, BFS (và những thuật toán sử dụng nó, như Edmonds-Karp), tìm kiếm ưu tiên đầu tiên (Dijkstra, Prim, A *), v.v., một danh sách kề là tốt như một ma trận. Chà, một ma trận có thể có một cạnh nhỏ khi đồ thị dày đặc, nhưng chỉ bởi một yếu tố không đổi không đáng kể. (Bao nhiêu? Đó là vấn đề thử nghiệm.)


2
Đối với các thuật toán như DFS và BFS, nếu bạn sử dụng ma trận thì bạn cần kiểm tra toàn bộ hàng mỗi lần bạn muốn tìm các nút liền kề, trong khi bạn đã có các nút liền kề trong danh sách liền kề. Tại sao bạn nghĩ an adjacency list is as good as a matrixtrong những trường hợp đó?
realUser404

@ realUser404 Chính xác, quét toàn bộ một hàng ma trận là thao tác O (n). Danh sách điều chỉnh tốt hơn cho các biểu đồ thưa thớt khi bạn cần đi qua tất cả các cạnh đi, chúng có thể làm điều đó trong O (d) (d: độ của nút). Ma trận có hiệu suất bộ đệm tốt hơn so với danh sách kề, vì truy cập tuần tự, do đó, đối với một biểu đồ hơi dày đặc, việc quét ma trận có thể có ý nghĩa hơn.
Joool Kuijpers

3

Để thêm vào câu trả lời của keyer5053 về việc sử dụng bộ nhớ.

Đối với bất kỳ đồ thị có hướng nào, một ma trận kề (tại 1 bit trên mỗi cạnh) sẽ tiêu tốn n^2 * (1)bit của bộ nhớ.

Đối với một biểu đồ hoàn chỉnh , một danh sách kề (với con trỏ 64 bit) sẽ tiêu tốn n * (n * 64)bit của bộ nhớ, không bao gồm chi phí danh sách.

Đối với một biểu đồ không đầy đủ, một danh sách kề sẽ tiêu tốn 0bit của bộ nhớ, không bao gồm chi phí danh sách.


Đối với danh sách kề, bạn có thể sử dụng công thức sau để xác định số cạnh tối đa ( e) trước khi ma trận kề là tối ưu cho bộ nhớ.

edges = n^2 / sđể xác định số cạnh tối đa, skích thước con trỏ của nền tảng ở đâu.

Nếu biểu đồ của bạn được cập nhật động, bạn có thể duy trì hiệu quả này với số cạnh trung bình (trên mỗi nút) là n / s.


Một số ví dụ với con trỏ 64 bit và biểu đồ động (Biểu đồ động cập nhật giải pháp cho vấn đề một cách hiệu quả sau khi thay đổi, thay vì tính toán lại từ đầu mỗi lần sau khi thay đổi được thực hiện.)

Đối với đồ thị có hướng, trong đó nlà 300, số cạnh tối ưu trên mỗi nút sử dụng danh sách kề là:

= 300 / 64
= 4

Nếu chúng ta cắm công thức này vào công thức của keyer5053, d = e / n^2(trong đó etổng số cạnh), chúng ta có thể thấy chúng ta ở dưới điểm ngắt ( 1 / s):

d = (4 * 300) / (300 * 300)
d < 1/64
aka 0.0133 < 0.0156

Tuy nhiên, 64 bit cho một con trỏ có thể là quá mức cần thiết. Thay vào đó, nếu bạn sử dụng số nguyên 16 bit làm điểm bù con trỏ, chúng ta có thể khớp tối đa 18 cạnh trước khi ngắt điểm.

= 300 / 16
= 18

d = ((18 * 300) / (300^2))
d < 1/16
aka 0.06 < 0.0625

Mỗi ví dụ này bỏ qua chi phí chung của danh sách kề ( 64*2đối với một con trỏ vectơ và 64 bit).


Tôi không hiểu phần này d = (4 * 300) / (300 * 300), phải không d = 4 / (300 * 300)? Vì công thức là d = e / n^2.
Saurabh

2

Tùy thuộc vào việc triển khai Ma trận điều chỉnh, 'n' của biểu đồ nên được biết trước để triển khai hiệu quả. Nếu đồ thị quá động và yêu cầu mở rộng ma trận mọi lúc và sau đó điều đó cũng có thể được tính là nhược điểm?


1

Nếu bạn sử dụng bảng băm thay vì ma trận kề hoặc danh sách, bạn sẽ nhận được thời gian và không gian lớn hơn cho tất cả các hoạt động (kiểm tra một cạnh là O(1), có được tất cả các cạnh liền kề O(degree), v.v.).

Có một số yếu tố chi phí không đổi mặc dù cho cả thời gian chạy và không gian (bảng băm không nhanh như danh sách liên kết hoặc tra cứu mảng và cần thêm một khoảng trống để giảm va chạm).


1

Tôi sẽ tiếp tục khắc phục sự đánh đổi của đại diện danh sách kề, vì các câu trả lời khác đã đề cập đến các khía cạnh khác.

Có thể biểu thị một biểu đồ trong danh sách kề với truy vấn EdgeExists trong thời gian không đổi được khấu hao, bằng cách tận dụng các cấu trúc dữ liệu của DictionaryHashset . Ý tưởng là giữ các đỉnh trong từ điển và với mỗi đỉnh, chúng ta giữ một tập băm tham chiếu đến các đỉnh khác mà nó có các cạnh.

Một sự đánh đổi nhỏ trong triển khai này là nó sẽ có độ phức tạp không gian O (V + 2E) thay vì O (V + E) như trong danh sách kề kề thông thường, vì các cạnh được biểu thị hai lần ở đây (vì mỗi đỉnh có bộ băm riêng của các cạnh). Nhưng các hoạt động như AddVertex , AddEdge , RemoveEdge có thể được thực hiện trong thời gian khấu hao O (1) với việc triển khai này, ngoại trừ RemoveVertex có ma trận kề (O) như ma trận kề. Điều này có nghĩa là ngoài việc đơn giản thực hiện, ma trận kề không có bất kỳ lợi thế cụ thể nào. Chúng ta có thể tiết kiệm không gian trên biểu đồ thưa thớt với hiệu suất gần như tương tự trong việc thực hiện danh sách kề này.

Hãy xem các triển khai bên dưới trong kho lưu trữ Github C # để biết chi tiết. Lưu ý rằng đối với biểu đồ có trọng số, nó sử dụng từ điển lồng nhau thay vì kết hợp bộ băm từ điển để phù hợp với giá trị trọng lượng. Tương tự như vậy đối với đồ thị có hướng, có các bộ băm riêng cho các cạnh vào & ra.

Thuật toán nâng cao

Lưu ý: Tôi tin rằng bằng cách sử dụng xóa lười biếng, chúng tôi có thể tối ưu hóa thêm hoạt động RemoveVertex thành O (1) được khấu hao, mặc dù tôi chưa thử nghiệm ý tưởng đó. Ví dụ, khi xóa, chỉ cần đánh dấu đỉnh là đã xóa trong từ điển và sau đó xóa các cạnh mồ côi một cách lười biếng trong các hoạt động khác.


Đối với ma trận kề, loại bỏ đỉnh mất O (V ^ 2) chứ không phải O (V)
Saurabh

Đúng. Nhưng nếu bạn sử dụng một từ điển để theo dõi các chỉ số mảng, thì nó sẽ chuyển xuống O (V). Hãy xem triển khai RemoveVertex này .
mã hóa 121
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.