Як можна зберігати граф у пам'яті?
Коротка відповідь
Граф у пам'яті зазвичай зберігають як: 1) матрицю суміжності (O(V^2), швидкі перевірки ребра), 2) список суміжності (O(V+E), швидка ітерація по сусідах, зручне додавання/видалення), 3) список ребер (O(E), просте завантаження і зберігання), 4) стиснуті структури CSR/CSC для великих розріджених графів. Вибір залежить від щільності графа, потрібних операцій (перевірка ребра, обхід сусідів, оновлення) та обмежень за пам'яттю.
Детальна відповідь
Основні способи зберігання
- Матриця суміжності: двовимірний масив V×V. Пам'ять O(V^2). Перевірка наявності ребра O(1). Ітерація по сусідах O(V). Добра для щільних графів і частих перевірок ребра.
- Список суміжності: для кожної вершини список/множина сусідів. Пам'ять O(V+E). Перевірка ребра O(deg(u)) або O(1) з Set. Ітерація по сусідах O(deg(u)). Найкращий вибір для розріджених і динамічних графів.
- Список ребер: масив пар/трійок (u, v, w). Пам'ять O(E). Зручний для зберігання, серіалізації та алгоритмів на ребрах. Перевірка ребра O(E) (якщо не індексувати).
- CSR/CSC: стиснуті формати для розріджених графів. Пам'ять близька до O(V+E). Відмінна ітерація по сусідах і cache-friendly обходи. Складно/дорого оновлювати онлайн.
Матриця суміжності (Adjacency Matrix)
Двовимірний масив розміром V×V, де елемент matrix[u][v] = 0/1 або вага ребра. Для неорієнтованого графа матриця симетрична.
- Пам'ять: O(V^2). Для великих розріджених графів - неефективно.
- Перевірка ребра u→v: O(1). Ітерація сусідів: O(V).
- Додавання вершини: O(V^2) (перевиділення). Додавання ребра: O(1).
- Підходить для щільних графів і задач з частими перевірками наявності ребра.
const n = 5; // кількість вершин
const matrix = Array.from({ length: n }, () => Array(n).fill(0));
function addEdge(u, v, w = 1, directed = false) {
matrix[u][v] = w; // 1 або вага
if (!directed) matrix[v][u] = w;
}
function hasEdge(u, v) {
return matrix[u][v] !== 0;
}
function neighbors(u) {
const res = [];
for (let v = 0; v < n; v++) if (matrix[u][v] !== 0) res.push(v);
return res;
}
addEdge(0, 1);
addEdge(0, 3);
console.log(hasEdge(0, 1)); // true
console.log(neighbors(0)); // [1, 3]Список суміжності (Adjacency List)
Для кожної вершини зберігаємо колекцію її сусідів: масив, Set або Map (для зважених графів). Добре масштабується для розріджених графів і підтримує динамічні зміни.
- Пам'ять: O(V+E).
- Перевірка ребра: O(deg(u)) з масивом, O(1) з Set/Map.
- Ітерація сусідів: O(deg(u)). Додавання/видалення ребра: амортизовано O(1).
// Незважений граф: Map<number, Set<number>>
const g = new Map();
function addVertex(u) {
if (!g.has(u)) g.set(u, new Set());
}
function addEdge(u, v, directed = false) {
addVertex(u); addVertex(v);
g.get(u).add(v);
if (!directed) g.get(v).add(u);
}
function hasEdge(u, v) {
return g.has(u) && g.get(u).has(v);
}
function neighbors(u) {
return g.get(u) ? [...g.get(u)] : [];
}
function removeEdge(u, v, directed = false) {
if (g.has(u)) g.get(u).delete(v);
if (!directed && g.has(v)) g.get(v).delete(u);
}
// Приклад використання
addEdge(0, 1);
addEdge(0, 2);
console.log(hasEdge(0, 1)); // true
console.log(neighbors(0)); // [1, 2]// Зважений граф: Map<number, Map<number, number>>
const wg = new Map();
function addWeightedEdge(u, v, w, directed = false) {
if (!wg.has(u)) wg.set(u, new Map());
if (!wg.has(v)) wg.set(v, new Map());
wg.get(u).set(v, w);
if (!directed) wg.get(v).set(u, w);
}
function weight(u, v) {
return wg.has(u) ? wg.get(u).get(v) : undefined; // undefined = ребра немає
}
addWeightedEdge(1, 3, 2.5);
console.log(weight(1, 3)); // 2.5Список ребер (Edge List)
Зберігаємо масив ребер, кожне ребро - пара (u, v) або трійка (u, v, w). Простий формат для обміну даними та деяких алгоритмів (наприклад, сортування за вагою для Крускала).
- Пам'ять: O(E).
- Перевірка ребра: O(E), якщо не будувати індекси.
- Ітерація по сусідах вимагає індексації/групування.
// Список ребер: [[u, v, w?]]
const edges = [ [0, 1], [0, 2], [2, 3] ];
// Для швидкого доступу до сусідів - згрупуємо в список суміжності під час завантаження
const adj = new Map();
for (const [u, v] of edges) {
if (!adj.has(u)) adj.set(u, []);
adj.get(u).push(v);
}
console.log(adj.get(0)); // [1, 2]CSR/CSC (Compressed Sparse Row/Column)
Стиснуті структури для розріджених графів (за аналогією зі розрідженими матрицями). CSR зберігає всіх сусідів вершини поспіль в одному масиві та масив зміщень для швидкого доступу.
- Пам'ять: близько до O(V+E), добра локальність у пам'яті.
- Ідеально для обходів і обчислень на великих графах. Оновлення (додавання/видалення) незручні - потрібна перебудова.
// Побудова CSR зі списку ребер
function buildCSR(n, edges, directed = false) {
const tmp = Array.from({ length: n }, () => []);
for (const [u, v] of edges) {
tmp[u].push(v);
if (!directed) tmp[v].push(u);
}
const offsets = new Uint32Array(n + 1);
let total = 0;
for (let i = 0; i < n; i++) {
offsets[i] = total;
total += tmp[i].length;
}
offsets[n] = total;
const neighbors = new Uint32Array(total);
let idx = 0;
for (let i = 0; i < n; i++) {
for (const v of tmp[i]) neighbors[idx++] = v;
}
return { offsets, neighbors };
}
function neighborsCSR(csr, u) {
const { offsets, neighbors } = csr;
return neighbors.subarray(offsets[u], offsets[u + 1]);
}
const csr = buildCSR(5, [[0,1],[0,2],[2,3]]);
console.log([...neighborsCSR(csr, 0)]); // [1, 2]Матриця інцидентності та інші варіанти
- Матриця інцидентності: V×E, корисна для деяких теоретичних/лінійно-алгебраїчних задач. Пам'ять O(V·E) - рідко використовується на практиці для великих графів.
- Об'єктна модель (вузли/ребра як об'єкти з посиланнями): зручно для складних атрибутів, але дорожче за пам'яттю і кешем.
Порівняння за операціями (усереднено)
- Пам'ять: матриця O(V^2) > список суміжності O(V+E) ≈ CSR O(V+E) > список ребер O(E) (без індексів).
- Перевірка ребра u→v: матриця O(1), список суміжності O(deg(u)) або O(1) при Set/Map, CSR O(log deg(u)) при бінарному пошуку (якщо відсортовано) або O(deg(u)).
- Ітерація сусідів: список суміжності/CSR O(deg(u)) - швидше і кеш-дружелюбніше, матриця O(V).
- Оновлення: список суміжності - дешеві; матриця - дешево для ребер, дорого для вершин; CSR - дорого (перебудова).
Як обрати структуру
- Щільний граф (багато ребер) і важлива перевірка наявності ребра: матриця суміжності.
- Розріджений граф, часті обходи й оновлення: список суміжності (Set/Map).
- Дуже великий розріджений граф, мало/жодних оновлень, важлива продуктивність обходу: CSR/CSC.
- Просте зберігання/обмін: список ребер.
Особливості: напрямлені, зважені, мультиграфи
- Напрямлені: зберігайте лише u→v (не дублюйте v→u). У матриці - несиметричні елементи.
- Зважені: у матриці - числа ваг, у списку суміжності - Map<сусід, вага> або масив об'єктів { to, w }.
- Мультиграфи: дозволені кратні ребра. У списку суміжності використовуйте масив (не Set) або зберігайте лічильник/список ребер між парою вершин.
- Петлі (u=u): підтримуються всіма структурами; у матриці - діагональні елементи.
Приклад: типовий граф для веб-задач (обходи BFS/DFS)
// Список суміжності з Set: зручно і швидко для перевірок
const G = new Map();
const dir = false; // неорієнтований
function v(u){ if(!G.has(u)) G.set(u, new Set()); }
function add(u,v){ v(u); v(v); G.get(u).add(v); if(!dir) G.get(v).add(u); }
// Побудуємо невеликий граф
add(0,1); add(0,2); add(1,3); add(2,3); add(3,4);
function bfs(start){
const q = [start], seen = new Set([start]), order = [];
while(q.length){
const u = q.shift(); order.push(u);
for(const v of G.get(u) || []) if(!seen.has(v)){ seen.add(v); q.push(v); }
}
return order;
}
function dfs(start){
const st = [start], seen = new Set(), order = [];
while(st.length){
const u = st.pop();
if(seen.has(u)) continue; seen.add(u); order.push(u);
for(const v of G.get(u) || []) if(!seen.has(v)) st.push(v);
}
return order;
}
console.log('BFS:', bfs(0)); // наприклад, [0,1,2,3,4]
console.log('DFS:', dfs(0));Підсумки
- Немає єдино правильного представлення: обирайте під задачі та обмеження.
- Матриця - для щільних графів і частих перевірок ребра; список суміжності - універсальний і динамічний вибір; список ребер - простий формат даних; CSR/CSC - для великих розріджених графів і високопродуктивних обходів.
Коротка відповідь
Для співбесідиКоротка відповідь допоможе вам впевнено відповідати на цю тему під час співбесіди.