0Pricing
Coding Interview Prep · Lekcja

Reprezentacje grafów i przygotowanie przejść

Zbudują Państwo grafy skierowane i nieskierowane za pomocą list sąsiedztwa, zainicjalizują BFS z deque oraz DFS ze stosem lub rekurencją, obsługując śledzenie odwiedzonych węzłów.

Reprezentacje grafów i przygotowanie przejść to bezpłatna lekcja Coding Interview Prep na CoddyKit. To lekcja 1 z 4. Możesz przeczytać całą lekcję poniżej za darmo — a potem ćwiczyć ją interaktywnie w przeglądarce z wbudowanym edytorem kodu i tutorem AI dostępnym 24/7. To część ścieżki edukacyjnej Coding Interview Prep, a Twój postęp synchronizuje się między webem a aplikacją CoddyKit. Kurs Coding Interview Prep zawiera 4 lekcji w sumie.

Czym jest graf?

Graf to zbiór węzłów (wierzchołków) połączonych przez krawędzie. W przeciwieństwie do drzew grafy mogą zawierać cykle, wiele ścieżek między węzłami oraz niespójne składowe. Grafy służą do modelowania rzeczywistych systemów, takich jak sieci społecznościowe, mapy dróg, drzewa zależności i odnośniki między stronami internetowymi. Niemal każda nietrywialna rozmowa kwalifikacyjna dotycząca projektowania systemów i algorytmów obejmuje grafy — opanowanie ich reprezentacji i przeszukiwania jest niezbędne.

# Graph terminology:
# - V: set of vertices (nodes)
# - E: set of edges
# - Directed graph: edges have direction (A -> B but not B -> A)
# - Undirected graph: edges are bidirectional
# - Weighted graph: edges have costs/weights
# - Cyclic: contains at least one cycle
# - Acyclic: no cycles (DAG = Directed Acyclic Graph)
# - Connected: every node reachable from every other
# - Disconnected: multiple isolated components
print('Graph: nodes + edges, directed/undirected, weighted/unweighted')

Reprezentacja za pomocą listy sąsiedztwa

Lista sąsiedztwa przechowuje listę sąsiadów każdego wierzchołka. W Pythonie należy użyć obiektu dict mapującego każdy wierzchołek na listę sąsiednich wierzchołków. Jest to najczęściej używana reprezentacja w zadaniach rekrutacyjnych: zajmuje O(V + E) pamięci (jest wydajna w przypadku grafów rzadkich), umożliwia iterowanie po sąsiadach w czasie O(stopnia) oraz sprawdzanie sąsiedztwa w średnim czasie O(1) w wariancie opartym na zbiorze haszującym. Większość zadań grafowych na LeetCode korzysta z tego formatu.

from collections import defaultdict

# Build an undirected graph
def build_undirected(edges):
    graph = defaultdict(list)
    for u, v in edges:
        graph[u].append(v)
        graph[v].append(u)  # both directions
    return graph

edges = [(0,1), (0,2), (1,3), (2,3), (3,4)]
graph = build_undirected(edges)
print(dict(graph))
# {0:[1,2], 1:[0,3], 2:[0,3], 3:[1,2,4], 4:[3]}

# Directed graph: only one direction
def build_directed(edges):
    graph = defaultdict(list)
    for u, v in edges:
        graph[u].append(v)  # only u -> v
    return graph

Reprezentacja za pomocą macierzy sąsiedztwa

Macierz sąsiedztwa to dwuwymiarowa tablica V×V, w której matrix[i][j] = 1 (lub waga krawędzi), jeśli istnieje krawędź z wierzchołka i do wierzchołka j, a w przeciwnym razie znajduje się 0. Zapewnia sprawdzanie istnienia krawędzi w czasie O(1), ale zużywa O(V²) pamięci niezależnie od liczby krawędzi — jest to nieefektywne w przypadku grafów rzadkich. Preferuje się ją, gdy graf jest gęsty (ma wiele krawędzi) lub gdy kluczowe jest szybkie sprawdzanie istnienia krawędzi, na przykład w algorytmie Floyda-Warshalla do znajdowania najkrótszych ścieżek między wszystkimi parami wierzchołków.

# Adjacency matrix for 5 nodes
V = 5
matrix = [[0] * V for _ in range(V)]

edges = [(0,1), (0,2), (1,3), (2,3), (3,4)]
for u, v in edges:
    matrix[u][v] = 1
    matrix[v][u] = 1  # undirected

# Print the matrix:
for row in matrix:
    print(row)
# Neighbour check: O(1)
print('Edge 0-2:', bool(matrix[0][2]))  # True
print('Edge 0-4:', bool(matrix[0][4]))  # False

# Space: O(V^2) vs adjacency list O(V+E)
# Dense graph: matrix often better; sparse: list better

Reprezentacja za pomocą listy krawędzi

Lista krawędzi to najprostsza reprezentacja: zwykła lista krotek (źródło, cel), opcjonalnie zawierających wagi. Zajmuje O(E) pamięci i umożliwia łatwe iterowanie po wszystkich krawędziach. Znalezienie sąsiadów danego wierzchołka wymaga jednak przeszukania wszystkich krawędzi, czyli O(E). Listy krawędzi są używane w algorytmach grafowych, które dokładnie iterują po wszystkich krawędziach, takich jak Bellman-Ford (relaksacja wszystkich krawędzi n-1 razy) oraz algorytm Kruskala do znajdowania minimalnego drzewa rozpinającego.

# Weighted edge list: (source, destination, weight)
edge_list = [
    (0, 1, 4),
    (0, 2, 1),
    (1, 3, 1),
    (2, 3, 5),
    (3, 4, 3)
]

# Useful for:
# Bellman-Ford: iterate all edges n-1 times
# Kruskal's MST: sort by weight then union-find

# Sort by weight for Kruskal:
edge_list_sorted = sorted(edge_list, key=lambda e: e[2])
print('Sorted by weight:', edge_list_sorted)

# Finding neighbours: O(E) scan -- inefficient for traversal
node_0_neighbors = [v for u, v, w in edge_list if u == 0]
print('Node 0 neighbors:', node_0_neighbors)

Konfiguracja BFS: kolejka i zbiór odwiedzonych

BFS (Breadth-First Search) przeszukuje graf poziomami, korzystając z kolejki. Kluczowym elementem jest zbiór odwiedzonych, który zapobiega ponownemu odwiedzaniu wierzchołków w grafach zawierających cykle. Bez zbioru odwiedzonych BFS w grafie cyklicznym zapętliłby się na zawsze. Standardowa konfiguracja polega na zainicjalizowaniu kolejki wierzchołkiem źródłowym, oznaczeniu go jako odwiedzonego, a następnie wielokrotnym usuwaniu elementu z kolejki, przetwarzaniu go i dodawaniu do kolejki nieodwiedzonych sąsiadów.

from collections import deque

def bfs(graph, start):
    visited = {start}        # mark source as visited
    queue = deque([start])   # initialise queue
    order = []
    while queue:
        node = queue.popleft()
        order.append(node)
        for neighbour in graph[node]:
            if neighbour not in visited:
                visited.add(neighbour)     # mark BEFORE enqueue
                queue.append(neighbour)
    return order

from collections import defaultdict
graph = defaultdict(list)
for u, v in [(0,1),(0,2),(1,3),(2,3),(3,4)]:
    graph[u].append(v); graph[v].append(u)

print(bfs(graph, 0))  # [0, 1, 2, 3, 4]

Konfiguracja DFS: stos lub rekurencja

DFS (Depth-First Search) eksploruje każdą gałąź tak głęboko, jak to możliwe, a następnie cofa się. Można go zaimplementować rekurencyjnie (z użyciem stosu wywołań) lub iteracyjnie (z użyciem jawnego stosu). W obu przypadkach w grafach cyklicznych potrzebny jest zbiór odwiedzonych. Wersja iteracyjna dodaje sąsiadów w odwrotnej kolejności, aby odwzorować kolejność przechodzenia rekurencyjnego DFS, choć kolejność eksploracji może się różnić w obu implementacjach.

def dfs_recursive(graph, node, visited=None, order=None):
    if visited is None: visited = set(); order = []
    visited.add(node)
    order.append(node)
    for neighbour in graph[node]:
        if neighbour not in visited:
            dfs_recursive(graph, neighbour, visited, order)
    return order

def dfs_iterative(graph, start):
    visited = set()
    stack = [start]
    order = []
    while stack:
        node = stack.pop()
        if node in visited: continue
        visited.add(node)
        order.append(node)
        for neighbour in reversed(graph[node]):  # reverse for same order as recursive
            if neighbour not in visited:
                stack.append(neighbour)
    return order

print('Recursive DFS:', dfs_recursive(graph, 0))
print('Iterative DFS:', dfs_iterative(graph, 0))

Kiedy używać BFS, a kiedy DFS

Należy wybrać BFS, gdy potrzebna jest najkrótsza ścieżka (najmniejsza liczba krawędzi) w grafie nieważonym lub gdy wierzchołki trzeba przetwarzać poziomami. DFS należy wybrać, gdy trzeba odwiedzić wszystkie osiągalne wierzchołki, wykrywać cykle, znajdować składowe spójne, wykonywać sortowanie topologiczne lub wyliczać wszystkie ścieżki. W praktyce: BFS służy do problemów typu „najkrótsza ścieżka/minimalna liczba przeskoków”, a DFS do problemów typu „istnienie/osiągalność/wyliczanie”.

# BFS use cases:
# - Shortest path in unweighted graph (fewest edges)
# - Level-order traversal
# - Word ladder (minimum transformations)
# - Clone graph

# DFS use cases:
# - Connected components (flood fill)
# - Cycle detection
# - Topological sort
# - All paths between two nodes
# - Maze solving (any path)
# - N-queens, Sudoku (backtracking)

# Both: O(V + E) time, O(V) space for visited
print('BFS: shortest hops | DFS: existence and enumeration')

Graf w formatach danych wejściowych LeetCode

Zadania grafowe na LeetCode mogą korzystać z różnych formatów danych wejściowych. Lista krawędzi: [[0,1],[0,2]] — należy zbudować listę sąsiedztwa. Lista sąsiedztwa oparta na indeksach: graph[i] to lista sąsiadów wierzchołka i. Siatka/macierz: dwuwymiarowa tablica m×n, w której komórki są wierzchołkami, a sąsiednie komórki (w górę, w dół, w lewo i w prawo) są sąsiadami. Wierzchołek z dziećmi: niestandardowe klasy, takie jak Node(val, neighbors). Należy rozpoznawać te formaty i w pierwszym kroku konwertować je na listę sąsiedztwa.

# Format 1: edge list -> adjacency list
def edges_to_adj(n, edges):
    graph = [[] for _ in range(n)]
    for u, v in edges:
        graph[u].append(v)
        graph[v].append(u)
    return graph

# Format 2: 2D grid -> adjacency (implicit)
# Neighbours of (r, c): (r-1,c), (r+1,c), (r,c-1), (r,c+1)
DIRS = [(-1,0),(1,0),(0,-1),(0,1)]
def grid_neighbours(grid, r, c):
    rows, cols = len(grid), len(grid[0])
    return [(r+dr, c+dc) for dr, dc in DIRS
            if 0 <= r+dr < rows and 0 <= c+dc < cols]

grid = [[1,1,0],[0,1,1],[1,0,0]]
print('Neighbours of (0,0):', grid_neighbours(grid, 0, 0))
print('Neighbours of (1,1):', grid_neighbours(grid, 1, 1))

Oznaczanie odwiedzonych pól w siatkach

W zadaniach dotyczących siatek istnieją dwa sposoby śledzenia odwiedzonych komórek. Opcja A: użycie osobnego zbioru visited krotek (row, col) — O(m*n) dodatkowej pamięci. Opcja B: modyfikowanie siatki w miejscu przez oznaczanie odwiedzonych komórek wartością specjalną (np. '#' lub 2) i przywrócenie ich później, jeśli jest to potrzebne. Podejście modyfikujące dane w miejscu zużywa O(1) dodatkowej pamięci i jest często stosowane w zadaniach typu flood fill oraz Number of Islands.

def num_islands(grid):
    if not grid:
        return 0
    rows, cols = len(grid), len(grid[0])
    count = 0

    def dfs(r, c):
        if r < 0 or r >= rows or c < 0 or c >= cols:
            return
        if grid[r][c] != '1':
            return
        grid[r][c] = '#'  # mark as visited (in-place)
        dfs(r+1, c); dfs(r-1, c)
        dfs(r, c+1); dfs(r, c-1)

    for r in range(rows):
        for c in range(cols):
            if grid[r][c] == '1':
                dfs(r, c)
                count += 1
    return count

grid = [['1','1','0','0'],
        ['1','1','0','0'],
        ['0','0','1','0'],
        ['0','0','0','1']]
print(num_islands(grid))  # 3

Inicjalizacja BFS z wieloma źródłami

BFS z wieloma źródłami rozpoczyna się jednocześnie z wielu wierzchołków: kolejkę inicjalizuje się wszystkimi wierzchołkami źródłowymi oznaczonymi jako odwiedzone. Stosuje się go w zadaniach takich jak „odległość od najbliższego zera”, „gnijące pomarańcze” i „ściany i bramy”, gdy potrzebna jest najkrótsza odległość od dowolnego wierzchołka źródłowego. BFS z wieloma źródłami działa w czasie O(V + E), czyli tak samo jak BFS z jednym źródłem, ponieważ każdy wierzchołek jest nadal odwiedzany najwyżej raz.

from collections import deque

def rotting_oranges(grid):
    rows, cols = len(grid), len(grid[0])
    queue = deque()
    fresh = 0
    # Multi-source: all rotten oranges start at time=0
    for r in range(rows):
        for c in range(cols):
            if grid[r][c] == 2:
                queue.append((r, c, 0))  # (row, col, time)
            elif grid[r][c] == 1:
                fresh += 1
    dirs = [(0,1),(0,-1),(1,0),(-1,0)]
    time = 0
    while queue:
        r, c, t = queue.popleft()
        for dr, dc in dirs:
            nr, nc = r+dr, c+dc
            if 0<=nr<rows and 0<=nc<cols and grid[nr][nc]==1:
                grid[nr][nc] = 2  # mark rotten
                fresh -= 1
                queue.append((nr, nc, t+1))
                time = t + 1
    return time if fresh == 0 else -1

print(rotting_oranges([[2,1,1],[1,1,0],[0,1,1]]))  # 4

Gęstość grafu a wybór reprezentacji

Wybór między listą sąsiedztwa a macierzą zależy od gęstości grafu — stosunku E/V². Graf rzadki (E << V²) zyskuje na zastosowaniu list sąsiedztwa: zajmują one O(V+E) pamięci w porównaniu z O(V²) w przypadku macierzy. Graf gęsty (E ≈ V²) zyskuje na zastosowaniu macierzy sąsiedztwa: sprawdzanie krawędzi zajmuje O(1), w porównaniu z O(stopnia) dla list. W zadaniach rekrutacyjnych listy sąsiedztwa są niemal zawsze właściwym wyborem, ponieważ większość zadań dotyczy grafów rzadkich.

# Graph density comparison:
# Sparse: social network (V=1B users, avg 200 friends)
#   E = 200 * 1B = 200B << V^2 = 10^18 -> adjacency list
# Dense: complete graph (every node connected to every other)
#   E = V*(V-1)/2 ≈ V^2 -> adjacency matrix

# Interview rule of thumb:
# - Default to adjacency list (defaultdict(list))
# - Use matrix only when asked about dense graph or O(1) edge lookup
# - Grid problems: use implicit adjacency (4-directional neighbours)

print('Sparse graph (E << V^2): use adjacency list')
print('Dense graph (E ~ V^2): consider adjacency matrix')

Szybki test

Sprawdź swoje zrozumienie zagadnień z Data Structures & Algorithms — Coding Interview Prep omówionych w tej lekcji.

Podsumowanie lekcji

W tej lekcji nauczyli się Państwo: trzech reprezentacji grafów (listy sąsiedztwa, macierzy i listy krawędzi) oraz tego, kiedy wybrać każdą z nich, konfiguracji BFS i DFS ze zbiorami odwiedzonych zapobiegającymi nieskończonym pętlom w grafach cyklicznych, a także praktycznych wzorców, takich jak oznaczanie komórek siatki w miejscu i BFS z wieloma źródłami. Następnie zastosujemy BFS do znajdowania najkrótszych ścieżek i przechodzenia grafu poziomami.

Często zadawane pytania

Czy lekcja „Reprezentacje grafów i przygotowanie przejść” jest bezpłatna?

Tak — pełny tekst „Reprezentacje grafów i przygotowanie przejść” jest dostępny za darmo tutaj w sieci. Aby ćwiczyć ją interaktywnie (wbudowany edytor kodu i tutor AI dostępny 24/7) i odblokować resztę kursu Coding Interview Prep, przejdź na CoddyKit PRO. Kurs Coding Interview Prep zawiera 4 lekcji w sumie.

Co nauczysz się w „Reprezentacje grafów i przygotowanie przejść”?

Zbudują Państwo grafy skierowane i nieskierowane za pomocą list sąsiedztwa, zainicjalizują BFS z deque oraz DFS ze stosem lub rekurencją, obsługując śledzenie odwiedzonych węzłów. Ćwiczysz Coding Interview Prep z praktycznym kodem, który uruchamiasz bezpośrednio w przeglądarce, a tutor AI dostępny 24/7 odpowiada na Twoje pytania podczas pracy nad lekcją.

Czy potrzebuję doświadczenia, aby zacząć Coding Interview Prep?

Nie wymagamy żadnego doświadczenia. Coding Interview Prep w CoddyKit jest strukturyzowany dla początkujących i zaawansowanych użytkowników, więc możesz zacząć tutaj lub od początku i uczyć się w swoim tempie. To lekcja 1 z 4.

Ile czasu zajmuje lekcja „Reprezentacje grafów i przygotowanie przejść”?

Większość lekcji CoddyKit trwa około 5–10 minut. Każda lekcja to mały, interaktywny krok, dzięki czemu robisz systematyczne postępy i zawsze wracasz dokładnie do tego samego miejsca — na webie i w aplikacji.

Czy mogę pisać i uruchamiać kod w tej lekcji Coding Interview Prep?

Tak. Każda lekcja Coding Interview Prep zawiera wbudowany edytor kodu, więc piszesz i uruchamiasz prawdziwy kod bezpośrednio w przeglądarce i od razu otrzymujesz sprzężenie zwrotne od AI — bez konfiguracji na komputerze.

Wszystkie lekcje w tym kursie

  1. Reprezentacje grafów i przygotowanie przejść
  2. BFS: najkrótsza ścieżka i przejście poziomami
  3. DFS: spójne składowe i flood fill
  4. Wykrywanie cykli w grafach skierowanych i nieskierowanych
← Powrót do Coding Interview Prep