LeetCode 206 Reverse Linked List 풀이: 포인터 방향을 뒤집는 반복문
작성·코드 검증: solve together · 작성 언어: Python
0. 문제 링크와 출처
- 문제: 206. Reverse Linked List
- 출처: LeetCode
- 학습: 연결 리스트 학습 가이드
- 자체 판단 난이도: Level 2
현재 노드의 next 방향을 이전 노드로 바꾸면서도 아직 방문하지 않은 다음 노드를 잃지 않아야
한다. 포인터 세 개의 역할과 갱신 순서를 정확히 관리해야 하지만 별도의 복잡한 자료구조는
필요하지 않으므로 Level 2로 판단했다.
1. 문제 요약
단일 연결 리스트의 시작 노드 head가 주어질 때, 리스트의 방향을 반대로 뒤집은 뒤 새로운
시작 노드를 반환한다. 기존 노드를 새로 복사하지 않고 next 연결을 직접 수정한다.
중요한 조건은 다음과 같다.
- 노드 수는
0이상5000이하이다. - 노드 값은
-5000이상5000이하이다. - 빈 리스트가 입력될 수 있다.
- 결과는 기존 연결 리스트의 노드 연결을 반대로 바꾼 리스트다.
예를 들어 1 -> 2 -> 3 -> 4 -> 5를 뒤집으면
5 -> 4 -> 3 -> 2 -> 1이 된다. 빈 리스트는 뒤집어도 빈 리스트이므로 None을 그대로
반환한다.
2. 문제 해결에 사용되는 알고리즘이나 자료구조 설명
연결 리스트 포인터 상태
반복문에서 세 가지 포인터를 사용한다.
previous: 현재 노드의 뒤에 연결할 이미 뒤집힌 리스트의 시작점current: 아직 방향을 바꾸지 않은 현재 노드next_node:current.next를 방향 변경 전에 저장한 다음 노드
초기 상태는 previous = None, current = head다. 한 번의 반복에서 다음 순서로
처리한다.
current.next를next_node에 저장한다.current.next = previous로 현재 노드의 방향을 뒤집는다.previous = current로 뒤집힌 리스트의 시작점을 갱신한다.current = next_node로 아직 처리하지 않은 다음 노드로 이동한다.
반복이 끝나면 current는 None이고, previous가 새 head다.
링크를 바꾸기 전에 다음 노드를 보존하기
연결 리스트는 배열처럼 이전 원소를 인덱스로 찾을 수 없다. 현재 노드의 next를 바로
덮어쓰면 원래 다음 노드로 갈 수 있는 유일한 연결을 잃게 된다.
그래서 방향을 바꾸기 전에 반드시 원래 다음 노드를 저장해야 한다.
저장 전: previous <- current -> next_node -> ...
방향 변경: previous <- current <- next_node? 가 아니라,
previous <- current next_node -> ...
실제 한 단계의 상태는 다음과 같다.
이전: None <- 1 2 -> 3 -> None
처리: None <- 1 <- 2 3 -> None
처리: None <- 1 <- 2 <- 3
상태 전이와 경계 처리
빈 리스트이면 current가 처음부터 None이므로 반복문이 실행되지 않고 previous, 즉
None을 반환한다. 노드가 하나뿐이면 그 노드의 next를 None으로 설정한 뒤 그 노드를
반환하므로 별도 분기가 필요하지 않다.
마지막 노드를 처리할 때 next_node는 None이 된다. 마지막 노드의 next를 이전 노드로
연결하고 current를 None으로 이동하면 반복이 끝나며, 마지막으로 처리한 노드가 새 head가
된다.
시간·공간 복잡도
각 노드를 한 번씩 방문하고 next 연결을 한 번씩 바꾸므로 시간 복잡도는 O(n)이다.
포인터 몇 개만 저장하고 새로운 배열이나 노드 목록을 만들지 않으므로 추가 공간 복잡도는
O(1)이다.
3. 문제 해결 코드 (Python)
current.next를 수정하기 전에 next_node에 원래 연결을 저장한다. 방향을 바꾼 뒤 두
포인터를 한 칸씩 이동하고, 모든 노드를 처리하면 previous를 반환한다.
class Solution:
def reverseList(self, head: 'ListNode | None') -> 'ListNode | None':
previous = None
current = head
while current is not None:
next_node = current.next
current.next = previous
previous = current
current = next_node
return previous
가장 중요한 순서는 next_node 저장, current.next 변경, 포인터 이동이다. previous는
이미 방향을 뒤집은 리스트의 head이고, 마지막 반복이 끝나면 전체 역순 리스트의 head가 된다.
4. 오답 접근과 반례
다음 노드를 저장하지 않고 연결을 바꾸기
다음과 같이 current.next를 먼저 바꾸면 원래 다음 노드로 이동할 방법을 잃는다.
current.next = previous
current = current.next # 원래 다음 노드가 아니라 previous로 이동
1 -> 2 -> 3에서 첫 노드의 next를 None으로 바꾼 뒤 current = current.next를 실행하면
리스트의 나머지 2 -> 3에 도달할 수 없다. 반드시 수정 전의 current.next를 임시 변수에
저장해야 한다.
노드 값만 역순으로 복사하기
값을 배열에 모아 역순으로 반환하면 값의 순서는 만들 수 있지만, 문제에서 요구하는 연결
리스트의 next 구조를 직접 뒤집는 방식이 아니다. O(n) 추가 공간도 필요하다.
head를 그대로 반환하기
연결만 뒤집고 원래 head를 반환하면 새 리스트의 시작점이 잘못된다.
원래: 1 -> 2 -> 3 -> None
뒤집은 연결: None <- 1 <- 2 <- 3
새 head: 3
원래 head인 1은 역순 리스트의 마지막 노드가 되므로, 마지막으로 처리한 previous를 반환해야
한다.
복사해 실행할 테스트
로컬 실행에서는 아래 ListNode와 헬퍼를 풀이 클래스 앞에 두고, 검증 코드를 클래스 뒤에 붙여 실행한다.
class ListNode:
def __init__(self, val=0, next=None):
self.val = val
self.next = next
def make_list(values):
dummy = tail = ListNode()
for value in values:
tail.next = ListNode(value)
tail = tail.next
return dummy.next
def to_list(node):
result = []
while node:
result.append(node.val)
node = node.next
return result
assert to_list(Solution().reverseList(make_list([1, 2, 3, 4, 5]))) == [5, 4, 3, 2, 1]
assert Solution().reverseList(None) is None
5. 공식 예제와 추가 검증
작성한 reverseList 함수를 Python에서 직접 연결 리스트 노드로 구성해 공식 예제와 빈 리스트,
단일 노드, 중복 값 케이스를 확인했다.
공식 예제
입력: head = [1,2,3,4,5]
기대 출력: [5,4,3,2,1]
확인 결과: 통과
입력: head = [1,2]
기대 출력: [2,1]
확인 결과: 통과
입력: head = []
기대 출력: []
확인 결과: 통과
추가 검증 1: 단일 노드
입력: head = [7]
기대 출력: [7]
확인 결과: 통과
검증 목적: 노드 하나의 next가 None으로 유지되는지 확인
추가 검증 2: 음수와 중복 값
입력: head = [-3,0,0,8]
기대 출력: [8,0,0,-3]
확인 결과: 통과
검증 목적: 값의 크기나 중복 여부와 관계없이 연결 순서만 뒤집는지 확인
추가 검증 3: 결과가 원본 노드를 재사용하는지 확인
입력: head = [1,2,3]
검증: 반환 리스트의 노드 id 집합과 입력 리스트의 노드 id 집합 비교
기대 결과: 같은 노드들을 역순으로 연결
확인 결과: 통과
검증 목적: 새 노드를 만들지 않고 next 연결만 수정하는지 확인
6. 다른 풀이 방식 검색 및 비교
- Reverse Linked List 공식 문제는 단일 연결 리스트의
next방향을 반대로 바꾸는 조건을 정의한다. - NeetCode의 Reverse Linked List 설명은 반복형 포인터 변경과 재귀형 리스트 반전을 비교한다.
재귀형 풀이는 현재 노드의 다음 리스트를 먼저 뒤집은 뒤 다음 노드의 next를 현재 노드로
연결하는 방식이다. 이해하기 쉽지만 재귀 호출 스택에 O(n) 공간을 사용한다.
이 글의 반복형 풀이는 previous, current, next_node만 유지하므로 O(1) 추가 공간으로
처리한다. 두 방식 모두 각 노드를 한 번씩 방문해 시간 복잡도는 O(n)이다.
마무리
Reverse Linked List의 핵심은 현재 노드의 연결을 바꾸기 전에 원래 다음 노드를 저장하는
것이다. previous와 current를 한 칸씩 이동하며 current.next를 이전 노드로 바꾸면
새 head를 잃지 않고 전체 리스트를 뒤집을 수 있다. 반복형 구현은 O(n) 시간과 O(1)
공간으로 동작한다.