한양과학기술고등학교

에듀테크 프로젝트 대시보드

연결 리스트 (Linked List)

연결 리스트 (Linked List)

데이터를 한 덩어리로 붙여 두는 배열과 달리, 값 하나하나를 노드라는 작은 상자에 담고 "다음 상자는 여기야"라는 포인터(주소)로 줄줄이 이어 붙인 자료구조. 상자들은 메모리 어디에 흩어져 있어도 상관없다.

한눈에 — 배열은 "번호표 붙은 사물함 한 줄", 연결 리스트는 "보물찾기 쪽지". 각 쪽지에 값과 다음 쪽지가 있는 곳이 적혀 있다. 중간에 쪽지 하나 끼워 넣는 건 쉽지만(양옆 화살표만 고쳐 쓰면 끝), "37번째 쪽지"를 보려면 처음부터 세며 따라가야 한다.

1. 직접 만져 보기 — 싱글 링크드 리스트

각 노드는 [ 값 | next ] 두 칸짜리 상자다. next 칸의 화살표가 다음 노드를 가리키고, 마지막 노드는 null(⏚)을 가리킨다. 노드를 클릭해 선택한 뒤 삽입·삭제해 보자.

■ 선택 노드 ■ 방금 추가 ■ 탐색 중 방문 ⏚ = null (리스트의 끝)
길이
0
마지막 연산
–
따라간 노드 수
0
노드를 클릭해 선택하고, 위 버튼으로 삽입·삭제해 보세요.

2. 배열은 왜 앞쪽 삽입이 느릴까

배열은 메모리에 빈틈 없이 딱 붙어 있다. 덕분에 arr[i]는 "시작 주소 + i × 원소크기"로 단번에 계산된다(O(1)). 하지만 맨 앞에 값을 넣으려면 뒤의 원소를 전부 한 칸씩 밀어야 한다.

배열 (연속 메모리) — 삽입 시 주황 칸이 밀려남
같은 값을 연결 리스트 head에 삽입 — 화살표 하나만 새로 씀
버튼을 눌러 두 방식의 삽입 비용을 비교하세요.

3. 시간 복잡도 비교

연산배열 (동적 배열)싱글 연결 리스트
i번째 원소 읽기O(1)O(n) — 처음부터 따라가야
맨 앞 삽입 / 삭제O(n) — 전부 이동O(1) — 화살표만 수정
맨 뒤 삽입O(1) 평균 (가끔 O(n) 재할당)tail 포인터 있으면 O(1)
"이 노드 뒤에" 삽입 / 삭제O(n)O(1) — 노드를 이미 알 때
값으로 탐색O(n)O(n)
메모리 오버헤드값만값 + 노드마다 포인터(들)
캐시 친화도높음 (연속 배치)낮음 (여기저기 흩어짐)
현실 조언 — "삽입/삭제가 많으니 연결 리스트"라는 말은 절반만 맞다. 요즘 CPU는 연속 메모리를 통째로 캐시에 올려 배열 순회를 매우 빠르게 한다. 중간 위치를 이미 참조로 들고 있고, 그 지점 삽입/삭제가 잦을 때 연결 리스트가 빛난다(예: LRU 캐시, 운영체제의 프로세스 큐).

4. 싱글 vs 더블 vs 원형

싱글 연결 리스트 — 노드가 next 하나만 가짐. 앞으로만 이동. 이전 노드를 지우거나 되돌아가려면 따로 추적해야 한다.

더블(이중) 연결 리스트 — next + prev. 양방향 이동, "이 노드"만 알아도 O(1) 삭제 가능. 대신 포인터가 2배, 수정할 링크도 2배.

원형 연결 리스트 — 마지막 노드의 next가 null 대신 head를 가리킴. 라운드 로빈 CPU 스케줄링, 음악 반복 재생 목록 등에 쓰인다.

5. 코드 — 파이썬 싱글 연결 리스트

class Node:
    def __init__(self, value):
        self.value = value
        self.next  = None          # 다음 노드 참조 (없으면 None)

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

    def push_front(self, value):        # O(1)
        node = Node(value)
        node.next = self.head          # 새 노드가 옛 head를 가리킴
        self.head = node               # head를 새 노드로 갱신
        self.size += 1

    def insert_after(self, node, value):  # O(1) — node를 이미 알 때
        new = Node(value)
        new.next  = node.next
        node.next = new
        self.size += 1

    def remove(self, value):           # O(n) — 앞 노드를 찾아야
        prev, cur = None, self.head
        while cur:
            if cur.value == value:
                if prev: prev.next = cur.next
                else:    self.head = cur.next
                self.size -= 1
                return True
            prev, cur = cur, cur.next
        return False

    def find(self, value):             # O(n) — 랜덤 접근 불가
        cur = self.head
        while cur:
            if cur.value == value: return cur
            cur = cur.next
        return None
포인터 재배치 순서가 생명 — push_front에서 self.head = node를 먼저 해버리면 옛 head를 가리킬 방법이 사라진다(메모리 누수 / 리스트 유실). 항상 새 노드의 next를 먼저 연결하고 나서 head를 옮긴다.

6. 어디에 쓰이나

스택·큐의 구현 — 스택/큐를 연결 리스트로 만들면 크기 제한도, 재할당 비용도 없다.

해시 테이블의 체이닝 — 해시 충돌이 난 키들을 같은 버킷에서 연결 리스트로 매단다.

LRU 캐시 — 캐싱 전략에서 이중 연결 리스트 + 해시맵으로 "가장 오래 안 쓴 항목"을 O(1)에 앞뒤로 옮긴다.

메모리 free list — 동적 메모리 할당자가 비어 있는 블록들을 연결 리스트로 관리한다.

Git·블록체인 — 커밋 그래프나 블록체인은 "이전을 가리키는" 연결 리스트의 확장이다.

한 줄 요약 — 값 + "다음 위치" 포인터를 가진 노드를 이어 붙인 사슬. 랜덤 접근을 포기하는 대신, 이미 손에 쥔 위치에서의 삽입·삭제를 O(1)로 만든다. 관련: 스택 & 큐, 플로이드 순환 탐지, Big-O 표기법.