# Problem
Given the head of a singly linked list, return true if it is a palindrome. Constraints:
|
# My Answer
# Definition for singly-linked list.
# class ListNode:
# def __init__(self, val=0, next=None):
# self.val = val
# self.next = next
class Solution:
def isPalindrome(self, head: ListNode) -> bool:
h=[]
while head:
h.append(head.val)
head = head.next
if h==h[::-1]:
return True
# Solution 1 - 리스트 변환
# Definition for singly-linked list.
class ListNode:
def __init__(self, x):
self.val = x
self.next = None
class Solution:
def isPalindrome(self, head: ListNode) -> bool:
q: List = []
if not head:
return True
node = head
# 리스트 변환
while node is not None:
q.append(node.val)
node = node.next
# 팰린드롬 판별
# 시작과 끝 하나씩 비교
while len(q) > 1:
if q.pop(0) != q.pop():
return False
return True
# Solution 2 - 데크를 이용한 최적화
import collections
from typing import Deque
# Definition for singly-linked list.
class ListNode:
def __init__(self, x):
self.val = x
self.next = None
class Solution:
def isPalindrome(self, head: ListNode) -> bool:
# 데크 자료형 선언
q: Deque = collections.deque()
if not head:
return True
node = head
while node is not None:
q.append(node.val)
node = node.next
while len(q) > 1:
if q.popleft() != q.pop():
return False
return True
파이썬의 리스트에서 pop(0)을 하게 되면 모든 값이 한 칸씩 shifting 되며 , 시간 복잡도 O(n) 발생
파이썬의 데크(Deque)는 이중 연결 리스트로 양쪽 방향 모두 추출하는데 시간 복잡도 O(1)에 실행
-> popleft(), pop() 사용
실행시간이 # Solution 1 보다 2배 이상 빨라짐
# Solution 3 - 고(GO)를 이용한 데크 구현
성능이 매우 좋지만 Go는 데크 자료형을 지원하지 않기 때문에 직접 구현해야함 - 비효율적
// Go
// 데크(deque) 자료형을 Go로 바닥부터 구현한 풀이
const minCapacity = 16
type Deque struct {
buf []interface{}
head int
tail int
count int
minCap int
}
func (q *Deque) Len() int {
return q.count
}
func (q *Deque) PushBack(elem interface{}) {
q.growIfFull()
q.buf[q.tail] = elem
q.tail = q.next(q.tail)
q.count++
}
func (q *Deque) PopFront() interface{} {
if q.count <= 0 {
panic("deque: PopFront() called on empty queue")
}
ret := q.buf[q.head]
q.buf[q.head] = nil
q.head = q.next(q.head)
q.count--
return ret
}
func (q *Deque) PopBack() interface{} {
if q.count <= 0 {
panic("deque: PopBack() called on empty queue")
}
q.tail = q.prev(q.tail)
ret := q.buf[q.tail]
q.buf[q.tail] = nil
q.count--
return ret
}
func (q *Deque) prev(i int) int {
return (i - 1) & (len(q.buf) - 1)
}
func (q *Deque) next(i int) int {
return (i + 1) & (len(q.buf) - 1)
}
func (q *Deque) growIfFull() {
if len(q.buf) == 0 {
if q.minCap == 0 {
q.minCap = minCapacity
}
q.buf = make([]interface{}, q.minCap)
return
}
if q.count == len(q.buf) {
q.resize()
}
}
func (q *Deque) resize() {
newBuf := make([]interface{}, q.count<<1)
if q.tail > q.head {
copy(newBuf, q.buf[q.head:q.tail])
} else {
n := copy(newBuf, q.buf[q.head:])
copy(newBuf[n:], q.buf[:q.tail])
}
q.head = 0
q.tail = q.count
q.buf = newBuf
}
func isPalindrome(head *ListNode) bool {
var q Deque
if head == nil {
return true
}
node := head
for node != nil {
q.PushBack(node.Val)
node = node.Next
}
for q.Len() > 1 {
if q.PopFront() != q.PopBack() {
return false
}
}
return true
}
# Solution 4 - 런너를 이용한 우아한 풀이
# Definition for singly-linked list.
class ListNode:
def __init__(self, x):
self.val = x
self.next = None
class Solution:
def isPalindrome(self, head: ListNode) -> bool:
rev = None
slow = fast = head
# 런너를 이용해 역순 연결 리스트 구성
while fast and fast.next:
fast = fast.next.next
rev, rev.next, slow = slow, rev, slow.next
if fast: # fast가 홀수인 경우
slow = slow.next
# 팰린드롬 여부 확인
# 처음부터 절반까지 리스트 rev와 절반부터 끝까지 리스트 slow 비교
while rev and rev.val == slow.val:
slow, rev = slow.next, rev.next
return not rev # 끝까지 가서 rev가 None이면 True 반환, 중간에 값이 달라서 while문 빠져나오면 False 반환
런너 (Runner) 기법 활용 ( 아래 설명 참고 )
느린 런너는 중간까지 이동하면서 역순 연결 리스트 rev를 만들어 나간다.
fast가 끝까지 가면 느린 런너는 중간까지 가고 이때 부터 비교한다.
처음부터 절반까지 역순 연결 리스트 rev와 절반부터 끝까지 남은 연결 리스트 slow 비교
다중 할당
rev, rev.next, slow = slow, rev, slow.next
만약 나눠서
rev, rev.next = slow,rev
slow = slow.next
이렇게 할당하게 되면 rev와 slow는 동일한 객체를 참조하고 있기 떄문에 전혀 다른 결과가 나온다
ex) rev = 1 , slow = 2->3이라면
첫째 줄 실행시 rev = 2->3, 이고 rev.next = 1 이므로 rev = 2->1 이 된다.
이때 rev는 slow와 같은 객체를 참조하고 있으므로 slow도 2->1이 된다
따라서 두번째 줄 실행시 slow = 1이라는 결과가 나오게 된다.
3개의 변수에 다중할당 했을 경우
첫째 줄 실행시 rev = 2->1, slow = 3 ( 동시에 작업 실행)
# 연결 리스트
: 데이터 요소의 선형 집합으로, 데이터의 순서가 메모리에 물리적인 순서대로 저장되지는 않는다.
동적으로 새로운 노드의 삽입, 삭제가 간편
연결 구조를 통해 물리 메모리를 연속적으로 사용하지 않아도 되기 때문에 관리가 쉬움
서로 연결된 형태로 구성되어 있으며, 메모리 어딘가에 여기저기 흩뿌려진 형상을 띤다.
특정 인덱스에 접근 ( 탐색) O(n) 소요
시작 또는 끝 지점에 추가,삭제, 추출 작업은 O(1)
# 런너 기법 (Runner)
: 연결 리스트를 순회할 때 2개의 포인터를 동시에 사용하는 기법
한 포인터가 다른 포인터보다 앞서게 하여 병합 지점이나 중간 위치, 길이 등을 판별할 때 유용
빠른 런너 (Fast Runner) , 느린 런너 ( Slow Runner )
빠른 런너가 2칸씩 이동, 느린 런너가 1칸씩 이동할 경우 빠른 런너가 끝에 도달하면 느린 런너는 중간 지점에 도달
# 다중 할당 (Multiple Assignment)
: 2개 이상의 값을 2개 이상의 변수에 동시에 할당
'=' 연산자를 활용해 할당을 진행하면 값을 할당하는 것이 아니라 불변 객체에 대한 참조를 할당하게 된다.
'Algorithm > LEET CODE ( 파이썬 알고리즘 인터뷰)' 카테고리의 다른 글
[LEET CODE] 206. Reverse Linked List (0) | 2021.03.22 |
---|---|
[LEET CODE] 21. Merge Two Sorted Lists ( 연산자 우선순위, 변수 스왑) (0) | 2021.03.22 |
[LEET CODE] 121. Best Time to Buy and Sell Stock (0) | 2021.03.22 |
[LEET CODE] 238. Product of Array Except Self (0) | 2021.03.21 |
[LEET CODE] 561. Array Partition I (0) | 2021.03.21 |