지식
Algorithm
단일 연결 리스트
노드와 포인터로 구성된 단일 연결 리스트의 구조, 삽입·삭제·역전 알고리즘을 Python으로 직접 구현합니다.
지난 글에서 동적 배열을 구현해봤습니다. 배열은 인덱스 접근이 빠르지만 중간 삽입·삭제가 느립니다. 단일 연결 리스트(Singly Linked List)는 이 약점을 보완하는 대표적인 자료구조입니다.
연결 리스트란
연결 리스트는 노드(Node)들이 포인터로 연결된 자료구조입니다. 각 노드는 데이터와 다음 노드를 가리키는 포인터를 가집니다. 배열과 달리 메모리가 연속적이지 않아도 됩니다.
class Node:
def __init__(self, data):
self.data = data
self.next = None # 다음 노드 포인터
class SinglyLinkedList:
def __init__(self):
self.head = None # 첫 번째 노드
self.tail = None # 마지막 노드 (선택적)
self._size = 0
기본 연산 구현
앞에 삽입 (prepend) — O(1)
def prepend(self, data):
new_node = Node(data)
new_node.next = self.head # 새 노드 → 기존 head
self.head = new_node # head 갱신
if self.tail is None:
self.tail = new_node
self._size += 1
뒤에 삽입 (append) — O(1) with tail pointer
def append(self, data):
new_node = Node(data)
if self.tail is None:
self.head = self.tail = new_node
else:
self.tail.next = new_node # 기존 tail → 새 노드
self.tail = new_node # tail 갱신
self._size += 1
tail 포인터를 유지하지 않으면 뒤 삽입이 O(n)이 됩니다. 반드시 tail을 관리하세요.
앞 삭제 (delete head) — O(1)
def delete_head(self):
if self.head is None:
raise IndexError("빈 리스트")
data = self.head.data
self.head = self.head.next # head를 한 칸 앞으로
if self.head is None:
self.tail = None
self._size -= 1
return data
뒤 삭제 — O(n): 단일 연결 리스트의 약점
def delete_tail(self):
if self.head is None:
raise IndexError("빈 리스트")
if self.head is self.tail:
data = self.head.data
self.head = self.tail = None
self._size -= 1
return data
# tail 이전 노드를 찾기 위해 전체 순회 필요 — O(n)
curr = self.head
while curr.next is not self.tail:
curr = curr.next
data = self.tail.data
curr.next = None
self.tail = curr
self._size -= 1
return data
이것이 단일 연결 리스트의 핵심 단점입니다. 이전(prev) 포인터가 없어서 tail 삭제가 O(n)입니다.
리스트 역전 — O(n)
def reverse(self):
prev, curr = None, self.head
self.tail = self.head # 현재 head가 역전 후 tail
while curr:
curr.next, prev, curr = prev, curr, curr.next
self.head = prev
포인터만 변경하므로 데이터 복사 없이 O(n)에 역전됩니다.
전체 구현
class SinglyLinkedList:
def __init__(self):
self.head = self.tail = None
self._size = 0
def __len__(self):
return self._size
def __iter__(self):
curr = self.head
while curr:
yield curr.data
curr = curr.next
def search(self, target): # O(n)
curr = self.head
idx = 0
while curr:
if curr.data == target:
return idx
curr = curr.next
idx += 1
return -1
def delete_value(self, val): # O(n)
if self.head is None:
return False
if self.head.data == val:
self.delete_head()
return True
curr = self.head
while curr.next:
if curr.next.data == val:
if curr.next is self.tail:
self.tail = curr
curr.next = curr.next.next
self._size -= 1
return True
curr = curr.next
return False
복잡도 요약
| 연산 | 복잡도 |
|---|---|
| 앞 삽입/삭제 | O(1) |
| 뒤 삽입 (tail 유지) | O(1) |
| 뒤 삭제 | O(n) |
| 중간 삽입 (위치 알 때) | O(1) (포인터 연결만) |
| 탐색 | O(n) |
| 인덱스 접근 | O(n) |
연결 리스트가 배열보다 유리한 경우
- 앞쪽에 빈번한 삽입/삭제: 배열은 O(n) 이동이 필요하지만 연결 리스트는 O(1)
- 크기를 모를 때: 배열처럼 확장 복사 없이 노드를 그냥 추가
- 중간 위치에서 이미 노드 포인터를 가질 때: 연결만 바꾸면 O(1) 삽입
반면 배열이 유리한 경우: 인덱스 접근(O(1)), 캐시 효율이 중요한 순차 처리.
실전 응용
# 연결 리스트로 스택 구현 (배열 대비 앞 push가 O(1))
class Stack:
def __init__(self):
self.list = SinglyLinkedList()
def push(self, val):
self.list.prepend(val) # O(1)
def pop(self):
return self.list.delete_head() # O(1)
def peek(self):
return self.list.head.data
# Python의 deque는 이중 연결 리스트로 구현
from collections import deque
dq = deque()
dq.appendleft(1) # O(1)
dq.popleft() # O(1)
정리
- 단일 연결 리스트 = 노드(data + next 포인터)의 체인
- head 포인터로 시작, tail 포인터 유지 시 뒤 삽입 O(1)
- 앞 삽입/삭제 O(1) vs 뒤 삭제/탐색 O(n)
- 역전은 포인터만 변경 — O(n), 공간 O(1)
지난 글: 동적 배열
다음 글: 이중 연결 리스트
읽어주셔서 감사합니다. 😊