Що краще, списки суміжності або матриці суміжності для задач графіків у C ++?


129

Що краще, списки суміжності або матриця суміжності для проблем із графіками в C ++? Які переваги та недоліки кожного?


21
Використовувана вами структура не залежить від мови, а від проблеми, яку ви намагаєтеся вирішити.
авакар

1
Я мав на увазі для загального використання, як алгоритм djikstra, я задав це питання, тому що я не знаю, пов'язана реалізація списку, варто спробувати, тому що важче кодувати, ніж матриця суміжності.
magiix

Списки на C ++ настільки ж прості, як і введення тексту std::list(а ще краще std::vector).
авакар

1
@avakar: або std::dequeабо std::set. Це залежить від того, як графік буде змінюватися з часом і які алгоритми ви збираєтеся запускати на них.
Олександр К.

Відповіді:


125

Це залежить від проблеми.

Матриця суміжності

  • Використовує O (n ^ 2) пам'ять
  • Швидкий пошук і перевірка наявності чи відсутності певного краю
    між будь-якими двома вузлами O (1)
  • Він повільно перебирається по всіх краях
  • Додавати / видаляти вузол повільно; складна операція O (n ^ 2)
  • Швидко додати новий край O (1)

Список суміжності

  • Використання пам'яті залежить від кількості ребер (а не кількості вузлів),
    що може заощадити багато пам’яті, якщо матриця суміжності рідка
  • Виявлення наявності або відсутності конкретного краю між будь-якими двома вузлами
    дещо повільніше, ніж у матриці O (k); де k - кількість вузлів сусідів
  • Він швидко перебирається через усі краї, тому що ви можете отримати доступ до будь-яких сусідських вузлів безпосередньо
  • Швидко додати / видалити вузол; простіше, ніж матричне подання
  • Швидко додати новий край O (1)

пов'язані списки важче кодувати, ви вважаєте, на їх реалізацію варто витратити трохи часу, щоб вивчити її?
magiix

11
@magiix: Так, я думаю, ви повинні зрозуміти, як кодувати пов'язані списки, якщо це потрібно, але також важливо не винаходити колесо: cplusplus.com/reference/stl/list
Марк Байєрс

хтось може надати посилання з чистим кодом для скажімо, вперше пошук ширини у форматі пов'язаних списків ??
magiix

Використання std :: list geeksforgeeks.org/breadth-first-traversal-for-a-graph
atif93

78

Ця відповідь стосується не лише C ++, оскільки все, що згадується, стосується самих структур даних, незалежно від мови. І моя відповідь передбачає, що ви знаєте основну структуру списків та матриць суміжності.

Пам'ять

Якщо пам'ять є вашою основною проблемою, ви можете дотримуватися цієї формули для простого графіка, який дозволяє циклів:

Матриця суміжності займає n 2 /8 байт простору (один біт на вході).

Список суміжності займає 8е простір, де e - кількість ребер (32-бітний комп'ютер).

Якщо визначити щільність графіка як d = e / n 2 (кількість ребер, розділене на максимальну кількість ребер), ми можемо знайти «точку розриву», де список займає більше пам’яті, ніж матриця:

8e> п 2 /8 при д> 1/64

Тож із цими числами (все ще 32-бітовими) точка перелому припадає на 1/64 . Якщо щільність (e / n 2 ) більша за 1/64, то матриця краща, якщо ви хочете зберегти пам'ять.

Про це ви можете прочитати на wikipedia (стаття про матриці суміжності) та багатьох інших сайтах.

Бічна примітка : можна підвищити ефективність простору матриці суміжності, використовуючи хеш-таблицю, де ключі - пара вершин (лише непряме).

Ітерація та пошук

Списки суміжності - це компактний спосіб представлення лише існуючих країв. Однак це відбувається за рахунок можливо повільного пошуку конкретних країв. Оскільки кожен список дорівнює мірі вершини, найгіршим часом пошуку для перевірки конкретного краю може стати O (n), якщо список не має порядку. Однак пошук сусідів вершини стає тривіальним, і для розрідженого або невеликого графіка вартість ітерації через списки суміжності може бути незначною.

Матриці суміжності, з іншого боку, використовують більше місця для забезпечення постійного часу пошуку. Оскільки кожен можливий запис існує, ви можете перевірити наявність краю в постійному часі за допомогою індексів. Однак пошук сусідів займає O (n), оскільки вам потрібно перевірити всіх можливих сусідів. Очевидним недоліком простору є те, що для розріджених графіків додається багато прокладки. Див. Обговорення в пам'яті вище для отримання додаткової інформації про це.

Якщо ви все ще не впевнені, що використовувати : Більшість проблем у реальному світі створюють рідкісні та / або великі графіки, які краще підходять для представлення списку суміжності. Вони можуть здатися складнішими для впровадження, але я запевняю вас, що їх немає, і коли ви пишете BFS або DFS і хочете отримати всі сусіди вузла, вони просто в одному рядку коду. Однак зауважте, що я взагалі не рекламую списки суміжності.


9
+1 для ознайомлення, але це має бути виправлено фактичною структурою даних, яка використовується для зберігання списків суміжності. Ви можете зберігати для кожної вершини свій список суміжності у вигляді карти чи вектора. У цьому випадку фактичні числа у ваших формулах мають бути оновлені. Крім того, подібні обчислення можуть бути використані для оцінки точок беззбитковості за часовою складністю певних алгоритмів.
Олександр К.

3
Так, ця формула призначена для конкретного сценарію. Якщо ви хочете отримати грубу відповідь, продовжуйте користуватися цією формулою або модифікуйте її відповідно до ваших специфікацій (наприклад, у більшості людей зараз 64-розрядний комп'ютер :))
keyser

1
Для тих, хто цікавиться, формула точки перелому (максимальна кількість середніх ребер у графі n вузлів) є e = n / s, де sрозмір вказівника.
deceleratedcaviar

33

Гаразд, я склав часові та просторові складності основних операцій на графіках.
Зображення внизу повинно бути зрозумілим.
Зверніть увагу, наскільки матриця суміжності є кращою, коли ми очікуємо, що графік буде щільним, і як перелік суміжності є кращим, коли ми очікуємо, що графік буде рідким.
Я зробив деякі припущення. Запитайте мене, чи потребує уточнення складність (Час або Простір). (Наприклад, для розрідженого графіка я вважав, що En є малою константою, оскільки я припускав, що додавання нової вершини додасть лише кілька ребер, оскільки ми очікуємо, що графік залишиться розрідженим навіть після додавання цього вершина.)

Скажіть, будь ласка, чи є помилки.

введіть тут опис зображення


Якщо невідомо, чи графік щільний чи розріджений, було б правильно сказати, що складність простору для списку суміжності буде O (v + e)?

Для більшості практичних алгоритмів одна з найважливіших операцій - це повторення всіх ребер, що виходять із заданої вершини. Ви можете додати його до свого списку - це O (градус) для AL та O (V) для AM.
макс

@johnred Хіба не краще сказати, що Додавання вершини (часу) для AL - це O (1), тому що замість O (en), оскільки ми насправді не додаємо ребра при додаванні вершини. Додавання краю може бути вирішено як окрема операція. Для AM це має сенс рахувати, але навіть там нам просто потрібно ініціалізувати відповідні рядки та стовпці нової вершини до нуля. Додавання ребер навіть для AM можна враховувати окремо.
Усман

Як додавати вершину до AL O (V)? Треба створити нову матрицю, скопіювати в неї попередні значення. Це повинно бути O (v ^ 2).
Alex_ban

19

Це залежить від того, що ви шукаєте.

За допомогою матриць суміжності ви можете швидко відповісти на запитання щодо того, чи певний край між двома вершинами належить графіку, а також ви можете мати швидкі вставки та видалення ребер. Недоліком є те , що ви повинні використовувати надлишкове простір, особливо для графів з великим числом вершин, що дуже неефективно , особливо якщо ваш графік розріджене.

З іншого боку, зі списками суміжності важче перевірити, чи є даний графік у графіку, оскільки вам потрібно шукати відповідний список, щоб знайти край, але вони є більш економічними.

Взагалі списки суміжності є правильною структурою даних для більшості застосувань графіків.


Що робити, якщо ви використовуєте словники для зберігання списку суміжності, це забезпечить вам наявність переваги за амортизованим часом O (1).
Рохіт Єравотула

10

Припустимо, у нас є графік, який має n кількість вузлів і m кількість ребер,

Приклад графіка
введіть тут опис зображення

Матриця суміжності: Ми створюємо матрицю, яка має n кількість рядків і стовпців, тому в пам'яті вона займе простір, пропорційний n 2 . Перевірка наявності двох вузлів, названих як u і v, має між ними краю, займе Θ (1) час. Наприклад, перевірка на (1, 2) край буде виглядати наступним чином у коді:

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

Якщо ви хочете визначити всі ребра, вам доведеться перебирати матрицю, для цього знадобиться дві вкладені петлі, і це займе Θ (n 2 ). (Ви можете просто використовувати верхню трикутну частину матриці для визначення всіх ребер, але це буде знову Θ (n 2 ))

Список суміжності: Ми створюємо список, який кожен вузол також вказує на інший список. У вашому списку буде n елементів, і кожен елемент вказуватиме на список, який містить кількість елементів, що дорівнює кількості сусідів цього вузла (дивіться зображення для кращої візуалізації). Таким чином, це займе простір в пам'яті, пропорційний n + m . Перевірка того, що (u, v) є ребром, займе час O (deg (u)), протягом якого deg (u) дорівнює кількості сусідів u. Тому що, максимум, ви повинні повторити список, який вказано на u. Визначення всіх ребер займе Θ (n + m).

Список суміжності прикладного графіка

введіть тут опис зображення
Ви повинні зробити вибір відповідно до своїх потреб. Через свою репутацію я не зміг поставити зображення матриці, вибачте за це


7

Якщо ви дивитесь на аналіз графів на C ++, ймовірно, першим місцем для початку буде бібліотека збільшених графіків , яка реалізує ряд алгоритмів, включаючи BFS.

EDIT

Це попереднє запитання щодо SO, ймовірно, допоможе:

як створити-ac-прискорити-непрямувати-графік-і-пройти-в-глибині-перший-пошук h


Дякую, я перевірю цю бібліотеку
magiix

+1 для збільшення графіку. Це шлях (крім звичайно, якщо це для навчальних цілей)
Tristram Gräbener

5

На це найкраще відповідати прикладами.

Подумайте, наприклад, про Флойда-Варшалла . Доводиться використовувати матрицю суміжності, інакше алгоритм буде асимптотично повільнішим.

Або що, якщо це щільний графік на 30 000 вершин? Тоді матриця суміжності може мати сенс, оскільки ви будете зберігати 1 біт на пару вершин, а не 16 біт на край (мінімум, який вам знадобиться для списку суміжності): це 107 МБ, а не 1,7 ГБ.

Але для таких алгоритмів, як DFS, BFS (і тих, хто їх використовує, наприклад, Edmonds-Karp), пошуку за пріоритетом (Dijkstra, Prim, A *) тощо, список суміжності такий же хороший, як і матриця. Добре, що матриця може мати невеликий край, коли графік щільний, але лише непересічним постійним фактором. (Скільки? Це питання експериментувати.)


2
Для таких алгоритмів, як DFS та BFS, якщо ви використовуєте матрицю, вам потрібно перевіряти весь рядок кожен раз, коли ви хочете знайти сусідні вузли, тоді як у вас уже є сусідні вузли в сусідньому списку. Чому ви вважаєте, що an adjacency list is as good as a matrixв цих випадках?
realUser404

@ realUser404 Точно сканування цілого рядка матриці є операцією O (n). Списки суміжності краще для розріджених графіків, коли вам потрібно пройти всі вихідні краї, вони можуть це зробити в O (d) (d: ступінь вузла). Матриці мають кращу ефективність кешу, ніж списки суміжності, однак через послідовний доступ, тому для дещо щільних графіків сканування матриць може мати більше сенсу.
Йохім Куджперс

3

Щоб додати відповідь keyser5053 про використання пам'яті.

Для будь-якого спрямованого графіка матриця суміжності (1 біт на край) витрачає n^2 * (1)біти пам'яті.

Для повного графіка список суміжності (з 64 бітовими вказівниками) споживає n * (n * 64)біти пам'яті, виключаючи накладні списки.

Для неповного графіка список суміжності споживає 0біти пам'яті, виключаючи накладні списки.


Для списку суміжності можна скористатися наступною формулою для визначення максимальної кількості ребер ( e), перш ніж матриця примикання буде оптимальною для пам'яті.

edges = n^2 / sвизначити максимальну кількість ребер, де sрозмір вказівника платформи.

Якщо графік динамічно оновлюється, ви можете підтримувати цю ефективність із середнім числом ребер (на вузол) n / s.


Деякі приклади із 64-бітовими вказівниками та динамічним графіком (Динамічний графік ефективно оновлює рішення проблеми після змін, а не перераховує її з нуля щоразу після внесення змін.)

Для спрямованого графіка, де nдорівнює 300, оптимальна кількість ребер на вузол із використанням списку суміжності:

= 300 / 64
= 4

Якщо ми підключимо це до формули keyser5053 d = e / n^2(де eзагальна кількість ребер), ми можемо побачити, що ми нижче точки розриву ( 1 / s):

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

Однак 64 біта для вказівника можуть бути надмірними. Якщо ви замість цього використовуєте 16-бітні цілі числа як зміщення вказівника, ми можемо встановити до 18 ребер перед точкою розриву.

= 300 / 16
= 18

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

Кожен з цих прикладів ігнорує накладні списки самих списків суміжності ( 64*2для вектору та 64 бітових вказівників).


Я не розумію частини d = (4 * 300) / (300 * 300), чи не має бути d = 4 / (300 * 300)? Оскільки формула є d = e / n^2.
Саурах

2

Залежно від реалізації матриці суміжності графік 'n' повинен бути відомий раніше для ефективної реалізації. Якщо графік занадто динамічний і потребує розширення матриці раз у раз, це також можна вважати недоліком?


1

Якщо ви використовуєте хеш-таблицю замість матриці суміжності або списку, ви отримаєте кращий або такий самий великий час виконання та простір для всіх операцій (перевірка наявності краю O(1), отримання всіх сусідніх країв є O(degree)тощо).

Існує деякий постійний коефіцієнт накладних витрат, хоча і для часу виконання, і для простору (хеш-таблиця не така швидка, як пошук пов'язаного списку чи масиву, і займає пристойну кількість додаткового місця для зменшення зіткнень).


1

Я просто торкнуся подолання компромісу регулярного представлення списку суміжних служб, оскільки інші відповіді охоплювали інші аспекти.

Можна представити графік у списку суміжності з запитом EdgeExists за амортизованим постійним часом, скориставшись перевагами структур даних словника та HashSet . Ідея полягає у тому, щоб вершини зберігалися у словнику, і для кожної вершини ми зберігаємо хеш-набір, що посилається на інші вершини, з якими є ребра.

Один незначний компроміс у цій реалізації полягає в тому, що він матиме просторову складність O (V + 2E) замість O (V + E), як у звичайному списку суміжності, оскільки ребра тут представлені двічі (оскільки кожна вершина має свій хеш-набір ребер). Але такі операції, як AddVertex , AddEdge , RemoveEdge, можна виконати за амортизований час O (1) з цією реалізацією, за винятком RemoveVertex який приймає O (V) як матрицю суміжності. Це означатиме, що крім простоти реалізації, матриця суміжності не має жодної конкретної переваги. У цій реалізації списку суміжності ми можемо заощадити місце на розрізненому графіку з майже однаковою ефективністю.

Ознайомтеся з наведеними нижче реалізаціями у сховищі Github C #. Зауважте, що для зваженого графіка він використовує вкладений словник замість комбінації набору хешів словника, щоб вмістити значення ваги. Аналогічно для спрямованого графіка існують окремі набори хешів для ребер та входів.

Розширені алгоритми

Примітка. Я вважаю, що за допомогою ледачого видалення ми можемо додатково оптимізувати операцію RemoveVertex до амортизованого O (1), хоча я не перевіряв цю ідею. Наприклад, після видалення просто позначте вершину як видалену в словнику, а потім ліниво очистіть осиротілі краї під час інших операцій.


Для матриці суміжності вилучення вершини приймає O (V ^ 2), а не O (V)
Саураб

Так. Але якщо ви використовуєте словник для відстеження індексів масиву, то він знизиться до O (V). Погляньте на цю реалізацію RemoveVertex .
justcoding121
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.