리트코드, Palindrome Linked List_연결리스트 팰린드롬
TL;DR
- 연결 리스트(Linked List)를 활용하여 팰린드롬 문제를 해결할 수 있는지
문제 분석
1. 주어진 연결 리스트 head가 팰린드롬일 경우 true를 반환하여라.
- 1 : 해결해야 하는 문제에 대해서 말하고 있다. 특이한 점은 연결 리스트로 자료형이 주어져 있다는 것이다.
입출력 형태
- 예시에서는 리스트 형태로 입력이 주어지지만, 입력은 연결 리스트 형태로 주어진다.
class ListNode:
def __init__(self, val=0, next=None):
self.val = val
self.next = next
- val에는 해당하는 노드의 값이 주어지고, next에는 연결된 다음 연결 리스트가 주어진다.
- print문을 통해서 출력을 해보면 아래 그림과 같다.
- 이와 같이 주어진 연결 리스트가 팰린드롬 조건을 만족하면 True, 아니라면 False를 반환하면 된다.
풀이
- 문제는 다양한 방법으로 풀 수 있다.
- 리스트(list)와 데크(deque)를 사용하는 추가적인 메모리를 할당하는 방법과, 현재 공부하고 있는 책인 "파이썬 알고리즘 인터뷰"에서 소개하는 런너(Runner) 기법을 사용한, 추가적인 메모리 할당 없이 문제를 풀 수 있는 방법이 있다.
1. 리스트와 투 포인터를 활용한 방법
class Solution:
def isPalindrome(self, head: ListNode) -> bool:
if not head:
return True
palindrome = []
while head:
palindrome.append(head.val)
head = head.next
# two pointer
left, right = 0, len(palindrome) - 1
while left < right:
if palindrome[left] == palindrome[right]:
left += 1
right -= 1
else:
return False
return True
- 만약, 주어진 연결 리스트 head가 비어 있다면 그 즉시 True를 반환하고 종료한다. 비어 있는 연결 리스트도 팰린드롬으로 볼 수 있다.
- 연결 리스트에 저장된 val을 저장할 리스트 palindrome를 선언한다.
- head가 None이 아닐 때까지, 저장된 val을 리스트에 저장한다. 저장한 이후에는 next를 통해 다음 노드로 넘어가야 한다.
- 투 포인터를 활용하여 양 끝을 비교, 팰린드롬인지 여부를 판단한다. 만약, palindrome[left]와 palindrome[right]가 다를 경우 팰린드롬이 아니므로 False를 반환한다.
- palidrome.pop(0)와 palindrome.pop()을 비교하는 방법으로도 문제를 해결할 수 있다.
2. deque를 사용하는 방법
class Solution:
def isPalindrome(self, head: ListNode) -> bool:
if not head:
return True
palindrome: deque = deque()
while head:
palindrome.append(head.val)
head = head.next
while len(palindrome) > 1:
if palindrome.popleft() != palindrome.pop():
return False
return True
- 리스트 대신 데크를 사용한 풀이 방법이다.
- O(n)이 필요한 pop(0) 연산을 O(1)인 데크의 popleft() 메소드를 통해서 시간 복잡도에서 유리함을 얻을 수 있다.
3. 연결 리스트를 그대로 사용한 풀이
class Solution:
def isPalindrome(self, head: ListNode) -> bool:
if not head:
return True
rev = None
slow, fast = head, head # 1
while fast and fast.next: # 2
fast = fast.next.next
rev, rev.next, slow = slow, rev, slow.next
if fast: # 3
slow = slow.next
while rev and rev.val == slow.val: # 4
rev, slow = rev.next, slow.next
return not rev
- 런너 기법은 서로 속도가 다른 두 개의 런너를 사용하여, 빠른 런너가 연결 리스트에 끝까지 갔을 때, 느린 런너가 연결 리스트의 중간 지점에 도착하도록 하여, 중간 지점을 중심으로 값을 비교하거나, 뒤집어야 할 때 유용하게 사용할 수 있다고 책에서 설명하고 있다.
- 1. slow와 fast는 처음에는 시작 지점인 head에서 시작한다.
- 2. fast가 연결 리스트의 끝까지 도착할 때까지 각 런너들이 이동한다.
- fast는 두 번 이동, slow는 한 번 이동한다.
- 이 때, slow의 val을 rev라는 연결 리스트에 저장한다. 이 rev는 주어진 연결 리스트의 역순으로, 중간 지점 이후의 값과 비교하기 위해 사용한다.
- rev에는 slow를, rev.next는 아직 없으로므로 현재 rev를, slow는 다음 단계로 이동한 slow를 할당한다.
- 3. 연결 리스트의 길이가 홀수인 경우를 위한 처리이다. 중간 지점은 팰린드롬에 포함되지 않으므로 넘겨주어야 한다.
- 이 과정이 지나면 rev는 2 -> 1 -> None 형태의 연결 리스트가 되고, slow는 중간 지점 이후의 연결 리스트들이 남게 된다.
- 4. 역순 연결 리스트인 rev와 남은 slow의 val을 비교한다.
- 팰린드롬일 경우에는 모든 연결 리스트를 조회하고 rev와 slow 모두 None이 된다. 따라서 이 때는 not rev 또는 not slow를 반환한다.
- 팰린드롬이 아닐 경우에는, rev와 slow가 None이 아니기 때문에 False가 반환된다.
- 정리하자면 slow가 조회하는 값들을 연결 리스트에 배치함으로써, 중간 지점을 중심으로 왼쪽의 연결 리스트를 역순으로 배치하고, 이를 오른쪽 연결 리스트와 비교하는 것이 되겠다.
'개발 > Algorithm' 카테고리의 다른 글
[리트코드] Merge Sorted Array(python) (0) | 2021.06.10 |
---|---|
[리트코드] Plus One(python) (0) | 2021.06.09 |
[프로그래머스] 피보나치 수(python) (0) | 2021.06.06 |
[리트코드] Best Time to Buy and Sell Stock(python) (0) | 2021.06.05 |
[리트코드] Product of Array Except Self(python) (0) | 2021.06.04 |