문제 정의
연결 리스트가 하나 주어져 있을 때, 리스트를 구성하는 요소들이 회문(palindrome)을 이루는지 확인해야 합니다. 회문이란 앞에서부터 읽으나 뒤에서부터 읽으나 순서가 동일한 수열이나 문자열을 의미합니다.
예를 들어 리스트가 [5, 4, 3, 4, 5]라면 어느 방향에서 읽어도 같으므로 회문입니다. 반면 [5, 4, 3, 2, 1]은 뒤집으면 [1, 2, 3, 4, 5]가 되어 원래 순서와 다르므로 회문이 아닙니다.
해결 전략
이 문제는 리스트를 배열에 복사하지 않고도 투 포인터(Two Pointer) 기법과 연결 리스트 뒤집기를 조합하면 효율적으로 해결할 수 있습니다. 핵심 아이디어는 다음과 같습니다.
- slow 포인터는 한 번에 한 칸씩, fast 포인터는 두 칸씩 이동시켜 리스트의 중간 지점을 찾습니다.
- slow 포인터가 지나가는 노드들을 순회하면서 리스트의 앞부분을 역방향으로 뒤집어 새로운 리스트(rev)를 만듭니다.
- 중간 지점을 기준으로 왼쪽 절반(뒤집힌 부분)과 오른쪽 절반의 값을 차례대로 비교합니다.
- 리스트 길이가 홀수라면 가운데 노드는 비교 대상에서 제외하도록 flag 변수로 처리합니다.
알고리즘 단계
- fast := head, slow := head, rev := None, flag := 1로 초기화합니다.
- head가 비어 있다면 True를 반환합니다.
- fast와 fast.next가 모두 존재하는 동안 반복합니다.
- 만약 fast.next.next가 존재하지 않으면(짝수 길이인 경우) flag := 0으로 설정한 뒤 반복을 중단합니다.
- fast := fast.next.next (두 칸 전진)
- temp := slow, slow := slow.next, temp.next := rev, rev := temp (앞부분 뒤집기)
- fast := slow.next로 설정하고, slow.next := rev로 연결을 재구성합니다.
- flag가 1이면(홀수 길이이면) slow := slow.next로 한 칸 더 이동해 가운데 노드를 건너뜁니다.
- fast와 slow가 모두 None이 아닌 동안 두 노드의 값을 비교하고, 하나라도 다르면 False를 반환합니다.
- 모든 값이 일치하면 True를 반환합니다.
파이썬 구현 코드
아래 예제를 통해 실제 동작 과정을 더 잘 이해할 수 있습니다.
class ListNode:
def __init__(self, data, next=None):
self.data = data
self.next = next
def make_list(elements):
head = ListNode(elements[0])
for element in elements[1:]:
ptr = head
while ptr.next:
ptr = ptr.next
ptr.next = ListNode(element)
return head
class Solution(object):
def isPalindrome(self, head):
fast, slow = head, head
rev = None
flag = 1
if not head:
return True
while fast and fast.next:
if not fast.next.next:
flag = 0
break
fast = fast.next.next
temp = slow
slow = slow.next
temp.next = rev
rev = temp
fast = slow.next
slow.next = rev
if flag:
slow = slow.next
while fast and slow:
if fast.data != slow.data:
return False
fast = fast.next
slow = slow.next
return True
head = make_list([5, 4, 3, 4, 5])
ob1 = Solution()
print(ob1.isPalindrome(head))
입력
[5, 4, 3, 4, 5]
출력
True
복잡도 분석 및 마무리
이 알고리즘은 fast 포인터로 리스트 끝까지 이동하는 동시에 앞부분을 뒤집고, 이후 절반만 한 번 더 비교하므로 시간 복잡도는 O(n)입니다. 또한 포인터 변수 몇 개만 사용하므로 공간 복잡도는 O(1)로, 리스트 전체를 배열에 담아 비교하는 방식(O(n) 공간 필요)보다 메모리 면에서 훨씬 효율적입니다.
다만 이 방식은 순회 과정에서 원본 리스트의 연결 구조가 변경된다는 점에 유의해야 하며, 원본 유지가 필요하다면 비교가 끝난 뒤 리스트를 다시 복원하는 과정을 추가하면 됩니다.