개념

각 노드가 데이터와 포인터를 가지고 한 줄로 연결되어 있는 자료구조

메모리 상에서 불연속적으로 저장되며, 포인터로 연결


시간복잡도

연산시간복잡도설명
접근O(n)처음부터 순차적으로 탐색
탐색O(n)처음부터 끝까지 순회 필요
삽입O(1)포인터만 변경 (위치를 알 때)
삭제O(1)포인터만 변경 (위치를 알 때)

특징

장점

  • 동적 크기: 필요할 때마다 메모리 할당 가능
  • 효율적인 삽입/삭제: 중간에 삽입/삭제 시 O(1) (위치를 알 때)
  • 메모리 효율: 필요한 만큼만 사용

단점

  • 느린 접근: 인덱스로 직접 접근 불가, O(n) 소요
  • 추가 메모리: 포인터 저장을 위한 메모리 필요
  • 캐시 비효율: 메모리가 불연속적이라 캐시 미스 발생

종류

1. 단일 연결 리스트 (Singly Linked List)

각 노드가 다음 노드만 가리킴

[data|next] -> [data|next] -> [data|next] -> None

2. 이중 연결 리스트 (Doubly Linked List)

각 노드가 이전 노드와 다음 노드를 모두 가리킴

None <- [prev|data|next] <-> [prev|data|next] <-> [prev|data|next] -> None

3. 원형 연결 리스트 (Circular Linked List)

마지막 노드가 첫 노드를 가리킴

[data|next] -> [data|next] -> [data|next] ┐
   ↑                                       │
   └───────────────────────────────────────┘

Python 구현

1. 노드 클래스 정의

class Node:
    def __init__(self, data):
        self.data = data
        self.next = None

2. 단일 연결 리스트 구현

class LinkedList:
    def __init__(self):
        self.head = None
 
    # 리스트 끝에 노드 추가 - O(n)
    def append(self, data):
        new_node = Node(data)
 
        if not self.head:
            self.head = new_node
            return
 
        current = self.head
        while current.next:
            current = current.next
        current.next = new_node
 
    # 리스트 맨 앞에 노드 추가 - O(1)
    def prepend(self, data):
        new_node = Node(data)
        new_node.next = self.head
        self.head = new_node
 
    # 특정 위치에 노드 삽입 - O(n)
    def insert_after(self, prev_node, data):
        if not prev_node:
            print("이전 노드가 존재하지 않습니다")
            return
 
        new_node = Node(data)
        new_node.next = prev_node.next
        prev_node.next = new_node
 
    # 특정 값을 가진 노드 삭제 - O(n)
    def delete(self, key):
        current = self.head
 
        # 삭제할 노드가 head인 경우
        if current and current.data == key:
            self.head = current.next
            return
 
        # 삭제할 노드 찾기
        prev = None
        while current and current.data != key:
            prev = current
            current = current.next
 
        # 노드를 찾지 못한 경우
        if not current:
            return
 
        # 노드 삭제
        prev.next = current.next
 
    # 리스트 출력 - O(n)
    def print_list(self):
        current = self.head
        while current:
            print(current.data, end=" -> ")
            current = current.next
        print("None")
 
    # 리스트 탐색 - O(n)
    def search(self, key):
        current = self.head
        while current:
            if current.data == key:
                return True
            current = current.next
        return False

3. 사용 예시

# 연결 리스트 생성
ll = LinkedList()
 
# 노드 추가
ll.append(1)
ll.append(2)
ll.append(3)
ll.prepend(0)
ll.print_list()  # 0 -> 1 -> 2 -> 3 -> None
 
# 노드 삽입
node = ll.head.next  # 1번 노드
ll.insert_after(node, 1.5)
ll.print_list()  # 0 -> 1 -> 1.5 -> 2 -> 3 -> None
 
# 노드 삭제
ll.delete(1.5)
ll.print_list()  # 0 -> 1 -> 2 -> 3 -> None
 
# 노드 탐색
print(ll.search(2))  # True
print(ll.search(5))  # False

자주 나오는 문제 유형

1. 연결 리스트 뒤집기 (Reverse)

문제: 연결 리스트를 역순으로 뒤집기

def reverse_list(head):
    prev = None
    current = head
 
    while current:
        # 다음 노드 임시 저장
        next_node = current.next
        # 현재 노드의 포인터 역전
        current.next = prev
        # prev와 current를 한 칸씩 이동
        prev = current
        current = next_node
 
    return prev  # 새로운 head
 
# 시간복잡도: O(n)
# 공간복잡도: O(1)

핵심: 포인터 방향을 하나씩 역전시키면서 순회


2. 중간 노드 찾기 (Fast & Slow Pointer)

문제: 연결 리스트의 중간 노드 찾기

def find_middle(head):
    slow = fast = head
 
    # fast는 2칸, slow는 1칸씩 이동
    while fast and fast.next:
        slow = slow.next
        fast = fast.next.next
 
    return slow  # slow가 중간 노드
 
# 시간복잡도: O(n)
# 공간복잡도: O(1)

핵심: 빠른 포인터와 느린 포인터를 사용한 투 포인터 기법


3. 사이클 탐지 (Floyd’s Cycle Detection)

문제: 연결 리스트에 사이클이 있는지 확인

def has_cycle(head):
    if not head:
        return False
 
    slow = fast = head
 
    while fast and fast.next:
        slow = slow.next
        fast = fast.next.next
 
        # 두 포인터가 만나면 사이클 존재
        if slow == fast:
            return True
 
    return False
 
# 시간복잡도: O(n)
# 공간복잡도: O(1)

핵심: 거북이와 토끼 알고리즘 - 사이클이 있으면 반드시 만남


4. 두 연결 리스트 합치기 (Merge Two Sorted Lists)

문제: 정렬된 두 연결 리스트를 하나로 합치기

def merge_two_lists(l1, l2):
    # 더미 노드 생성
    dummy = Node(0)
    current = dummy
 
    # 두 리스트를 비교하며 합치기
    while l1 and l2:
        if l1.data <= l2.data:
            current.next = l1
            l1 = l1.next
        else:
            current.next = l2
            l2 = l2.next
        current = current.next
 
    # 남은 노드 연결
    current.next = l1 if l1 else l2
 
    return dummy.next
 
# 시간복잡도: O(n + m)
# 공간복잡도: O(1)

핵심: 더미 노드를 사용해 코드 간결화


5. N번째 노드 삭제 (Remove Nth Node from End)

문제: 뒤에서 N번째 노드를 삭제

def remove_nth_from_end(head, n):
    dummy = Node(0)
    dummy.next = head
 
    # fast를 n칸 먼저 이동
    fast = slow = dummy
    for _ in range(n):
        fast = fast.next
 
    # fast와 slow를 함께 이동
    while fast.next:
        fast = fast.next
        slow = slow.next
 
    # slow.next가 삭제할 노드
    slow.next = slow.next.next
 
    return dummy.next
 
# 시간복잡도: O(n)
# 공간복잡도: O(1)

핵심: 두 포인터를 N칸 간격으로 유지하며 이동


추천 연습 문제

기초

중급

고급


핵심 요약

개념설명
구조노드(데이터 + 포인터)가 연결된 형태
삽입/삭제위치를 알 때 O(1), 찾아야 하면 O(n)
주요 기법투 포인터, 더미 노드, 재귀
vs 배열동적 크기 vs 빠른 접근

배열 vs 연결 리스트

특성배열연결 리스트
메모리연속적불연속적
접근O(1)O(n)
삽입/삭제O(n)O(1) (위치 알 때)
크기고정 (Python은 동적)동적
캐시효율적비효율적

주의사항

1. Null 체크 필수

# NoneType 에러 방지
if node and node.next:
    # 안전한 접근
    value = node.next.data

2. 더미 노드 활용

# head 변경 처리를 단순화
dummy = Node(0)
dummy.next = head
# ... 작업
return dummy.next

3. 포인터 순서 주의

# 잘못된 순서
current.next = new_node
new_node.next = current.next  # 이미 변경됨!
 
# 올바른 순서
new_node.next = current.next
current.next = new_node