Skip to content
Published on

자료구조 완전 정리 2025: Array부터 Trie, B-Tree까지 — 면접에서 쓰이는 모든 자료구조

Authors

목차

1. Big-O 표기법

1.1 시간 복잡도 개요

알고리즘의 효율성을 입력 크기(n)의 함수로 표현합니다.

O(1)        — 상수 시간     예: 배열 인덱스 접근
O(log n)    — 로그 시간     예: 이진 탐색
O(n)        — 선형 시간     예: 배열 순회
O(n log n)  — 선형 로그     예: 머지 소트
O(n^2)      — 이차 시간     예: 버블 소트
O(2^n)      — 지수 시간     예: 피보나치 재귀
O(n!)       — 팩토리얼      예: 순열 생성

1.2 증가율 비교

n       O(1)  O(log n)  O(n)   O(n log n)  O(n^2)     O(2^n)
10      1     3         10     33          100        1,024
100     1     7         100    664         10,000     1.27e30
1000    1     10        1000   9,966       1,000,000  ---
10000   1     13        10000  132,877     1---

1.3 상각 분석 (Amortized Analysis)

개별 연산은 비쌀 수 있지만, 연속된 연산의 평균 비용이 낮은 경우입니다.

동적 배열 append:
  보통: O(1) — 빈 슬롯에 추가
  가끔: O(n) — 배열 가득 차면 2배 크기로 복사

  n번 append 총 비용: n + n/2 + n/4 + ...2n
  상각 비용: O(2n/n) = O(1)

2. 선형 자료구조

2.1 Array (배열)

연속된 메모리 공간에 같은 타입의 요소를 저장합니다.

인덱스:  0    1    2    3    4
       [10] [20] [30] [40] [50]
        ^                    ^
     arr[0]              arr[4]
연산시간 복잡도
접근 (인덱스)O(1)
탐색O(n)
삽입 (끝)O(1) 상각
삽입 (중간)O(n)
삭제 (끝)O(1)
삭제 (중간)O(n)
# 동적 배열 구현
class DynamicArray:
    def __init__(self):
        self.size = 0
        self.capacity = 1
        self.data = [None] * self.capacity

    def append(self, value):
        if self.size == self.capacity:
            self._resize(2 * self.capacity)
        self.data[self.size] = value
        self.size += 1

    def _resize(self, new_capacity):
        new_data = [None] * new_capacity
        for i in range(self.size):
            new_data[i] = self.data[i]
        self.data = new_data
        self.capacity = new_capacity

    def get(self, index):
        if 0 <= index < self.size:
            return self.data[index]
        raise IndexError("Index out of range")

2.2 Linked List (연결 리스트)

각 노드가 데이터와 다음 노드를 가리키는 포인터를 가집니다.

단일 연결 리스트 (Singly):
  [10|][20|][30|][40|]None

이중 연결 리스트 (Doubly):
  None[|10|][|20|][|30|]None

원형 연결 리스트 (Circular):
  [10|][20|][30|][10|] (처음으로)
연산시간 복잡도
접근 (인덱스)O(n)
탐색O(n)
삽입 (앞)O(1)
삽입 (뒤, tail 포인터)O(1)
삭제 (노드 알 때)O(1)
class ListNode:
    def __init__(self, val=0, next=None):
        self.val = val
        self.next = next

class LinkedList:
    def __init__(self):
        self.head = None

    def prepend(self, val):
        new_node = ListNode(val, self.head)
        self.head = new_node

    def delete(self, val):
        dummy = ListNode(0, self.head)
        prev, curr = dummy, self.head
        while curr:
            if curr.val == val:
                prev.next = curr.next
                break
            prev, curr = curr, curr.next
        self.head = dummy.next

    def reverse(self):
        prev, curr = None, self.head
        while curr:
            nxt = curr.next
            curr.next = prev
            prev = curr
            curr = nxt
        self.head = prev

Array vs Linked List:

기준ArrayLinked List
접근O(1)O(n)
삽입/삭제 (앞)O(n)O(1)
캐시 효율좋음 (연속 메모리)나쁨 (분산 메모리)
메모리 오버헤드낮음포인터 추가 필요

2.3 Stack (스택)

LIFO(Last In, First Out) 구조.

push(10)push(20)push(30)pop()

  |    |   |    |   | 30 |   | 20 |
  |    |   | 20 |   | 20 |   |    |30 반환
  | 10 |   | 10 |   | 10 |   | 10 |
  +----+   +----+   +----+   +----+
  • 활용: 함수 호출 스택, 괄호 매칭, undo/redo, DFS
# 스택 — 파이썬 리스트로 구현
class Stack:
    def __init__(self):
        self.items = []

    def push(self, item):
        self.items.append(item)

    def pop(self):
        if self.is_empty():
            raise IndexError("Stack is empty")
        return self.items.pop()

    def peek(self):
        return self.items[-1] if self.items else None

    def is_empty(self):
        return len(self.items) == 0

2.4 Queue (큐)

FIFO(First In, First Out) 구조.

enqueue(10)enqueue(20)enqueue(30)dequeue()

FrontFront[10]      [10][20]     [10][20][30]       [20][30]10 반환
  • 활용: BFS, 작업 스케줄링, 메시지 큐

2.5 Deque (덱)

양쪽 끝에서 삽입/삭제가 가능한 큐.

앞에서 삽입/삭제 ← [10][20][30] → 뒤에서 삽입/삭제
  • 파이썬의 collections.deque는 이중 연결 리스트 기반

3. 해시 자료구조

3.1 Hash Table (해시 테이블)

키를 해시 함수로 변환하여 버킷에 저장합니다.

key: "apple"hash("apple")3
key: "banana"hash("banana")7
key: "cherry"hash("cherry")3  ← 충돌!

버킷:
[0][1][2][3]  ("apple", 100)  ("cherry", 300)  ← 체이닝
[4][5][6][7]  ("banana", 200)

3.2 충돌 해결 전략

체이닝(Chaining): 같은 버킷에 연결 리스트로 저장

[3]  ("apple", 100)  ("cherry", 300)None

오픈 어드레싱(Open Addressing): 다른 빈 슬롯을 찾아 저장

선형 탐사(Linear Probing):
  hash("cherry") = 3 → 이미 사용 → 4 확인 → 비어있음 → 저장

이차 탐사(Quadratic Probing):
  hash("cherry") = 33+1^2=43+2^2=7...

이중 해싱(Double Hashing):
  hash("cherry") = 33 + hash2("cherry")...

3.3 부하 율(Load Factor)과 리해싱

Load Factor = 저장된 요소 수 / 버킷 수

LF > 0.75리해싱 (버킷 수 2배로 확장, 모든 요소 재배치)
class HashMap:
    def __init__(self, capacity=16):
        self.capacity = capacity
        self.size = 0
        self.buckets = [[] for _ in range(capacity)]
        self.load_factor_threshold = 0.75

    def _hash(self, key):
        return hash(key) % self.capacity

    def put(self, key, value):
        if self.size / self.capacity >= self.load_factor_threshold:
            self._rehash()

        idx = self._hash(key)
        for i, (k, v) in enumerate(self.buckets[idx]):
            if k == key:
                self.buckets[idx][i] = (key, value)
                return
        self.buckets[idx].append((key, value))
        self.size += 1

    def get(self, key):
        idx = self._hash(key)
        for k, v in self.buckets[idx]:
            if k == key:
                return v
        raise KeyError(key)

    def _rehash(self):
        old_buckets = self.buckets
        self.capacity *= 2
        self.buckets = [[] for _ in range(self.capacity)]
        self.size = 0
        for bucket in old_buckets:
            for key, value in bucket:
                self.put(key, value)
연산평균최악
삽입O(1)O(n)
조회O(1)O(n)
삭제O(1)O(n)

4. 트리 자료구조

4.1 Binary Search Tree (이진 탐색 트리)

왼쪽 자식 < 부모 < 오른쪽 자식 규칙을 따릅니다.

        8
       / \
      3   10
     / \    \
    1   6    14
       / \   /
      4   7 13
class TreeNode:
    def __init__(self, val):
        self.val = val
        self.left = None
        self.right = None

class BST:
    def __init__(self):
        self.root = None

    def insert(self, val):
        self.root = self._insert(self.root, val)

    def _insert(self, node, val):
        if not node:
            return TreeNode(val)
        if val < node.val:
            node.left = self._insert(node.left, val)
        elif val > node.val:
            node.right = self._insert(node.right, val)
        return node

    def search(self, val):
        return self._search(self.root, val)

    def _search(self, node, val):
        if not node or node.val == val:
            return node
        if val < node.val:
            return self._search(node.left, val)
        return self._search(node.right, val)

    def inorder(self, node, result=None):
        if result is None:
            result = []
        if node:
            self.inorder(node.left, result)
            result.append(node.val)
            self.inorder(node.right, result)
        return result
연산평균최악 (편향)
탐색O(log n)O(n)
삽입O(log n)O(n)
삭제O(log n)O(n)

4.2 AVL Tree (자가 균형 이진 탐색 트리)

모든 노드의 왼쪽/오른쪽 서브트리 높이 차이가 최대 1인 BST입니다.

불균형 발생 시 회전(Rotation)으로 균형 복원:

LL 회전 (Right Rotation):      RR 회전 (Left Rotation):
    z                              z
   /                                \
  y         →      y                 y       →      y
 /                 / \                \             / \
x                 x   z                x           z   x

LR 회전:                        RL 회전:
    z                              z
   /                                \
  x         →      y                x       →      y
   \               / \             /               / \
    y             x   z           y               z   x
  • 모든 연산 O(log n) 보장
  • 삽입/삭제마다 회전이 필요할 수 있어 약간의 오버헤드

4.3 Red-Black Tree (레드-블랙 트리)

5가지 규칙을 만족하는 자가 균형 BST입니다.

규칙:
1. 모든 노드는 빨강 또는 검정
2. 루트는 검정
3. 모든 리프(NIL)는 검정
4. 빨강 노드의 자식은 모두 검정 (연속 빨강 불가)
5. 임의 노드에서 모든 리프까지 검정 노드 수 동일

        (B:8)
       /      \
    (R:3)    (R:10)
    / \        \
 (B:1)(B:6)  (B:14)
       / \     /
    (R:4)(R:7)(R:13)
  • Java의 TreeMap, C++의 std::map에서 사용
  • AVL보다 삽입/삭제가 빠름 (회전 횟수가 적음)
  • AVL보다 탐색은 약간 느림 (덜 균형적)

4.4 B-Tree / B+Tree

디스크 기반 저장에 최적화된 자가 균형 다진 탐색 트리입니다.

B-Tree (차수 3):
          [10, 20]
         /   |    \
   [3,7]  [12,15] [25,30,40]

B+Tree (차수 3):
          [10, 20]
         /   |    \
   [3,7]  [10,15] [20,25,30]
      ↓ → ↓ → ↓ → ↓ → ↓     ← 리프 노드가 연결 리스트로 연결

B-Tree vs B+Tree:

특성B-TreeB+Tree
데이터 위치모든 노드리프 노드만
리프 연결없음연결 리스트
범위 쿼리비효율적효율적
사용일반 인덱스DB 인덱스 (MySQL InnoDB)

4.5 Segment Tree

구간 쿼리(합, 최솟값, 최댓값 등)를 O(log n)에 처리합니다.

배열: [1, 3, 5, 7, 9, 11]

구간 합 Segment Tree:
              [36]           (0-5)
           /        \
        [9]          [27]     (0-2), (3-5)
       /   \        /    \
     [4]   [5]   [16]   [11]  (0-1),(2),(3-4),(5)
    / \          / \
  [1] [3]     [7] [9]
class SegmentTree:
    def __init__(self, arr):
        self.n = len(arr)
        self.tree = [0] * (4 * self.n)
        self.build(arr, 1, 0, self.n - 1)

    def build(self, arr, node, start, end):
        if start == end:
            self.tree[node] = arr[start]
        else:
            mid = (start + end) // 2
            self.build(arr, 2 * node, start, mid)
            self.build(arr, 2 * node + 1, mid + 1, end)
            self.tree[node] = self.tree[2 * node] + self.tree[2 * node + 1]

    def query(self, node, start, end, l, r):
        if r < start or end < l:
            return 0
        if l <= start and end <= r:
            return self.tree[node]
        mid = (start + end) // 2
        left = self.query(2 * node, start, mid, l, r)
        right = self.query(2 * node + 1, mid + 1, end, l, r)
        return left + right

5. Heap (힙)

5.1 Min Heap / Max Heap

완전 이진 트리에서 부모가 자식보다 작은(Min) 또는 큰(Max) 구조입니다.

Min Heap:                 Max Heap:
      1                        9
     / \                      / \
    3   5                    7   8
   / \ / \                  / \ / \
  7  8 9  10               3  5 4  6

배열로 표현:

인덱스:  0  1  2  3  4  5  6
Min:   [ 1, 3, 5, 7, 8, 9, 10]

부모: (i-1) // 2
왼쪽 자식: 2*i + 1
오른쪽 자식: 2*i + 2

5.2 Heap 연산

class MinHeap:
    def __init__(self):
        self.heap = []

    def push(self, val):
        self.heap.append(val)
        self._sift_up(len(self.heap) - 1)

    def pop(self):
        if not self.heap:
            raise IndexError("Heap is empty")
        self._swap(0, len(self.heap) - 1)
        val = self.heap.pop()
        if self.heap:
            self._sift_down(0)
        return val

    def _sift_up(self, i):
        parent = (i - 1) // 2
        while i > 0 and self.heap[i] < self.heap[parent]:
            self._swap(i, parent)
            i = parent
            parent = (i - 1) // 2

    def _sift_down(self, i):
        n = len(self.heap)
        while True:
            smallest = i
            left = 2 * i + 1
            right = 2 * i + 2
            if left < n and self.heap[left] < self.heap[smallest]:
                smallest = left
            if right < n and self.heap[right] < self.heap[smallest]:
                smallest = right
            if smallest == i:
                break
            self._swap(i, smallest)
            i = smallest

    def _swap(self, i, j):
        self.heap[i], self.heap[j] = self.heap[j], self.heap[i]
연산시간 복잡도
pushO(log n)
pop (최솟값)O(log n)
peekO(1)
heapify (배열 → 힙)O(n)

5.3 Heap Sort

def heap_sort(arr):
    import heapq
    heapq.heapify(arr)
    return [heapq.heappop(arr) for _ in range(len(arr))]
  • 시간 복잡도: O(n log n)
  • 공간 복잡도: O(1) (제자리 정렬 가능)
  • 활용: 우선순위 큐, Top-K 문제, 다익스트라 알고리즘

6. Trie (트라이)

6.1 Trie 구조

문자열 검색에 최적화된 트리 자료구조입니다.

단어: "cat", "car", "card", "dog", "do"

Root
├── c
│   └── a
│       ├── t (*)
│       └── r (*)
│           └── d (*)
└── d
    └── o (*)
        └── g (*)

(*) = 단어의 끝

6.2 Trie 구현

class TrieNode:
    def __init__(self):
        self.children = {}
        self.is_end = False

class Trie:
    def __init__(self):
        self.root = TrieNode()

    def insert(self, word):
        node = self.root
        for char in word:
            if char not in node.children:
                node.children[char] = TrieNode()
            node = node.children[char]
        node.is_end = True

    def search(self, word):
        node = self._find_node(word)
        return node is not None and node.is_end

    def starts_with(self, prefix):
        return self._find_node(prefix) is not None

    def _find_node(self, prefix):
        node = self.root
        for char in prefix:
            if char not in node.children:
                return None
            node = node.children[char]
        return node

    def autocomplete(self, prefix):
        """prefix로 시작하는 모든 단어 반환"""
        node = self._find_node(prefix)
        if not node:
            return []
        results = []
        self._dfs(node, prefix, results)
        return results

    def _dfs(self, node, path, results):
        if node.is_end:
            results.append(path)
        for char, child in node.children.items():
            self._dfs(child, path + char, results)
연산시간 복잡도 (m = 문자열 길이)
삽입O(m)
검색O(m)
접두사 검색O(m)
  • 활용: 자동완성, 맞춤법 검사, IP 라우팅 (Longest Prefix Match), 사전 검색

7. 그래프 (Graph)

7.1 그래프 표현

그래프:
  A --- B
  |   / |
  |  /  |
  C --- D

인접 리스트 (Adjacency List):
  A: [B, C]
  B: [A, C, D]
  C: [A, B, D]
  D: [B, C]

인접 행렬 (Adjacency Matrix):
     A  B  C  D
  A [0, 1, 1, 0]
  B [1, 0, 1, 1]
  C [1, 1, 0, 1]
  D [0, 1, 1, 0]
비교인접 리스트인접 행렬
공간O(V + E)O(V^2)
간선 존재 확인O(degree)O(1)
모든 이웃 순회O(degree)O(V)
적합희소 그래프밀집 그래프

7.2 BFS (너비 우선 탐색)

from collections import deque

def bfs(graph, start):
    visited = set([start])
    queue = deque([start])
    order = []

    while queue:
        node = queue.popleft()
        order.append(node)
        for neighbor in graph[node]:
            if neighbor not in visited:
                visited.add(neighbor)
                queue.append(neighbor)
    return order
BFS 순서 (A에서 시작):
Level 0: A
Level 1: B, C
Level 2: D

: [A][B, C][C, D][D][]

7.3 DFS (깊이 우선 탐색)

def dfs(graph, start, visited=None):
    if visited is None:
        visited = set()
    visited.add(start)
    result = [start]
    for neighbor in graph[start]:
        if neighbor not in visited:
            result.extend(dfs(graph, neighbor, visited))
    return result

7.4 Dijkstra 알고리즘

가중 그래프에서 최단 경로를 찾습니다.

import heapq

def dijkstra(graph, start):
    distances = {node: float('inf') for node in graph}
    distances[start] = 0
    pq = [(0, start)]  # (거리, 노드)

    while pq:
        dist, node = heapq.heappop(pq)
        if dist > distances[node]:
            continue
        for neighbor, weight in graph[node]:
            new_dist = dist + weight
            if new_dist < distances[neighbor]:
                distances[neighbor] = new_dist
                heapq.heappush(pq, (new_dist, neighbor))

    return distances
  • 시간 복잡도: O((V + E) log V) (최소 힙 사용 시)
  • 음수 가중치 없을 때만 사용 가능

7.5 위상 정렬 (Topological Sort)

DAG(방향 비순환 그래프)에서 선행 관계를 유지하는 정렬입니다.

from collections import deque

def topological_sort(graph, in_degree):
    queue = deque([v for v in graph if in_degree[v] == 0])
    result = []

    while queue:
        node = queue.popleft()
        result.append(node)
        for neighbor in graph[node]:
            in_degree[neighbor] -= 1
            if in_degree[neighbor] == 0:
                queue.append(neighbor)

    return result if len(result) == len(graph) else []  # 사이클 감지
  • 활용: 빌드 의존성, 작업 스케줄링, 컴파일 순서

7.6 Bellman-Ford 알고리즘

음수 가중치가 있는 그래프에서도 최단 경로를 찾습니다.

def bellman_ford(edges, num_vertices, start):
    distances = [float('inf')] * num_vertices
    distances[start] = 0

    for _ in range(num_vertices - 1):
        for u, v, w in edges:
            if distances[u] + w < distances[v]:
                distances[v] = distances[u] + w

    # 음수 사이클 감지
    for u, v, w in edges:
        if distances[u] + w < distances[v]:
            return None  # 음수 사이클 존재

    return distances
  • 시간 복잡도: O(V * E)

8. 고급 자료구조

8.1 Union-Find (분리 집합)

처음: 각 노드가 자기 자신을 대표
{0} {1} {2} {3} {4}

union(0, 1): {0, 1} {2} {3} {4}
union(2, 3): {0, 1} {2, 3} {4}
union(0, 3): {0, 1, 2, 3} {4}

find(1) == find(3)?Yes (같은 집합)
find(1) == find(4)?No (다른 집합)
class UnionFind:
    def __init__(self, n):
        self.parent = list(range(n))
        self.rank = [0] * n

    def find(self, x):
        # 경로 압축 (Path Compression)
        if self.parent[x] != x:
            self.parent[x] = self.find(self.parent[x])
        return self.parent[x]

    def union(self, x, y):
        # 랭크 기반 합치기 (Union by Rank)
        px, py = self.find(x), self.find(y)
        if px == py:
            return False
        if self.rank[px] < self.rank[py]:
            px, py = py, px
        self.parent[py] = px
        if self.rank[px] == self.rank[py]:
            self.rank[px] += 1
        return True
  • 경로 압축 + 랭크 기반 합치기: 거의 O(1) (역 아커만 함수)
  • 활용: 크루스칼 MST, 연결 요소 판별, 사이클 감지

8.2 Skip List

정렬된 연결 리스트에 여러 레벨의 인덱스를 추가한 확률적 자료구조입니다.

Level 3: HEAD ────────────────────── 50 ─────── NIL
Level 2: HEAD ──── 20 ──────────── 50 ─── 70 ── NIL
Level 1: HEAD ── 10 ── 20 ── 30 ── 50 ── 70 ── 80 ── NIL
Level 0: HEAD ── 10 ── 20 ── 30 ── 40 ── 50 ── 60 ── 70 ── 80 ── NIL
연산평균최악
탐색O(log n)O(n)
삽입O(log n)O(n)
삭제O(log n)O(n)
  • Redis의 Sorted Set에서 사용
  • 구현이 Red-Black Tree보다 단순

8.3 Bloom Filter

원소가 집합에 속하는지 확률적으로 판단하는 자료구조입니다.

k=3 해시 함수, m=10 비트 배열

insert("apple"):
  h1("apple")=2, h2("apple")=5, h3("apple")=8
  비트 배열: [0,0,1,0,0,1,0,0,1,0]

query("apple"): 위치 2,5,8 모두 1"아마도 있음"
query("grape"): h1=1, h2=5, h3=7 → 위치 10"확실히 없음"
  • False Positive: "있다"고 했지만 실제로 없을 수 있음
  • False Negative: 없음 (확실히 없다는 건 확실)
  • 활용: 캐시 필터, 스팸 필터, DB 존재 확인

8.4 LRU Cache

가장 오래 사용되지 않은 항목을 제거하는 캐시입니다.

from collections import OrderedDict

class LRUCache:
    def __init__(self, capacity):
        self.capacity = capacity
        self.cache = OrderedDict()

    def get(self, key):
        if key not in self.cache:
            return -1
        self.cache.move_to_end(key)
        return self.cache[key]

    def put(self, key, value):
        if key in self.cache:
            self.cache.move_to_end(key)
        self.cache[key] = value
        if len(self.cache) > self.capacity:
            self.cache.popitem(last=False)
  • HashMap + Doubly Linked List로 O(1) get/put 구현
  • 활용: 브라우저 캐시, 데이터베이스 버퍼, CDN

9. 자료구조 시간 복잡도 비교표

자료구조접근탐색삽입삭제공간
ArrayO(1)O(n)O(n)O(n)O(n)
Dynamic ArrayO(1)O(n)O(1)*O(n)O(n)
Linked ListO(n)O(n)O(1)O(1)O(n)
StackO(n)O(n)O(1)O(1)O(n)
QueueO(n)O(n)O(1)O(1)O(n)
Hash Table-O(1)*O(1)*O(1)*O(n)
BST-O(log n)*O(log n)*O(log n)*O(n)
AVL Tree-O(log n)O(log n)O(log n)O(n)
Red-Black Tree-O(log n)O(log n)O(log n)O(n)
B-Tree-O(log n)O(log n)O(log n)O(n)
Heap-O(n)O(log n)O(log n)O(n)
Trie-O(m)O(m)O(m)O(n*m)
Skip List-O(log n)*O(log n)*O(log n)*O(n log n)

* = 평균 시간. 최악의 경우 다를 수 있음. m = 문자열 길이.


10. 문제별 자료구조 선택 가이드

10.1 의사결정 트리

빠른 삽입/삭제가 중요? → 해시 테이블 또는 연결 리스트
정렬된 순서 필요?BST 계열 (AVL, Red-Black)
최솟값/최댓값 빠르게? → 힙
문자열 접두사 검색? → 트라이
범위 쿼리? → 세그먼트 트리 또는 B-Tree
집합 소속 빠르게? → 해시셋 또는 블룸 필터
연결 요소 관리?Union-Find
최단 경로? → 그래프 + Dijkstra/BFS
FIFO? → 큐
LIFO? → 스택

10.2 실전 활용 맵

문제 유형추천 자료구조이유
캐시 구현HashMap + DLL (LRU)O(1) get/put
자동완성Trie접두사 검색 O(m)
실시간 순위표Skip List 또는 Balanced BST정렬 + 빠른 갱신
작업 스케줄러Priority Queue (Heap)우선순위 기반 추출
소셜 네트워크Graph (인접 리스트)관계 탐색
DB 인덱스B+Tree범위 쿼리 + 디스크 최적화
중복 판별HashSet 또는 Bloom FilterO(1) 판별
구간 합/최솟값Segment TreeO(log n) 쿼리
네트워크 연결성Union-Find거의 O(1) 합치기/찾기
괄호 매칭StackLIFO 패턴
너비 우선 탐색Queue + Graph레벨 순 탐색

11. 면접 질문 15선

Q1. 배열과 연결 리스트의 차이를 설명하고 각각 언제 사용하나요?

배열은 연속 메모리 할당으로 인덱스 접근이 O(1)이지만 중간 삽입/삭제가 O(n)입니다. 연결 리스트는 노드가 포인터로 연결되어 삽입/삭제가 O(1)이지만 인덱스 접근이 O(n)입니다. 배열은 임의 접근이 잦고 크기가 예측 가능할 때, 연결 리스트는 빈번한 삽입/삭제와 동적 크기가 필요할 때 사용합니다.

Q2. 해시 테이블의 충돌 해결 방법을 설명하세요.

체이닝(Chaining)은 같은 버킷에 연결 리스트로 여러 요소를 저장합니다. 구현이 간단하고 부하율이 높아도 성능 저하가 점진적입니다. 오픈 어드레싱(Open Addressing)은 충돌 시 다른 빈 슬롯을 찾습니다. 선형 탐사, 이차 탐사, 이중 해싱 등이 있습니다. 캐시 효율이 좋지만 부하율이 높으면 성능이 급격히 저하됩니다.

Q3. BST에서 삭제 연산을 설명하세요.

세 가지 경우가 있습니다: (1) 리프 노드: 단순 삭제. (2) 자식 1개: 자식으로 대체. (3) 자식 2개: 중위 후속자(오른쪽 서브트리의 최솟값) 또는 중위 선행자(왼쪽 서브트리의 최댓값)로 대체하고 해당 노드를 삭제합니다.

Q4. Heap과 BST의 차이를 설명하세요.

Heap은 부모가 자식보다 크거나(Max) 작은(Min) 완전 이진 트리로, 최솟값/최댓값 추출이 O(1)이고 삽입/삭제가 O(log n)입니다. BST는 왼쪽 자식 < 부모 < 오른쪽 자식 규칙으로, 정렬된 순서 탐색이 가능하고 특정 값 검색이 O(log n)입니다. Heap은 우선순위 큐에, BST는 정렬된 데이터 관리에 적합합니다.

Q5. Trie의 시간/공간 복잡도와 HashMap 기반 구현의 장단점을 설명하세요.

Trie는 문자열 길이 m에 대해 삽입/검색이 O(m)입니다. 공간은 최악 O(n*m)이지만 공통 접두사를 공유하여 실제로는 적습니다. HashMap 기반 구현(각 노드의 children을 HashMap으로)은 알파벳 크기에 유연하고 메모리 효율적이지만, 배열 기반보다 상수 시간이 큽니다.

Q6. BFS와 DFS의 차이와 각각의 활용 사례를 말해주세요.

BFS는 큐를 사용하여 레벨 순으로 탐색하고, 최단 경로(가중치 없는 그래프)를 찾을 때 적합합니다. DFS는 스택(또는 재귀)을 사용하여 깊이 우선으로 탐색하고, 경로 탐색, 사이클 감지, 위상 정렬에 적합합니다. BFS는 O(V+E) 시간, O(V) 공간을 사용합니다.

Q7. Red-Black Tree와 AVL Tree의 차이를 설명하세요.

둘 다 자가 균형 BST입니다. AVL은 더 엄격한 균형(높이 차이 1 이하)으로 탐색이 빠르지만 삽입/삭제 시 회전이 많습니다. Red-Black은 덜 엄격한 균형으로 삽입/삭제가 빠르지만 탐색은 약간 느립니다. 읽기 중심이면 AVL, 쓰기 중심이면 Red-Black이 유리합니다.

Q8. LRU Cache를 O(1)에 구현하는 방법을 설명하세요.

HashMap과 이중 연결 리스트를 조합합니다. HashMap은 키 → 노드 참조를 저장하고, 이중 연결 리스트는 사용 순서를 관리합니다. get은 HashMap으로 O(1) 조회 후 노드를 리스트 끝으로 이동. put은 새 노드를 끝에 추가하고, 용량 초과 시 리스트 앞의 노드를 제거합니다.

Q9. Union-Find에서 경로 압축과 랭크 기반 합치기를 설명하세요.

경로 압축(Path Compression)은 find 시 경로의 모든 노드를 루트에 직접 연결하여 이후 find를 O(1)에 가깝게 합니다. 랭크 기반 합치기(Union by Rank)는 항상 작은 트리를 큰 트리 아래에 붙여 트리 높이를 최소화합니다. 두 최적화를 함께 사용하면 거의 O(alpha(n))으로 상수 시간에 가깝습니다.

Q10. B-Tree가 데이터베이스 인덱스에 적합한 이유를 설명하세요.

B-Tree는 차수가 높아 트리 높이가 낮습니다. 디스크 I/O는 블록 단위로 이루어지므로, 한 노드에 여러 키를 저장하는 B-Tree는 I/O 횟수를 최소화합니다. B+Tree는 모든 데이터가 리프에 있고 리프가 연결 리스트로 연결되어 범위 쿼리가 효율적입니다.

Q11. Bloom Filter는 무엇이고 어디에 사용하나요?

Bloom Filter는 원소의 집합 소속 여부를 확률적으로 판단하는 자료구조입니다. False Positive(있다고 했지만 실제로 없음)는 가능하지만, False Negative(없다고 했지만 실제로 있음)는 불가능합니다. 공간 효율적이며 캐시 필터, 스팸 URL 검사, DB의 불필요한 디스크 접근 방지 등에 사용됩니다.

Q12. Dijkstra와 Bellman-Ford 알고리즘의 차이를 설명하세요.

Dijkstra는 음수 가중치 없는 그래프에서 최단 경로를 찾으며, 우선순위 큐를 사용하여 O((V+E)log V)입니다. Bellman-Ford는 음수 가중치도 처리 가능하고 음수 사이클도 감지하지만 O(V*E)로 느립니다. 음수 가중치가 없으면 Dijkstra, 있으면 Bellman-Ford를 사용합니다.

Q13. Stack으로 Queue를 구현하는 방법을 설명하세요.

두 개의 스택을 사용합니다. push 스택에 enqueue하고, dequeue 시 pop 스택이 비어있으면 push 스택의 모든 요소를 pop 스택으로 옮깁니다. 이렇게 하면 상각 O(1)에 enqueue/dequeue가 가능합니다.

Q14. 해시 테이블의 Load Factor가 성능에 미치는 영향을 설명하세요.

Load Factor(부하율)는 저장된 요소 수를 버킷 수로 나눈 값입니다. 부하율이 높아지면 충돌 확률이 증가하여 체이닝은 연결 리스트가 길어지고, 오픈 어드레싱은 클러스터링이 심해집니다. 일반적으로 0.75를 임계값으로 설정하고, 초과하면 리해싱(버킷 수 확장)을 수행합니다.

Q15. 그래프에서 사이클을 감지하는 방법을 설명하세요.

무방향 그래프: Union-Find로 간선 추가 시 같은 집합이면 사이클, 또는 DFS에서 부모가 아닌 방문 노드를 만나면 사이클. 방향 그래프: DFS에서 현재 경로(재귀 스택)에 있는 노드를 다시 만나면 사이클(back edge 감지). 위상 정렬에서 모든 노드를 방문하지 못하면 사이클이 있습니다.


12. 퀴즈 5선

퀴즈 1. 크기 n인 정렬된 배열에서 이진 탐색의 최대 비교 횟수는?

floor(log2(n)) + 1번입니다. 이진 탐색은 매번 탐색 범위를 절반으로 줄이므로 O(log n)입니다. 예를 들어 n=1000이면 최대 10번의 비교로 찾을 수 있습니다.

퀴즈 2. Min Heap에서 최댓값을 찾는 시간 복잡도는?

**O(n)**입니다. Min Heap에서 최댓값은 반드시 리프 노드에 있고, 리프 노드는 약 n/2개이므로 모두 확인해야 합니다. Heap은 최솟값만 O(1)에 접근 가능합니다.

퀴즈 3. 해시 테이블에서 모든 키를 정렬된 순서로 출력하려면?

**O(n log n)**이 필요합니다. 해시 테이블은 정렬을 유지하지 않으므로, 모든 키를 추출한 후 정렬해야 합니다. 정렬된 순서가 자주 필요하면 TreeMap(균형 BST 기반)을 사용하는 것이 좋습니다.

퀴즈 4. n개 노드의 완전 이진 트리 높이는?

**floor(log2(n))**입니다. 완전 이진 트리의 레벨 k에는 최대 2^k개 노드가 있고, n개 노드를 담으려면 약 log2(n) 레벨이 필요합니다.

퀴즈 5. Trie에서 "abc"를 삭제할 때 "ab"도 단어라면 어떻게 되나요?

"abc"의 끝 표시(is_end)만 false로 바꾸고, 'c' 노드에 자식이 없으면 'c' 노드를 삭제합니다. 'b' 노드는 "ab"의 끝이므로 삭제하지 않습니다. 삭제는 리프에서부터 자식이 없고 단어 끝이 아닌 노드만 재귀적으로 제거합니다.


13. 참고 자료

도서

  1. Thomas H. Cormen et al., "Introduction to Algorithms (CLRS)" (4th ed., 2022)
  2. Robert Sedgewick, Kevin Wayne, "Algorithms" (4th ed., 2011)
  3. Steven S. Skiena, "The Algorithm Design Manual" (3rd ed., 2020)

온라인 학습

  1. LeetCode: https://leetcode.com/
  2. NeetCode 로드맵: https://neetcode.io/
  3. Visualgo — 자료구조 시각화: https://visualgo.net/
  4. Big-O Cheat Sheet: https://www.bigocheatsheet.com/

강의

  1. MIT 6.006 Introduction to Algorithms (OpenCourseWare)
  2. Stanford CS 161 Design and Analysis of Algorithms
  3. UC Berkeley CS 61B Data Structures

코딩 면접 준비

  1. Gayle Laakmann McDowell, "Cracking the Coding Interview" (6th ed.)
  2. Alex Xu, "System Design Interview" Vol. 1 & 2
  3. Aditya Bhargava, "Grokking Algorithms" (2nd ed., 2024)

웹 자료

  1. GeeksforGeeks Data Structures: https://www.geeksforgeeks.org/data-structures/
  2. CP-Algorithms: https://cp-algorithms.com/
  3. Tech Interview Handbook: https://www.techinterviewhandbook.org/