Что лучше, списки смежности или матрицы смежности для задач графа в C ++?

129

Что лучше, списки смежности или матрица смежности для задач с графами в C ++? Каковы преимущества и недостатки каждого?

magiix
источник
21
Используемая вами структура зависит не от языка, а от проблемы, которую вы пытаетесь решить.
avakar
1
Я имел в виду для общего использования, как алгоритм djikstra, я задал этот вопрос, потому что я не знаю, стоит ли попробовать реализацию связанного списка, потому что ее сложнее кодировать, чем матрица смежности.
magiix
Списки в C ++ так же просто, как вводить std::list(или еще лучше std::vector).
avakar
1
@avakar: или std::dequeили std::set. Это зависит от того, как график будет меняться со временем и какие алгоритмы вы собираетесь использовать на них.
Alexandre C.

Ответы:

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 ++, поскольку все упомянутое касается самих структур данных, независимо от языка. И мой ответ предполагает, что вы знаете базовую структуру списков и матриц смежности.

объем памяти

Если память - ваша главная забота, вы можете следовать этой формуле для простого графика, который допускает циклы:

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

Список смежности занимает пространство 8e, где e - количество ребер (32-битный компьютер).

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

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

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

Вы можете прочитать об этом в Википедии (статья о матрицах смежности) и на многих других сайтах.

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

Итерация и поиск

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

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

Если вы все еще не уверены, что использовать : большинство реальных проблем создают разреженные и / или большие графы, которые лучше подходят для представлений списков смежности. Может показаться, что их сложнее реализовать, но я уверяю вас, что это не так, и когда вы пишете BFS или DFS и хотите получить всех соседей узла, они находятся всего в одной строке кода. Однако обратите внимание, что я не продвигаю списки смежности в целом.

Кейзер
источник
9
+1 для понимания, но это должно быть исправлено фактической структурой данных, используемой для хранения списков смежности. Вы можете сохранить для каждой вершины ее список смежности в виде карты или вектора, и в этом случае необходимо обновить фактические числа в ваших формулах. Кроме того, аналогичные вычисления могут использоваться для оценки точек безубыточности для временной сложности конкретных алгоритмов.
Alexandre C.
3
Да, это формула для конкретного сценария. Если вам нужен приблизительный ответ, используйте эту формулу или измените ее в соответствии с вашими требованиями по мере необходимости (например, у большинства людей сейчас 64-битный компьютер :))
keyser
1
Для тех, кто интересуется, формула для точки излома (максимальное количество средних ребер в графе из n узлов):, e = n / sгде s- размер указателя.
deceleratedcaviar
33

Хорошо, я собрал временные и пространственные сложности основных операций на графах.
Изображение ниже не требует пояснений.
Обратите внимание, насколько предпочтительна матрица смежности, когда мы ожидаем, что граф будет плотным, и как предпочтительнее использовать список смежности, когда мы ожидаем, что граф будет разреженным.
Я сделал некоторые предположения. Спросите меня, нуждается ли в пояснении сложность (время или пространство). (Например, для разреженного графа я взял En как небольшую константу, так как я предполагал, что добавление новой вершины добавит только несколько ребер, потому что мы ожидаем, что граф останется разреженным даже после добавления этого вершина) .

Подскажите пожалуйста, есть ли ошибки.

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

Джон Ред
источник
В случае, если неизвестно, является ли граф плотным или разреженным, будет ли правильным сказать, что пространственная сложность для списка смежности будет O (v + e)?
Для большинства практических алгоритмов одной из наиболее важных операций является перебор всех ребер, выходящих из данной вершины. Вы можете добавить его в свой список - это O (степень) для AL и O (V) для AM.
max
@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.

РЕДАКТИРОВАТЬ

Этот предыдущий вопрос о SO, вероятно, поможет:

как-создать-ac-boost-unirected-graph-and-traverse-it-in-depth-first-search h

Бинарный ботаник
источник
Спасибо, я проверю эту библиотеку
magiix
+1 для графика повышения. Это путь (за исключением, конечно, в образовательных целях)
Тристрам Гребенер,
5

Лучше всего ответить на этот вопрос с помощью примеров.

Подумайте, например, о Флойд-Уоршалле . Мы должны использовать матрицу смежности, иначе алгоритм будет асимптотически медленнее.

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

Но для таких алгоритмов, как DFS, BFS (и тех, которые его используют, например Эдмондс-Карп), приоритетного поиска (Dijkstra, Prim, A *) и т. Д., Список смежности ничем не хуже матрицы. Что ж, матрица может иметь небольшое ребро, когда граф плотный, но только с незначительным постоянным множителем. (Сколько? Это вопрос экспериментов.)

Евгений Сергеев
источник
2
Для таких алгоритмов, как DFS и BFS, если вы используете матрицу, вам необходимо проверять всю строку каждый раз, когда вы хотите найти соседние узлы, тогда как у вас уже есть соседние узлы в соседнем списке. Как вы думаете, почему an adjacency list is as good as a matrixв таких случаях?
realUser404
@ realUser404 Точно, сканирование всей строки матрицы - это операция O (n). Списки смежности лучше подходят для разреженных графов, когда вам нужно пройти все исходящие ребра, они могут сделать это за O (d) (d: степень узла). Однако матрицы имеют лучшую производительность кеша, чем списки смежности, из-за последовательного доступа, поэтому для довольно плотных графов сканирование матриц может иметь больше смысла.
Jochem Kuijpers
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-битных указателей).

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

В зависимости от реализации матрицы смежности число n графа должно быть известно заранее для эффективной реализации. Если график слишком динамичный и требует то и дело расширения матрицы, это тоже можно считать недостатком?

ChrisOdney
источник
1

Если вы используете хеш-таблицу вместо матрицы или списка смежности, вы получите лучшее время выполнения и пространство для всех операций (проверка на наличие ребра O(1), получение всех смежных ребер O(degree)и т. Д.).

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

Максимум
источник
1

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

Можно представить граф в списке смежности с запросом EdgeExists за амортизированное постоянное время, воспользовавшись преимуществами структур данных Dictionary и HashSet . Идея состоит в том, чтобы хранить вершины в словаре, и для каждой вершины мы сохраняем хэш-набор, ссылающийся на другие вершины, с которыми у нее есть ребра.

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

Взгляните на реализации ниже в репозитории Github C # для получения подробной информации. Обратите внимание, что для взвешенного графа он использует вложенный словарь вместо комбинации словаря и хеш-набора, чтобы учесть значение веса. Аналогично для ориентированного графа есть отдельные хеш-наборы для входных и выходных ребер.

Продвинутая алгоритмы

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

justcoding121
источник
Для матрицы смежности удаление вершины принимает O (V ^ 2), а не O (V)
Саураб
Да. Но если вы используете словарь для отслеживания индексов массива, он опустится до O (V). Взгляните на эту реализацию RemoveVertex .
justcoding121,