연결 리스트 (Linked List)
정의
연결 리스트 (Linked List) 는 노드 + 포인터로 구성된 선형 자료구조. 각 노드는 데이터 + 다음 노드 포인터.
종류:
- Singly Linked List: 노드가 next 포인터 1개. 한 방향 순회.
- Doubly Linked List: 노드가 prev, next 포인터 2개. 양방향 순회.
- Circular Linked List: 마지막 노드의 next 가 head. 순환.
C++ STL std::list 는 doubly linked list, Java LinkedList 도 doubly. Python 표준 라이브러리에는 linked list 없음 (대신 collections.deque 나 직접 구현).
문제 상황과 동기
배열은 랜덤 접근 O(1), 삽입/삭제 O(N) (shift). 연결 리스트는 랜덤 접근 O(N), 삽입/삭제 O(1) (포인터 조작만).
- naive (배열): 중간 삽입 시 뒤 원소를 모두 shift. O(N).
- linked list: 중간 삽입 시 포인터 2개만 바꾸면 됨. O(1) (삽입 위치를 이미 알 때).
핵심 통찰: 메모리는 불연속, 포인터로 순서 유지. 캐시 locality 는 나쁘지만, 삽입/삭제가 잦으면 유리.
시각화
핵심 아이디어
Singly Linked List
struct Node {
int data;
Node* next;
};
invariant: head 에서 시작, next 를 따라가면 모든 노드 방문. 끝 노드의 next 는 nullptr.
Doubly Linked List
struct Node {
int data;
Node* prev;
Node* next;
};
양방향 순회 가능. 삭제 시 prev 만 알아도 이전 노드에 접근 가능.
연산
-
삽입 (insertion):
- 삽입 위치 노드
cur를 찾는다 (O(N) 순회). - 새 노드
new를 만든다. new->next = cur->next,cur->next = new. (singly)- doubly 는
new->prev = cur,new->next = cur->next,cur->next->prev = new,cur->next = new. - 포인터 조작만 O(1), 위치 찾는 게 O(N).
- 삽입 위치 노드
-
삭제 (deletion):
- 삭제할 노드
del을 찾는다 (O(N) 순회). - singly: 이전 노드
prev를 찾아prev->next = del->next. (이전 노드 찾기 O(N)). - doubly:
del->prev->next = del->next,del->next->prev = del->prev. 이전 노드 찾기 불필요. - 노드 free. 포인터 조작 O(1).
- 삭제할 노드
-
탐색 (search): head 에서 순회. O(N).
캐시 locality: 배열은 연속 메모리, 캐시 친화. 연결 리스트는 노드가 흩어져 있음, 캐시 miss 많음. 큰 N 에서 배열이 더 빠를 수 있다.
알고리즘
Singly Linked List 삽입 (head 에 삽입)
insert_front(head, data):
new = new Node(data)
new->next = head
head = new
return head
Doubly Linked List 삭제 (노드 del 삭제)
delete_node(del):
if del->prev:
del->prev->next = del->next
if del->next:
del->next->prev = del->prev
free(del)
순회 (singly)
traverse(head):
cur = head
while cur != nullptr:
process(cur->data)
cur = cur->next
구현
// Singly Linked List, head 에 삽입 후 순회
#include <bits/stdc++.h>
using namespace std;
struct Node {
int data;
Node* next;
Node(int d) : data(d), next(nullptr) {}
};
int main() {
int n; cin >> n;
Node* head = nullptr;
for (int i = 0; i < n; i++) {
int x; cin >> x;
Node* newNode = new Node(x);
newNode->next = head;
head = newNode;
}
// 순회
Node* cur = head;
while (cur) {
cout << cur->data << " ";
cur = cur->next;
}
}5
1
2
3
4
55 4 3 2 1복잡도
| 항목 | Singly | Doubly | 배열 |
|---|---|---|---|
| 랜덤 접근 (at) | O(N) | O(N) | O(1) |
| head 삽입/삭제 | O(1) | O(1) | O(N) |
| tail 삽입 (tail 포인터 있으면) | O(1) | O(1) | O(1) (amortized, vector) |
| 중간 삽입/삭제 (위치 알 때) | O(1) | O(1) | O(N) |
| 탐색 (search) | O(N) | O(N) | O(N) (정렬 안 됨), O(log N) (정렬 + binary search) |
| 공간 | O(N) + 포인터 | O(N) + 2*포인터 | O(N) |
변형 / 활용
| 응용 | 설명 |
|---|---|
| LRU Cache | doubly linked list + hash map. 최근 사용 노드를 head 로 이동. |
| Skip List | 다단계 포인터. 이진 탐색 비슷. 평균 O(log N) 탐색. |
| XOR Linked List | 메모리 절약. prev XOR next 로 1 포인터. 거의 안 쓰임. |
| Josephus Problem | 원형 연결 리스트. 매 K 번째 제거. |
| Polynomial | 계수 + 지수를 노드로 저장. 다항식 연산. |
함정
1. 메모리 누수 (memory leak)
C++ 에서 delete 안 하면 메모리 누수. Python / Java 는 GC.
2. 이전 노드 찾기 (singly)
singly linked list 에서 노드 삭제 시, 이전 노드를 찾아야 한다. O(N) 순회. doubly 는 prev 포인터로 O(1).
3. 포인터 null 체크
cur->next 를 쓰기 전에 cur != nullptr 체크. segfault 조심.
4. 순환 리스트 무한 루프
circular linked list 에서 종료 조건 잘못 쓰면 무한 루프. head 로 돌아왔는지 체크.
5. Cache locality
배열보다 캐시 miss 많다. 큰 N + 순회 많으면 배열이 더 빠를 수 있다. Linked list 는 삽입/삭제가 지배적일 때만 유리.
BOJ 연습 문제
| 번호 | 제목 | 정답률 | 링크 |
|---|---|---|---|
| BOJ 1406 | 에디터 (doubly linked list) | - | kokoa-lab |
| BOJ 5397 | 키로거 (doubly linked list) | - | kokoa-lab |
| BOJ 1158 | 요세푸스 문제 (circular list) | - | kokoa-lab |
| BOJ 23309 | 철도 공사 (doubly linked list) | - | kokoa-lab |
참고
이 글의 용어 (3개)
- [Redis] Cache Patterns: Cache-Aside, Stampede, Evictiondatabase-internals
- 정의 Cache Pattern 은 Redis (또는 다른 캐시) 를 어떤 경로로 읽고 쓰는지 의 디자인 결정. 같은 인프라로도 패턴 선택에 따라 일관성 / 지연 / 부하 가 완전…
- 덱 (Deque)algorithm
- 정의 Deque (덱, double-ended queue) 는 양쪽 끝에서 O(1) push/pop 이 가능한 선형 자료구조. 큐 + 스택의 일반화. 내부 구현은 대개 chunk…
- Skip List: 확률적 O(log N) 정렬 구조algorithm
- 정의 Skip List 는 정렬된 연결 리스트를 여러 층으로 쌓아 확률적으로 O(log N) 탐색을 제공합니다. 각 노드는 확률 1/2 로 더 높은 층에 승격됩니다. 상위 층은 …
💬 댓글