0Pricing
DSA Interview Prep · 강의

그래프 표현과 순회 설정

인접 리스트로 방향 그래프와 무방향 그래프를 만들고, deque로 BFS를 초기화하며, 스택이나 재귀로 DFS를 설정하고 방문 여부를 관리합니다.

그래프 표현과 순회 설정은(는) CoddyKit의 무료 DSA Interview Prep 강의입니다. 이것은 4개 중 1번째 강의입니다. 아래에서 전체 강의를 무료로 읽을 수 있으며, 내장 코드 에디터와 24/7 AI 튜터와 함께 브라우저에서 직접 실습할 수 있습니다. 이 강의는 DSA Interview Prep 학습 경로의 일부이며, 진행 상황이 웹과 CoddyKit 앱에 동기화됩니다. DSA Interview Prep 강의에는 총 4개의 강의가 포함되어 있습니다.

그래프란 무엇인가

그래프는 간선으로 연결된 노드(정점)의 모음입니다. 트리와 달리 그래프에는 순환, 노드 사이의 여러 경로, 연결되지 않은 구성 요소가 있을 수 있습니다. 그래프는 소셜 네트워크, 도로 지도, 의존성 트리, 웹 페이지 링크와 같은 실제 시스템을 모델링합니다. 거의 모든 복잡한 시스템 설계 및 알고리즘 면접에서 그래프가 등장하므로, 그래프의 표현과 순회를 익히는 것이 필수적입니다.

# 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')

인접 리스트 표현

인접 리스트는 각 노드의 이웃 노드 목록을 저장합니다. 파이썬에서는 각 노드를 인접 노드 목록에 매핑하는 dict를 사용합니다. 이는 면접 문제에서 가장 흔한 표현입니다. 공간 복잡도는 O(V + E)(희소 그래프에 효율적)이고, 이웃 노드를 순회하는 데 O(차수), 해시 집합 변형을 사용하면 인접 여부 확인에 평균 O(1)이 걸립니다. 대부분의 LeetCode 그래프 문제는 이 형식을 사용합니다.

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

인접 행렬 표현

인접 행렬은 간선이 i에서 j로 연결되어 있으면 matrix[i][j] = 1(또는 간선 가중치)을 저장하고, 그렇지 않으면 0을 저장하는 V×V 2차원 배열입니다. 간선 조회에 O(1)을 제공하지만 간선 수와 관계없이 O(V²) 공간을 사용하므로 희소 그래프에서는 비효율적입니다. 그래프가 밀집되어 있거나(간선이 많은 경우) 플로이드-워셜 모든 쌍 최단 경로처럼 간선 존재 여부를 빠르게 확인하는 것이 중요할 때 선호됩니다.

# 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

간선 리스트 표현

간선 리스트는 가장 단순한 표현으로, 출발지와 도착지 튜플의 목록이며 필요에 따라 가중치를 포함할 수 있습니다. O(E) 공간을 사용하고 모든 간선을 순회하기 쉽습니다. 하지만 특정 노드의 이웃을 찾으려면 모든 간선을 스캔해야 하므로 O(E)이 걸립니다. 간선 리스트는 벨만-포드(모든 간선을 n-1번 완화)나 크루스칼 최소 신장 트리 알고리즘처럼 모든 간선을 정확히 순회하는 그래프 알고리즘에서 사용됩니다.

# 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)

BFS 설정: 큐와 방문 집합

BFS(너비 우선 탐색)는 큐를 사용해 그래프를 레벨별로 탐색합니다. 핵심 요소는 순환 그래프에서 노드를 다시 방문하지 않도록 하는 방문 집합입니다. 방문 집합이 없으면 순환 그래프에서 BFS가 영원히 반복됩니다. 표준 설정은 다음과 같습니다. 시작 노드로 큐를 초기화하고 해당 노드를 방문한 것으로 표시한 다음, 반복해서 큐에서 꺼내 처리하고 방문하지 않은 이웃 노드를 큐에 넣습니다.

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]

DFS 설정: 스택 또는 재귀

DFS(깊이 우선 탐색)는 각 가지를 따라 가능한 한 멀리 탐색한 후 되돌아옵니다. 재귀적으로(호출 스택 사용) 구현하거나 반복적으로(명시적 스택 사용) 구현할 수 있습니다. 두 방식 모두 순환 그래프에서는 방문 집합이 필요합니다. 반복 방식에서는 재귀 DFS의 탐색 순서와 맞추기 위해 이웃 노드를 역순으로 넣지만, 두 구현의 탐색 순서는 서로 다를 수 있습니다.

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))

BFS와 DFS를 사용하는 경우

비가중치 그래프에서 최단 경로(간선 수가 가장 적은 경로)가 필요하거나 노드를 레벨별로 처리해야 할 때는 BFS를 선택합니다. 도달 가능한 모든 노드를 탐색하거나, 순환을 감지하거나, 연결 요소를 찾거나, 위상 정렬을 수행하거나, 모든 경로를 열거해야 할 때는 DFS를 선택합니다. 실전에서는 ‘최단/최소 이동 횟수’에는 BFS를, ‘존재 여부/도달 가능성/열거’에는 DFS를 사용합니다.

# 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')

LeetCode 입력 형식에서 그래프 만들기

LeetCode 그래프 문제는 다양한 입력 형식으로 주어집니다. 간선 리스트: [[0,1],[0,2]] — 인접 리스트를 만듭니다. 인접 리스트 인덱스 기반 형식: graph[i]는 i의 이웃 목록입니다. 격자/행렬: 셀이 노드이고 인접한 셀(위/아래/왼쪽/오른쪽)이 이웃인 m×n 2차원 배열입니다. 자식이 있는 노드: Node(val, neighbors)와 같은 사용자 정의 클래스입니다. 이러한 형식을 파악하고 첫 단계로 인접 리스트로 변환하십시오.

# 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))

격자에서 방문 표시하기

격자 문제에서 방문한 셀을 추적하는 방법은 두 가지입니다. 방법 A: (row, col) 튜플로 이루어진 별도의 visited 집합을 사용합니다. 추가 공간은 O(m*n)입니다. 방법 B: 격자를 제자리에서 수정하여 방문한 셀에 특수 표시 값(예: '#' 또는 2)을 기록하고, 필요한 경우 나중에 원래대로 복원합니다. 제자리 수정 방식은 추가 공간 O(1)을 사용하며 영역 채우기와 섬 개수 세기 문제에서 자주 사용됩니다.

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

여러 시작점으로 BFS 초기화하기

다중 출발점 BFS는 방문한 것으로 표시한 모든 출발 노드를 큐에 넣어 동시에 여러 노드에서 시작합니다. 이는 ‘가장 가까운 0까지의 거리’, ‘썩어 가는 오렌지’, ‘벽과 문’처럼 출발 노드 중 어느 노드에서든 가장 짧은 거리를 구해야 하는 문제에 사용됩니다. 다중 출발점 BFS의 시간 복잡도는 O(V + E)로 단일 출발점 방식과 같습니다. 각 노드를 여전히 최대 한 번만 방문하기 때문입니다.

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

그래프 밀도와 표현 선택

인접 리스트와 행렬 중 어떤 것을 선택할지는 그래프의 밀도인 E/V²의 비율에 따라 달라집니다. 희소 그래프(E << V²)는 인접 리스트가 유리합니다. 공간이 행렬의 O(V²)보다 작은 O(V+E)이기 때문입니다. 밀집 그래프(E ≈ V²)는 인접 행렬이 유리합니다. 간선 조회가 리스트의 O(차수)보다 빠른 O(1)이기 때문입니다. 면접 문제에서는 대부분 희소 그래프를 다루므로 인접 리스트가 거의 항상 올바른 선택입니다.

# 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')

빠른 확인

이 수업의 자료 구조 및 알고리즘 — 코딩 면접 준비 개념에 대한 이해도를 테스트해 보십시오.

학습 내용 요약

이 수업에서는 세 가지 그래프 표현(인접 리스트, 행렬, 간선 리스트)과 각각을 선택하는 경우, 순환 그래프에서 무한 반복을 방지하기 위한 방문 집합을 사용한 BFS 및 DFS 설정, 그리고 제자리 격자 표시와 다중 출발점 BFS 같은 실전 패턴을 배웠습니다. 다음으로 BFS를 적용해 최단 경로와 레벨 순회를 찾습니다.

자주 묻는 질문

“그래프 표현과 순회 설정” 강의는 무료인가요?

네 — “그래프 표현과 순회 설정” 전체 내용을 이 웹사이트에서 무료로 읽을 수 있습니다. 인터랙티브하게 실습하려면(내장 코드 에디터와 24/7 AI 튜터), CoddyKit PRO로 업그레이드하면 DSA Interview Prep 강의 전체를 잠금 해제할 수 있습니다. DSA Interview Prep 강의에는 총 4개의 강의가 포함되어 있습니다.

“그래프 표현과 순회 설정”에서 뭘 배우나요?

인접 리스트로 방향 그래프와 무방향 그래프를 만들고, deque로 BFS를 초기화하며, 스택이나 재귀로 DFS를 설정하고 방문 여부를 관리합니다. 브라우저에서 직접 실행하는 실습 코드로 DSA Interview Prep을(를) 배우며, 24/7 AI 튜터가 강의를 진행하면서 질문에 답변해줍니다.

DSA Interview Prep을(를) 시작하는 데 경험이 필요한가요?

사전 경험은 필요하지 않습니다. CoddyKit의 DSA Interview Prep은(는) 초급자부터 고급 학습자까지를 위해 구성되어 있으므로, 여기서 시작하거나 처음부터 시작할 수 있으며 자신의 속도대로 진행할 수 있습니다. 이것은 4개 중 1번째 강의입니다.

“그래프 표현과 순회 설정” 강의는 얼마나 걸리나요?

대부분의 CoddyKit 강의는 약 5~10분이 소요됩니다. 각 강의는 간결하고 인터랙티브하여 꾸준한 진행이 가능하며, 웹과 앱에서 중단한 부분부터 바로 시작할 수 있습니다.

이 DSA Interview Prep 강의에서 코드를 작성하고 실행할 수 있나요?

네. 모든 DSA Interview Prep 강의에는 내장 코드 에디터가 포함되어 있으므로, 브라우저에서 바로 실제 코드를 작성하고 실행한 후 즉시 AI 피드백을 받을 수 있습니다 — 로컬 설정이 필요 없습니다.

이 강의의 모든 강의

  1. 그래프 표현과 순회 설정
  2. BFS: 최단 경로와 레벨 순회
  3. DFS: 연결 요소와 플러드 필
  4. 방향 그래프와 무방향 그래프의 순환 탐지
← DSA Interview Prep(으)로 돌아가기