Представление и обработка графов: матрицы и списки смежности

Модуль "Графы" • урок 1 из 4 • ~12 минут

Суть урока. Граф - одна из самых универсальных структур данных. Если деревья моделируют иерархии, то графы описывают произвольные связи между объектами: дороги между городами, дружбы в соцсетях, зависимости между задачами. В этом уроке мы разберем базовые понятия и три способа представления графа в памяти.

Основные понятия

простой неориентированный граф: вершины A, B, C и D, ребра между ними, степень вершины A равна трем
Вершины и ребра простого графа: степень вершины - число ребер при ней

Граф G = (V, E) состоит из:

Виды графов

Терминология

Граф vs Дерево
Дерево - это частный случай графа: связный ациклический неориентированный граф. У дерева с N вершинами всегда ровно N-1 ребер. Если добавить хотя бы одно ребро - появится цикл.

Представление и обработка графов в памяти

Существует три основных способа хранить граф в программе. Каждый имеет свои плюсы и минусы.

Представление и обработка графов: один граф, его матрица смежности, список смежности и список ребер
Одни и те же четыре ребра в трех представлениях: единицы матрицы стоят там же, где соседи в списках

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

Двумерный массив adj[N][N], где adj[i][j] = 1, если существует ребро из вершины i в вершину j, и 0 - если нет. Для взвешенных графов вместо 1 хранят вес ребра.

// Матрица смежности для неориентированного графа
#define MAX_V 100

int adj[MAX_V][MAX_V]; // Глобальный массив, инициализирован нулями
int n; // Количество вершин

void AddEdge(int u, int v) {
    adj[u][v] = 1;
    adj[v][u] = 1; // Для неориентированного графа
}

// Вывод всех соседей вершины v
void PrintNeighbors(int v) {
    for (int i = 0; i < n; i++) {
        if (adj[v][i]) printf("%d ", i);
    }
}

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

Для каждой вершины хранится список (массив или связный список) ее соседей. Это самый популярный способ представления на практике.

// Список смежности на основе массивов (простейший вариант)
#define MAX_V 100
#define MAX_E 1000

// Для каждой вершины храним список соседей
int neighbors[MAX_V][MAX_V]; // neighbors[v][k] - k-й сосед вершины v
int degree[MAX_V];           // degree[v] - количество соседей вершины v

void AddEdge(int u, int v) {
    neighbors[u][degree[u]++] = v;
    neighbors[v][degree[v]++] = u; // Для неориентированного графа
}

// Вывод всех соседей вершины v
void PrintNeighbors(int v) {
    for (int i = 0; i < degree[v]; i++) {
        printf("%d ", neighbors[v][i]);
    }
}

3. Список ребер

Массив пар (u, v) - просто перечисление всех ребер графа.

// Список ребер
struct Edge {
    int u, v;    // Вершины, соединенные ребром
    int weight;  // Вес ребра (для взвешенных графов)
};

struct Edge edges[MAX_E];
int edgeCount = 0;

void AddEdge(int u, int v, int w) {
    edges[edgeCount].u = u;
    edges[edgeCount].v = v;
    edges[edgeCount].weight = w;
    edgeCount++;
}

Сравнение представлений

Когда что использовать?
  • Матрица смежности: плотные графы (M ≈ N²), частая проверка "есть ли ребро?", небольшое N.
  • Список смежности: разреженные графы (M ≪ N²), обходы в глубину и в ширину. Самый универсальный выбор.
  • Список ребер: алгоритмы, работающие непосредственно с ребрами (сортировка ребер, алгоритм Краскала).

Проверьте себя

Граф имеет 100 вершин и всего 200 ребер (разреженный). Какое представление наиболее экономично по памяти?

Коротко: главное

  • Граф G = (V, E) - вершины и ребра. Бывает ориентированным, неориентированным, взвешенным.
  • Дерево - частный случай графа (связный, ациклический, N-1 ребер).
  • Матрица смежности - O(N²) памяти, O(1) проверка ребра.
  • Список смежности - O(N+M) памяти, O(degree) перебор соседей. Самый популярный.
  • Список ребер - O(M) памяти, удобен для алгоритмов над ребрами.
Как вам урок? Спасибо за отзыв!