JUNSEOK
01 · 수학과 자료구조·22분·4개 레슨

연결 리스트·트리·그래프

DOM, Virtual DOM, 번들러 의존성 그래프의 뿌리.

노드-포인터 자료구조의 세계. DOM 트리, Virtual DOM, 컴포넌트 트리, 번들러 의존성 그래프 — FE의 중심 구조가 전부 여기 있다.


0. "노드와 포인터"가 뭔데? — 초심자용

0-1. 앞장까지의 자료구조와 뭐가 다른가

1-41-5의 자료구조는 대부분 한 줄로 쭉 늘어놓은 형태였다. 배열은 연속된 칸, 스택/큐도 일렬로 쌓거나 줄 세우는 구조다.

이 장에서 다루는 것들은 "옆으로 가지를 치는" 구조다.

  • 연결 리스트: 각 데이터가 "다음은 저기야"라고 손가락질(포인터)로 이어짐
  • 트리: 한 데이터가 여러 자식을 가짐 (가족 관계도)
  • 그래프: 자식·부모 구분 없이 자유롭게 연결 (지하철 노선도)

0-2. "노드"와 "포인터"

  • 노드(Node): 값 하나를 담고 있는 상자
  • 포인터(Pointer): 다른 노드를 가리키는 화살표 (JS에서는 그냥 객체 참조)
[A] → [B] → [C]

A 상자는 "값 A"와 "다음은 B" 정보를 갖고 있다. 데이터 자체가 구조를 만든다 — 메모리 어디에 있든, 화살표만 따라가면 순회할 수 있다.

0-3. 프론트엔드와 직접 맞닿은 예시

  • DOM 트리: <html> 아래 <body> 아래 <div>... 전형적인 트리
  • 컴포넌트 트리: React에서 <App> 아래 <Layout> 아래 <Button>
  • Virtual DOM diff: 두 트리를 비교해서 달라진 곳만 찾기
  • 번들러 의존성: a.jsb.js를 import하고, b.jsc.js를 import... 그래프
  • 라우팅 경로: URL 경로 매칭이 트리 구조

이 장의 내용은 바로 다음 주 코드에서 만난다. 추상적인 개념이 아니다.

0-4. 왜 "복잡"해 보이나

배열 [1, 2, 3]은 머릿속에서 바로 그려진다. 하지만 "A 노드가 B를 가리키고, B는 C와 D를, D는 다시 A를..." 하면 순식간에 복잡해진다. 그래서 이 장은 코드를 보기 전에 그림을 그리는 연습이 필요하다.


1. 연결 리스트 (Linked List)

1-1. 구조

각 노드가 "값 + 다음 노드 참조"를 들고 있다.

[A|*] → [B|*] → [C|*] → null

1-2. 배열 vs 연결 리스트

배열연결 리스트
랜덤 접근O(1)O(n)
중간 삽입/삭제O(n)O(1) (포인터 있을 때)
메모리연속, 캐시 친화흩어짐, 포인터 오버헤드
크기 변경재할당 필요자유로움

1-3. ⚠️ 함정: 이론적 O(1) vs 실제 성능

연결 리스트의 "중간 삽입 O(1)"은 이미 그 위치의 포인터를 들고 있을 때만 성립. 검색이 필요하면 O(n)이다.

또한 현대 CPU는 캐시 지역성 때문에 연속 메모리를 훨씬 빠르게 읽는다. 순회만 필요하면 배열이 대부분 더 빠르다 (3단계에서 심화).

"빅오가 같으면 상수가 작은 쪽이 이긴다".

1-4. 단일 연결 리스트 구현

class ListNode {
  constructor(val, next = null) { this.val = val; this.next = next; }
}

class LinkedList {
  constructor() { this.head = null; this.size = 0; }

  prepend(val) {
    this.head = new ListNode(val, this.head);
    this.size++;
  }

  append(val) {
    const node = new ListNode(val);
    if (!this.head) this.head = node;
    else {
      let cur = this.head;
      while (cur.next) cur = cur.next;
      cur.next = node;
    }
    this.size++;
  }

  delete(val) {
    if (!this.head) return;
    if (this.head.val === val) { this.head = this.head.next; this.size--; return; }
    let cur = this.head;
    while (cur.next && cur.next.val !== val) cur = cur.next;
    if (cur.next) { cur.next = cur.next.next; this.size--; }
  }
}

1-5. 대표 문제: 뒤집기

3개 포인터(prev, cur, next)로 순회하며 방향을 뒤집는다.

function reverse(head) {
  let prev = null, cur = head;
  while (cur) {
    const next = cur.next;
    cur.next = prev;
    prev = cur;
    cur = next;
  }
  return prev;
}

이 패턴은 수많은 연결 리스트 문제의 기본.

1-6. 이중 연결 리스트와 원형

  • 이중: 이전 노드 포인터도 유지 → 양방향 순회, 중간 삭제 O(1)
  • 원형: 꼬리가 머리를 가리킴 → 스케줄링, 원형 버퍼

2. 이진 트리와 BST

2-1. 용어

  • 루트: 최상위 노드
  • 리프: 자식 없는 노드
  • 깊이(depth): 루트에서의 거리
  • 높이(height): 가장 먼 리프까지의 거리
  • 균형: 모든 리프의 깊이가 비슷함

2-2. 순회 4가지

        A
       / \
      B   C
     / \   \
    D   E   F
  • 전위 (Pre-order): 루트 → 왼 → 오 → A B D E C F
  • 중위 (In-order): 왼 → 루트 → 오 → D B E A C F
  • 후위 (Post-order): 왼 → 오 → 루트 → D E B F C A
  • 레벨 (BFS): A B C D E F

2-3. BST (Binary Search Tree)

왼쪽 서브트리의 모든 값 < 루트 < 오른쪽 서브트리의 모든 값.

BST의 중위 순회 결과는 항상 오름차순.

class BSTNode {
  constructor(val) { this.val = val; this.left = null; this.right = null; }
}

class BST {
  constructor() { this.root = null; }

  insert(val) {
    this.root = this._insert(this.root, val);
  }
  _insert(node, val) {
    if (!node) return new BSTNode(val);
    if (val < node.val) node.left = this._insert(node.left, val);
    else if (val > node.val) node.right = this._insert(node.right, val);
    return node;
  }

  contains(val) {
    let cur = this.root;
    while (cur) {
      if (val === cur.val) return true;
      cur = val < cur.val ? cur.left : cur.right;
    }
    return false;
  }
}

2-4. 복잡도

  • 균형 잡힌 BST: 탐색/삽입/삭제 O(log n)
  • 최악 (정렬된 데이터를 순차 삽입): O(n) — 선형 리스트가 됨

균형 트리 (AVL, Red-Black): 삽입/삭제 시 자동으로 회전하여 균형 유지. 이름과 용도만 알면 된다.


3. 트라이 (Trie, Prefix Tree)

3-1. 용도

문자열 집합에 대한 접두사 검색을 O(m)에 (m = 검색 문자열 길이). 배열에서 매번 순회하면 O(n×m).

3-2. 구조

               (root)
              /  |  \
             a   b   c
             |   |   |
             p   a   a
             |   |   |
             p   t   t
             |   |
             l   *
             |
             e
             *

각 노드에 "이 노드에서 끝나는 단어인가" 플래그.

3-3. 구현

class TrieNode {
  constructor() { this.children = new Map(); this.isEnd = false; }
}

class Trie {
  constructor() { this.root = new TrieNode(); }

  insert(word) {
    let node = this.root;
    for (const ch of word) {
      if (!node.children.has(ch)) node.children.set(ch, new TrieNode());
      node = node.children.get(ch);
    }
    node.isEnd = true;
  }

  search(word) {
    const node = this._traverse(word);
    return node !== null && node.isEnd;
  }

  startsWith(prefix) {
    return this._traverse(prefix) !== null;
  }

  _traverse(str) {
    let node = this.root;
    for (const ch of str) {
      if (!node.children.has(ch)) return null;
      node = node.children.get(ch);
    }
    return node;
  }

  autocomplete(prefix, limit = 10) {
    const node = this._traverse(prefix);
    if (!node) return [];
    const result = [];
    const dfs = (cur, path) => {
      if (result.length >= limit) return;
      if (cur.isEnd) result.push(prefix + path);
      for (const [ch, next] of cur.children) dfs(next, path + ch);
    };
    dfs(node, '');
    return result;
  }
}

3-4. 복잡도

  • 삽입/검색: O(m)
  • 공간: 알파벳 수 × 총 문자 수 — 매우 크다

3-5. 실전: 주소 자동완성, 태그 필터

검색어 입력할 때마다 Trie에서 접두사 매칭. "서울"만 쳐도 시군구 후보 전체를 O(m)에 제공.


4. Union-Find (Disjoint Set)

4-1. 용도

"두 원소가 같은 집합에 있는가?"를 빠르게 판정.

  • find(x): x가 속한 집합의 대표 원소
  • union(x, y): 두 집합을 합침

4-2. 기본 구현

class UnionFind {
  constructor(n) {
    this.parent = Array.from({ length: n }, (_, i) => i);
    this.rank = new Array(n).fill(0);
  }

  find(x) {
    if (this.parent[x] !== x) {
      this.parent[x] = this.find(this.parent[x]);  // 경로 압축
    }
    return this.parent[x];
  }

  union(x, y) {
    const px = this.find(x), py = this.find(y);
    if (px === py) return false;
    // union by rank: 낮은 트리를 높은 트리 밑에
    if (this.rank[px] < this.rank[py]) this.parent[px] = py;
    else if (this.rank[px] > this.rank[py]) this.parent[py] = px;
    else { this.parent[py] = px; this.rank[px]++; }
    return true;
  }
}

4-3. 최적화 두 가지

  • 경로 압축 (Path Compression): find 시 중간 노드들을 직접 루트에 붙임
  • Union by Rank: 낮은 트리를 높은 트리 밑에 붙여 깊이 증가 방지

두 최적화 다 적용하면 아커만 역함수 α(n) ≈ 상수. 실질적으로 O(1).

4-4. 실전

  • MST (Kruskal): 간선을 가중치 순으로 정렬 후, 사이클을 만들지 않는 것만 선택
  • 그래프 연결성: 동적으로 간선이 추가될 때 "같은 컴포넌트인가?" 판정
  • 이미지 영역 감지: 같은 색 픽셀을 합치기

5. 그래프 표현

5-1. 인접 행렬 (Adjacency Matrix)

  A B C D
A 0 1 1 0
B 1 0 0 1
C 1 0 0 1
D 0 1 1 0
  • 공간: O(V²)
  • 간선 존재 확인: O(1)
  • 모든 이웃 탐색: O(V)
  • 간선이 조밀한(dense) 그래프에 유리

5-2. 인접 리스트 (Adjacency List)

A → [B, C]
B → [A, D]
C → [A, D]
D → [B, C]
const graph = {
  A: ['B', 'C'],
  B: ['A', 'D'],
  C: ['A', 'D'],
  D: ['B', 'C'],
};
// 또는 Map
const graph2 = new Map([['A', ['B','C']], ...]);
  • 공간: O(V + E)
  • 간선 존재 확인: O(deg(v))
  • 모든 이웃 탐색: O(deg(v))
  • 희소(sparse) 그래프에 유리 — 대부분의 실무 그래프

5-3. 가중 그래프

const graph = {
  A: [{ to: 'B', w: 3 }, { to: 'C', w: 1 }],
  ...
};

6. DFS와 BFS

한 방향 끝까지 → 되돌아오며 다른 방향. 스택 또는 재귀.

function dfs(start, graph) {
  const visited = new Set();
  function visit(node) {
    if (visited.has(node)) return;
    visited.add(node);
    console.log(node);
    for (const next of graph[node] ?? []) visit(next);
  }
  visit(start);
}

용도:

  • 백트래킹 (N-Queens, 스도쿠, 순열)
  • 위상 정렬
  • 연결 컴포넌트 세기
  • 사이클 감지

같은 거리 전부 방문 → 다음 거리. 큐.

function bfs(start, graph) {
  const visited = new Set([start]);
  const queue = [start];
  while (queue.length) {
    const node = queue.shift();   // ⚠️ 큰 그래프면 head 포인터 큐를
    console.log(node);
    for (const next of graph[node] ?? []) {
      if (!visited.has(next)) {
        visited.add(next);
        queue.push(next);
      }
    }
  }
}

용도:

  • 가중치 없는 그래프의 최단 경로
  • 레벨별 순회
  • 웹 크롤러
  • 소셜 네트워크의 "친구의 친구"

6-3. DFS vs BFS

DFSBFS
자료구조스택(재귀)
메모리O(h) (높이)O(w) (너비)
특성깊이 먼저거리 먼저
최단 경로XO (가중치 1일 때)

7. FE 실전 연결

7-1. DOM 트리

document 자체가 트리. querySelector, children, parentNode 순회는 그래프 순회다.

// BFS로 모든 자손 순회
function bfsDOM(root) {
  const queue = [root];
  while (queue.length) {
    const el = queue.shift();
    console.log(el.tagName);
    for (const child of el.children) queue.push(child);
  }
}

7-2. Virtual DOM diff

React가 두 가상 트리를 비교할 때 DFS로 순회. 같은 위치의 노드를 비교하며 key 기반으로 매칭.

7-3. 컴포넌트 트리

React 컴포넌트는 트리 구조. context 전파는 상위→하위 DFS 전파와 유사.

7-4. 번들러 의존성 그래프

import A from './a' 문들이 DAG(Directed Acyclic Graph)를 형성. 번들러는 이 그래프를 위상 정렬해 출력 순서를 결정 (1-7 챕터).


연습 문제

  1. 단일 연결 리스트를 뒤집는 reverse(head)를 재귀와 반복 양쪽으로 구현하라.
  2. 두 정렬된 연결 리스트를 병합하는 merge(a, b)를 구현하라.
  3. 이진 트리의 모든 순회(전위/중위/후위/레벨)를 구현하라.
  4. 트라이로 자동완성 기능을 구현하고 ["apple", "app", "apply", "application"]을 넣은 뒤 "app" 검색 결과를 구하라.
  5. Union-Find로 n 노드 그래프에서 간선 [[0,1],[1,2],[3,4]]를 추가한 뒤 연결 컴포넌트 수를 구하라.
  6. 인접 리스트로 표현된 그래프에서 BFS로 최단 거리 배열을 반환하는 함수를 작성하라.
  7. DOM 트리에서 특정 class를 가진 가장 가까운 조상 노드를 찾는 closest(el, cls)를 순회로 직접 구현하라.

연습 문제 정답

1. 뒤집기

// 반복
function reverseIter(head) {
  let prev = null, cur = head;
  while (cur) { const next = cur.next; cur.next = prev; prev = cur; cur = next; }
  return prev;
}

// 재귀
function reverseRec(head) {
  if (!head || !head.next) return head;
  const newHead = reverseRec(head.next);
  head.next.next = head;
  head.next = null;
  return newHead;
}

2. 병합

function merge(a, b) {
  const dummy = new ListNode(0);
  let tail = dummy;
  while (a && b) {
    if (a.val <= b.val) { tail.next = a; a = a.next; }
    else { tail.next = b; b = b.next; }
    tail = tail.next;
  }
  tail.next = a || b;
  return dummy.next;
}

3. 순회 4종

const preOrder = (n, r=[]) => { if (!n) return r; r.push(n.val); preOrder(n.left,r); preOrder(n.right,r); return r; };
const inOrder  = (n, r=[]) => { if (!n) return r; inOrder(n.left,r); r.push(n.val); inOrder(n.right,r); return r; };
const postOrder= (n, r=[]) => { if (!n) return r; postOrder(n.left,r); postOrder(n.right,r); r.push(n.val); return r; };
function levelOrder(root) {
  if (!root) return [];
  const r = [], q = [root];
  while (q.length) {
    const n = q.shift();
    r.push(n.val);
    if (n.left) q.push(n.left);
    if (n.right) q.push(n.right);
  }
  return r;
}

4. Trie 자동완성

const t = new Trie();
["apple", "app", "apply", "application"].forEach(w => t.insert(w));
t.autocomplete("app");  // ["app", "apple", "apply", "application"]

5. 연결 컴포넌트

const uf = new UnionFind(5);
uf.union(0, 1);
uf.union(1, 2);
uf.union(3, 4);
const roots = new Set();
for (let i = 0; i < 5; i++) roots.add(uf.find(i));
roots.size;  // 2 (그룹 {0,1,2}와 {3,4})

6. BFS 최단 거리

function bfsDistance(start, graph) {
  const dist = new Map([[start, 0]]);
  const queue = [start];
  while (queue.length) {
    const node = queue.shift();
    for (const next of graph[node] ?? []) {
      if (!dist.has(next)) {
        dist.set(next, dist.get(node) + 1);
        queue.push(next);
      }
    }
  }
  return dist;
}

7. closest

function closest(el, cls) {
  let cur = el;
  while (cur && cur !== document) {
    if (cur.classList?.contains(cls)) return cur;
    cur = cur.parentNode;
  }
  return null;
}

조상 체인을 타고 올라가며 클래스 확인. 이것이 Element.closest()의 실체다.


체크리스트

  • 연결 리스트와 배열의 복잡도 트레이드오프를 안다
  • 이론적 O(1)과 실제 성능의 차이 (캐시 지역성)를 안다
  • 연결 리스트 뒤집기를 3개 포인터로 구현할 수 있다
  • 이진 트리 순회 4종 (pre/in/post/level)을 구분할 수 있다
  • BST의 탐색/삽입/삭제 복잡도와 최악 조건을 안다
  • 트라이의 시간·공간 복잡도와 활용 예시를 안다
  • Union-Find의 두 최적화 (경로 압축, union by rank)를 안다
  • 인접 행렬 vs 인접 리스트의 선택 기준을 안다
  • DFS와 BFS의 자료구조 차이와 용도를 구분할 수 있다
  • BFS가 가중치 없는 최단 경로를 찾는 이유를 설명할 수 있다
  • DOM/Virtual DOM/컴포넌트/번들러 의존성이 모두 그래프임을 안다

이전: 1-5. 스택·큐·힙 | 다음: 1-7. 핵심 알고리즘

진도 체크시작 전
NEXT · 1-7

핵심 알고리즘

정렬, 이분 탐색, DP, 그리디, 위상 정렬, 최단 경로.

이어서 학습하기 →