Способы хранения графа в памяти компьютера

b7cef8022b0ff02afe5b930aed37ef2e.png

В предыдущей статье мы познакомились с терминами и определениями теории графов. В этой же статье обсудим различные способы представления графа в памяти компьютера для его обработки. Покажем, какие структуры данных можно использовать, а также проговорим преимущества и недостатки каждого способа.

a68019b21b127ce697f18be2b655784f.png

Допустим, у нас есть неориентированный граф G из V = 5 вершин и E = 6 рёбер. Вершины пронумерованы от 1 до 5, рёбра для удобства также пронумеруем буквами a, b, c, d, e. Одна вершина с петлёй, изолирована от других. 

Перечисление множеств

По определению, граф — это топологическая модель, которая состоит из множества вершин и множества рёбер, их соединяющих. Значит, самый «простой» способ представить граф — определить оба этих множества.

00859725e1f26532c827987550bede46.png

Лично мне ни разу не доводилось видеть алгоритм обработки графа, который бы использовал такой способ хранения вершин и рёбер. 

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

Кстати, сами вершины обычно вообще отдельно не хранятся, а указывается только их количество V, они автоматически принимают номера от 0 до V-1. Для хранения цвета / веса или других характеристик вершин можно использовать параллельные массивы для каждого критерия.

Матрица смежности

Это самый популярный и расточительный способ представления графа в памяти. Его уместно использовать, если количество рёбер велико, порядка V^2.

Для хранения рёбер используется двумерная матрица размерности [V, V], каждый [a, b] элемент которой равен 1, если вершины a и b являются смежными и 0 в противном случае. 

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

3ace4ba0364c80f10fc3646645850e17.png

Сложность по памяти: O (V^2).

Сложность перечисления всех рёбер: O (V^2).

Сложность перечисления всех вершин, смежных с а: O (V).

Сложность проверки смежности вершин a и b: О (1).

Матрица инцидентности

Это самый расточительный способ хранения графа, его уместно использовать, если количество рёбер невелико.

Для хранения используется двумерная матрица размера [V, E], в каждом столбце которой записано одно ребро таким образом: напротив вершин, инцидентных этому ребру, записаны 1, в остальных случаях 0.

Таким образом, сумма чисел в каждом столбце равна 2, а сумма чисел в строчке a равна степени вершины а.

9b9cb076490c7e4d859952ab6c404457.png

Сложность по памяти: O (V x E).

Сложность перечисления всех рёбер: O (V x E) — хоть каждое ребро и хранится в отдельном столбце, но для получения информации об инцидентных ему вершинах нужно перебрать все числа в столбце.

Сложность перечисления всех вершин, смежных с а: O (V x E).

Сложность проверки смежности вершин a и b: O (E) — достаточно пройтись по строчкам a и b в поисках двух единиц.

Перечень рёбер

Если из матрицы инцидентности убрать все нули, в каждом столбце останется только два числа для каждого ребра — номера инцидентных ему вершин. То есть, для перечисления рёбер достаточно составить список из пар чисел, это очень экономный способ: каждое ребро хранится один раз, когда во всех других вариантах каждое ребро, как правило, записывается дважды.

Недостаток — при поиске вершин в списке рёбер нужно выполнять по две проверки — сравнивать и первую вершину, и вторую.

46b2bef010dc3a58bec666406757a26e.png

Сложность по памяти: O (E).

Сложность перечисления всех рёбер: O (E).

Сложность перечисления всех вершин, смежных с а: O (E).

Сложность проверки смежности вершин a и b: О (E).

Список рёбер можно сгруппировать по вершинам, что позволит ускорить поиск смежных вершин. У нас получится ещё три очередных способа, которые очень похожи друг на друга, отличаются лишь способом записи векторов.

Векторы смежности

Для записи вектора смежности используется двумерная матрица размером [V, S], где S — максимальная степень вершины в графе.

В каждой строчке a записаны номера вершин, смежных с а, после чего записаны нули (несуществующие номера вершин).

9793746aa41c7579d452e8bbb68d9f68.png

Сложность по памяти: O (V x S).

Сложность перечисления всех рёбер: O (V x E)

Сложность перечисления всех вершин, смежных с а: O (Sa) (Sa = степень вершины а)

Сложность проверки смежности вершин a и b: О (Sa) = O (Sb)

Массивы смежности

Для экономии памяти, используется ступенчатый массив, длина каждой строки равна степени данной вершины.

380ffe52fcd8386c173172ad2d82b268.png

Сложность по памяти: O (сумма степеней всех вершин).

Списки смежности

Здесь используется односвязный список для перечисления всех смежных вершин.

df67c8cd1017d0026bfbaac565287bac.png

Сложность по памяти: O (V + сумма степеней всех вершин).

Структура с оглавлением

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

ee3ce82368048157a68a2662ff7db584.png

Сложность по памяти: O (V + сумма степеней всех вершин).

Список вершин и список рёбер

Самый экстравагантный способ хранения графа.

Вершины записываются в односвязных список, от каждой вершины есть указатель на список всех рёбер, инцидентных данной вершины. Каждое ребро, в свою очередь, имеет указатель на вторую инцидентную ей вершину и на следующее ребро. 

d6e5f0bf9b20173e7c56806bfcaa44d3.png

Получается весьма разветвлённый граф для представления графа.

5fc6f86ebd3e84274be311e90c2044d4.png

Сложность по памяти: O (V + сумма степеней всех вершин).

Сложность перечисления всех рёбер: O (V x E)

Сложность перечисления всех вершин, смежных с а: O (Sa) (Sa = степень вершины а)

Сложность проверки смежности вершин a и b: О (Sa) = O (Sb)

Теперь, когда мы знаем способы представления графа в памяти компьютера, можем выбирать наиболее приемлемые варианты для каждой конкретной задачи.

На курсе «Алгоритмы и структуры данных» мы рассматриваем следующие алгоритмы в модуле «Теория графов»: поиск вширь и вглубь, топологическая сортировка (Кана, Таряна, Демукрона), поиск компонент сильной связности (Косарайю), поиск минимального скелета (Прима, Краскала, Борувки), поиск кратчайшего пути (Флойда-Уоршалла, Баллмана-Форда, Дейкстры), алгоритм Джонсона для поиск кратчайшего пути в орграфе с отрицательными дугами (см. мою статью), решение задачи коммивояжера (ближайшего соседа, кратчайшего ребра, ближайшего посредника) и вишенка на торте — алгоритм А* звезда поиска кратчайшего пути с эвристической функцией.

Также хочу пригласить всех желающих на бесплатный демоурок по теме «Дерево отрезков — быстро и просто». Зарегистрироваться на урок.

© Habrahabr.ru