* 해당 포스트는 <파이썬 알고리즘 인터뷰> 공부 후 정리 목적으로 작성되었습니다. *

 

leetcode 232번 ( https://leetcode.com/problems/implement-queue-using-stacks/ )

 

풀이방법 1. 

교재를 보지 않고 시도했던 방법이다. 이전 문제와 비슷하게, 스택에서 사용할 수 있는 메소드만 사용해서 큐를 구현하는 문제이다. 

 

이전의 큐는 원소를 빼낼 때는 앞에서, 원소를 넣을 때는 뒤에서 넣었다(FIFO). 반면 스택은 원소를 넣는 것과 빼는 작업이 모두 맨 뒤에서 일어난다(LIFO). 그래서 스택을 큐로 구현할 때처럼 큐의 앞에서 원소를 뺀 다음에 큐의 맨 뒤에 넣는 방식으로, 공간을 안 사용하고 풀 수는 없다. 스택에서 원소를 하나 빼서 하나를 다시 넣는다고 해서 스택의 값이 변하지 않기 때문이다. 

 

그래서 스택 하나를 더 사용해서 정렬 작업을 할 것이다. 

(여기서는 스택을 모두 리스트로 구현하였다.)

이제 push(), pop(), peek() 메소드를 구현해야 하는데, 어디서 정렬하는지는 상관없다. 이번 풀이에서는 push()할 때 정렬을 같이 진행했다. 

 

기존 스택을 stack, 순서를 바꾸는 데 필요한 새 스택을 reverse로 두었다.

1. 새 값이 들어오면 먼저 기존 stacl에 있던 원소들을 전부 reverse로 옮긴다.

2. 빈 스택에 새 값을 먼저 넣은 뒤, reverse에 있던 값들을 다시 stack으로 옮기면 된다. 

하노이 탑과 비슷한 원리이다. 

 

push()에서 정렬 작업을 모두 마쳤기 때문에 pop(), peek() 메소드는 간단하게 구현할 수 있다. 

peek()은 stack의 맨 마지막 원소를 리턴하면 되므로, 우선 원소가 있는지를 확인한 뒤 -1번째 인덱스를 리턴하면 된다. 

(비어 있는 리스트에서 인덱스를 참조할 경우 오류가 나므로 주의하자)

 

pop()의 경우, 리스트에서 기본으로 제공되는 메소드이므로 그냥 사용하면 된다. 

peek()과 마찬가지로 원소가 없는 리스트에서 pop()을 할 경우 오류가 발생하므로 길이를 확인한 뒤 실행하자. 

 

전체 코드는 다음과 같다. 

class MyQueue(object):

    def __init__(self):
        self.stack = []
        self.reverse = []

    def push(self, x):
        """
        :type x: int
        :rtype: None
        """
        for _ in range(len(self.stack)):
            self.reverse.append(self.stack.pop())
        
        self.stack.append(x)
        for _ in range(len(self.reverse)):
            self.stack.append(self.reverse.pop())

        

    def pop(self):
        """
        :rtype: int
        """
        if len(self.stack) >= 1:
            return self.stack.pop()
        

    def peek(self):
        """
        :rtype: int
        """
        if len(self.stack) >= 1:
            return self.stack[-1]
        

    def empty(self):
        """
        :rtype: bool
        """
        return len(self.stack) == 0

 

풀이방법 2. 

교재에서 언급된 풀이방법이다. 두 개의 스택(리스트로 구현)을 사용한다는 점은 같은데, 시공간 복잡도가 더 효율적이다. 

 

이전 방법에서는 push()를 시행할 때마다 정렬을 진행하였는데, 이 경우 매번 stack에 있던 원소를 reverse로 옮긴 뒤, 새 원소를 넣고, 다시 reverse에서 stack으로 옮기면서 시간이 더 소요될 수 있다. 

 

여기서도 두 개의 스택을 사용하지만 각 스택을 그때그때 들어오는 원소를 담아두는 input, peek()나 pop()이 호출될 때 스택을 정렬하기 위한 output으로 구분하였다. 

즉 push()가 호출될 때마다 매번 정렬이 일어나지 않고, 정렬이 일어나더라도 모든 원소를 옮기지 않는 방식이다. 

 

peek()나 pop()이 호출되면 우선 output 스택에 원소가 있는지 확인한다. 만약 원소가 있다면, 이 원소들은 input 스택에 들어있는 원소보다 이전에 들어온 원소들이 이미 정렬된 상태이므로, 여기서 가장 위에 있는 것을 제거한다. 

 

만약 output 스택에 원소가 없다면, 그 때는 input 스택에 있던 원소를 output 스택으로 옮긴다. 이전 풀이에서의 하노이 탑과 같은 원리이기 때문에 input에서 가장 먼저 들어왔던 원소가 output 스택의 맨 위에 오게 된다. 

이후 또 peek()나 pop()이 호출된다면, 그 때는 input 스택에 원소가 있던 없던 상관없이 output 스택의 맨 위에 가장 먼저 들어온 원소가 있으므로, output 스택이 빌 때까지 차례대로 빼서 사용하면 된다. 

 

같은 원리이지만 불필요한 정렬을 줄인 방법이라고 볼 수 있겠다. 

 

* 해당 포스트는 <파이썬 알고리즘 인터뷰> 공부 후 정리 목적으로 작성되었습니다. *

 

leetcode 225번 ( https://leetcode.com/problems/implement-stack-using-queues/description/ )

 

풀이방법.

보통 큐나 스택은 배열이나 연결 리스트(linked-list)로 많이 구현하는데, 이 문제는 특이하게 스택을 큐로 구현하는 문제였다. 큐와 스택은 배열이나 연결 리스트처럼 실제로 구현된 자료구조 형태가 아닌 추상적인 자료구조(ADT, abstract data types)이다. 그래서 실제 사용할 때는 다른 자료구조를 이용하여 구현한다. 

 

파이썬에는 스택의 push()와 pop(), 큐의 enqueue(), dequeue() 메소드가 모두 구현된 데크(deque) 자료형이 있다. 그러나 이 문제에서는 큐 ADT에서만 사용할 수 있는 메소드(enqueue, dequeue)만 가지고 스택을 구현해야 한다. 

 

데크에서는 popleft(), append()가 각각 enqueue(), dequeue()와 같은 기능을 한다.

 

스택과 큐는 원소를 추가할 때는 리턴되는 값이 없고 원소를 빼낼 때 값을 리턴하는데, 리턴하는 값의 순서가 다르다. 그러므로 원소를 넣을 때나 뺄 때 추가로 정렬 작업이 필요하다. 

여기서는 원소를 넣을 때 정렬 작업을 해 주었다. 

class Stack:
	
	def __init__(self):
		self.queue = collections.deque()
        
	def push(self, val):
		self.queue.append(val)
		self.queue.append(self.queue.popleft())
            
	def pop(self):
		return self.queue.popleft()
        
	def peek(self):
		return self.queue[0]
        
	def empty(self):
		return len(self.queue) == 0

 

* 해당 포스트는 <파이썬 알고리즘 인터뷰> 공부 후 정리 목적으로 작성되었습니다. *

 

leetcode 739번 ( https://leetcode.com/problems/daily-temperatures/description/ )

 

 

풀이 방법 1. 

스택을 사용한다. 

결과로는 각 i번째 날 이후로 따뜻해지기 위해 기다려야 하는 일 수를 리턴해야 하므로, 스택 및 결과에는 인덱스 정보를 저장하는 것이 좋겠다. 

 

우선 큰 틀에서 for loop를 사용해서 temperatures의 전체 요소를 순회할 수 있도록 한다. 

각 시행의 값(i번째 날의 인덱스와 온도)을 가지고, 우선 스택에 값이 있는지를 확인한다. 

 

파이썬에서는 for문으로 리스트의 인덱스와 값을 동시에 순회하고 싶을 때 enumerate()를 사용할 수 있다. 

for index, temperature in enumerate(temperatures):
	# index: index
	# temperature: value

 

이후 스택의 가장 위에 있는 값보다 i번째 날의 온도가 더 높으면, while 문을 사용해서 하나씩 스택에서 제거한다. 

while stack and temperature > temperatures[stack[-1]]:
	# pop index of day from stack
	# assign value of index to answer list

 

* 해당 포스트는 <파이썬 알고리즘 인터뷰> 공부 후 정리 목적으로 작성되었습니다. *

 

leetcode 316번 ( https://leetcode.com/problems/remove-duplicate-letters/description/ )

 

풀이방법 1. 

재귀함수를 이용할 수 있다. 

중복 문자를 제거해서 알파벳 순서(lexicographical order)가 가장 빠르려면, 알파벳에서 앞에 오는 문자는 앞에, 뒤에 오는 문자는 뒤에 있어야 한다(당연). 

set() 함수를 사용하면 문자열이나 리스트의 원소들 중 중복을 제거해서 지정된 순서 없이 리턴한다. 

이후 sorted() 함수를 리스트에 사용하면 리스트의 원소들을 알파벳 순서에 맞게 정렬할 수 있다. 

for char in set(sorted(s)):

 

해당 풀이에서는 조건에 맞는 문자열을 하나씩 뒤에다가 붙여서 결과값을 리턴하는 방식으로 진행한다. 

 

즉 i번째 단계에서 계산 중인 정답 문자열에다가 새 문자열을 이어 붙인다고 해 보자. 

이어 붙이려는 문자열은 자신의 앞에만 포함되어 있는 다른 문자열이 있으면 안 된다. 

예를 들어 acbefjgk 라는 문자열에서 a 다음에 b라는 문자열을 이어 붙일 수는 없다. 왜냐하면 a와 b 사이에는 c라는 문자열이 있고, c는 b 다음에는 한 번도 등장하지 않으므로 반드시 a와 b 사이에 등장해야 하기 때문이다. 

 

이를 확인하려면 문자열 char의 앞에 있는 문자열이 문자열 char의 뒤에도 등장하는지를 확인해야 한다. 

index() 함수를 사용해서 전체 문자열 중 char이 등장하는 첫 번째 인덱스를 알아낼 수 있다. 

이후 문자열 슬라이싱을 사용해서 해당 인덱스 이후의 문자열을 따로 슬라이싱한 뒤, set()을 사용하면 중복에 상관 없이 어떤 문자열이 등장하는지를 알 수 있다. 

suffix = s[s.index(char):]	# suffix of char
if set(s) == set(suffix):	
	# do something
	return thisfunction(strs.replace(char, "")

 

만약 문자열 char의 앞에 있는 모든 문자열이 문자열 char의 뒤에도 등장한다면, 어차피 for loop는 알파벳의 정렬된 순서대로 진행되므로 해당 문자열 char을 정답 문자열에 추가할 수 있다. 

 

다만 문자열 char이 정답 문자열에 포함되었으므로, 재귀함수를 사용해서 계산될 파라미터 문자열에서는 char 문자열을 모두 공백으로 바꿔 주어야 하겠다. replace() 함수로 간단하게 처리할 수 있다. 

 

string.replace(대체될 문자열 A, 대체할 문자열 B) : string 안에 있는 문자열 A를 문자열 B로 바꾼다. 

 

풀이방법 2.

스택을 이용해서 풀 수 있다. 

 

기본적으로는 주어지는 문자열들을 스택에 쌓다가, 특정 조건에 해당하면 스택의 문자열들을 제거한다. 재귀함수와 방식은 유사하고 사용하는 자료형이 다르다고 볼 수 있다. 

 

이외에도 Counter 자료형을 이용해서, 주어진 문자열에 각 문자가 몇 번 등장하는지를 기록 후 사용한다. 

counter = collections.Counter(s)

 

또한 set 자료형을 사용해서, 이미 계산을 마친 문자열은 해당 집합에 추가해서 이후 같은 문자열이 나올 경우 계산하지 않고 다음 loop로 넘어간다(continue). 

 

스택의 문자열을 제거하는 필수 조건도 앞의 재귀 함수와 비슷하다. 

스택에 문자열이 남아 있고, 새로 추가되는 문자열이 스택의 맨 위 문자열보다 알파벳 순서가 빠르고, 스택의 맨 위 문자열이 이 이후에도 등장한다는 조건이 충족되면, 스택과 set에서 문자열을 제거한다. 

while stack and stack[-1] > char and counter[stack[-1]] > 0:
	sets.remove(stack.pop())

 

최종적으로 스택에 남은 문자열이 정답이 된다. 

리스트 자료형을 문자열로 변경하기 위해서 공백 문자열을 기준으로 join() 메소드를 사용하면 간단히 바꿀 수 있다. 

return "".join(stack)

 

* 해당 포스트는 <파이썬 알고리즘 인터뷰> 공부 후 정리 목적으로 작성되었습니다. *

 

leetcode 20번 ( https://leetcode.com/problems/valid-parentheses/description/ )

 

풀이방법 1. 

스택을 이용해서 풀이한다. 

 

파이썬에는 별도의 스택이나 큐 자료구조가 없다. 리스트 자료형으로도 스택의 push()와 pop(), 큐의 enqueue(), dequeue() 메소드를 사용할 수 있다. 

다만 리스트는 포인터가 1개라서 리스트의 맨 앞에서 원소를 제거하는 데는 O(n)이 걸리므로, 성능을 생각한다면 collections 라이브러리의 deque 자료형을 쓰는 것이 더 빠르다. 

 

괄호는 여는 문자 (, [, { 와 닫는 문자 ), ], } 로 이루어져 있다. 

여는 문자가 나오면 스택에 넣는다. 닫는 문자가 나오면 스택의 맨 위의 여는 문자와 비교한 뒤, 일치하는 쌍이면 맨 위에서부터 괄호를 제거하고, 일치하지 않으면 False를 리턴하면 된다. 

또는 문자열이 아직 남아 있는데 스택이 빌 경우도 False를 리턴한다. 

마지막에 모든 문자열에 대해서 비교작업을 한 뒤, 스택이 비어 있는 경우에만 True를 리턴한다. 

 

스택은 저번처럼 좌우가 똑같은 문자열(팰린드롬)을 비교하는 등의 좌우대칭 문제뿐만 아니라, 괄호 등의 쌍을 비교하는 문제에서도 사용된다. 비슷한 유형이 나온다면 스택을 고려해 보자. 

 

* 해당 포스트는 <파이썬 알고리즘 인터뷰> 공부 후 정리 목적으로 작성되었습니다. *

 

leetcode 5번 ( https://leetcode.com/problems/longest-palindromic-substring/ )

 

풀이방법 1.

투 포인터를 사용하면 된다. 보통 투 포인터는 입력의 양 끝에서 중앙으로 모이는 형태를 생각하지만, 반대로 특정 기준점에서 양쪽으로 퍼져 나가는 형태도 가능하다. 

 

left, right 포인터를 어느 지점을 기준으로 양쪽으로 퍼져 나갈지를 정하는 시작점이라고 보자. 

 

left 포인터는 문자열의 0번째 인덱스부터 len(string)-2번째 인덱스까지를 시작점으로 둘 수 있다. 반면 right 포인터는 문자열의 1번째 인덱스부터 len(string)-1번째 인덱스까지를 시작점으로 둘 수 있다. 

 

left, right 포인터를 각각 시작점으로 정했다면 해당 시작점을 기준으로 left 포인터는 왼쪽으로 한 칸씩, right 포인터는 오른쪽으로 한 칸씩 이동하며 확장한다. left와 right 포인터의 값이 같다면 계속 확장하고, 그렇지 않다면 지금까지의 left와 right 포인터 사이에 위치한 문자열을 반환한다. 

 

문자열을 확장하며 팰린드롬을 찾아서 리턴하는 로직과, 가장 큰 팰린드롬을 찾는 로직을 구분하면 코드가 더 간결할 것이다. 따라서 앞의 로직은 함수 내부의 함수로 따로 선언해 주자.

교재에서는 expand(left, right)라는 내부 함수로 선언했다. 

 

이때 문자열 슬라이싱 인덱스가 [a:b]일 경우, 실제로 반환되는 문자열은 a번째 인덱스부터 b-1번째 인덱스이므로 주의해야 한다. 

또한, left와 right 포인터의 현재 위치는 포함하지 않고, 그 사이에 있는 문자열만 리턴해야 하므로, expand() 함수는 다음과 같이 리턴한다. 

return string[left+1, right]

 

또한 팰린드롬의 길이는 홀수일 수도, 짝수일 수도 있다. 홀수일 경우 가운데 문자 하나로부터 양쪽으로 확장해야 하고, 짝수일 경우 나란히 있는 문자 두 개를 기준으로 양쪽으로 확장해야 한다. 

따라서 홀수 팰린드롬을 찾으려면 left와 right 포인터가 1칸 차이여야 하고, 짝수 팰린드롬을 찾으려면 두 포인터가 2칸 차이여야 하겠다. 

expand(i, i+1)	# odd
expand(i, i+2)	# even

 

문제에서는 가장 긴 팰린드롬을 반환하라고 했으므로, 가장 긴 팰린드롬을 저장할 변수도 필요하다. 

여러 개의 변수를 파라미터로 받을 수 있는 max() 함수를 사용하되 기준을 정하지 않으면 가장 긴 팰린드롬이 아니라 알파벳 순서 상 가장 먼저인 팰린드롬이 리턴될 것이다. 

따라서 max() 함수에도 key 파라미터 값으로 len 함수를 할당해서, 알파벳 순서가 아니라 팰린드롬의 길이를 기준으로 하도록 바꿔 준다. 

result = max(result, expand(i, i+1), expand(i, i+2), key=len)
return result

 

* 해당 포스트는 <파이썬 알고리즘 인터뷰> 공부 후 정리 목적으로 작성되었습니다. *

 

leetcode 49번 ( https://leetcode.com/problems/group-anagrams/ )

 

풀이방법 1. 

주어진 입력을 그대로 사용하지 않고 변형한 뒤 사용하면 쉽게 풀 수 있는 문제였다. 

특히 리스트가 주어졌지만 리스트의 인덱스를 리턴하는 문제가 아니라면 입력으로 주어진 리스트의 순서를 바꿔도 된다는 생각을 해 보면 좋겠다. 

 

문자열 A와 B가 서로 애너그램일 경우, 문자열 A의 모든 문자를 단 한 번씩 사용해서 문자열 B를 나타낼 수 있으며, 그 반대도 성립한다. 

서로 애너그램인 문자열들은 여러 개가 있을 수 있으며, 그 문자열들을 하나의 리스트로 묶기 위해서는 그 문자열들을 다른 문자열들과 구분할 수 있어야 한다. 

 

특정 애너그램을 구성하는 문자열들은 모두 같은 문자열로 구성되어 있다. 해당 문자열을 알파벳 순서로 정렬하면, 각 애너그램에 따른 unique한 값을 얻을 수 있다. 

즉 속한 애너그램이 다르면 문자열의 값이 다르게 나오기 때문에 서로 애너그램인지 아닌지를 구분할 수 있는 것이다. 

 

이 원리를 이용하고, dictionary 자료형을 사용하면 각 애너그램들을 묶어서 저장할 수 있다. 

key 값으로는 문자열을 정렬한 값이 할당되고, value 값으로는 해당 key 값으로 나타낼 수 있는 문자열 리스트가 할당된다. 

 

 

다만 문자열을 정렬하기 위해서 필요한 sort()와 sorted() 중, sorted()는 iterable 자료형은 모두 매개변수로 받을 수 있기 때문에 문자열도 매개변수로 받을 수 있다.

(각 인덱스 값을 돌아가면서 리턴하는 것이 가능하므로, 문자열도 iterable이다)

그러므로 sorted()를 사용해서 문자열을 리스트로 변환한 뒤, 다시 join을 사용해서 리스트를 문자열로 변환한다. 

 

다만 join은 A와 B를 합치는 함수이므로, 대상이 될 A 문자열이 필요하다. 

공백 문자열에다가 join을 사용하면 해당 리스트만 문자열로 반환할 수 있다. 

key = "".join(sorted(string))
dicts[key].append(string)

 

이후 values() 메소드를 사용해서 dictionary 안에 속한 모든 값 리스트를 리턴하면 된다. 

 

* 해당 포스트는 <파이썬 알고리즘 인터뷰> 공부 후 정리 목적으로 작성되었습니다. *

 

leetcode 819번 ( https://leetcode.com/problems/most-common-word/ )

 

풀이방법 1. 

리스트 컴프리헨션(list comprehension)과 Counter 자료구조를 사용한다. 

 

list comprehension

list comprehension이란 기존에 있는 리스트의 값을 사용해서 새로운 리스트를 만드는 것이다. 

우선 주어진 문자열을 대소문자로 변환하고 특수문자를 제거한 뒤, 문자열 리스트로 만드는 작업이 필요하다. 

그 이후에는 각 문자열이 banned_list에 있는 문자열이 아닌지를 확인해서 리스트에 추가 및 정렬하는 작업이 필요하다. 

 

list comprehension을 통해서 간단한 코드로 주어진 문자열을 조건에 맞는 문자열 리스트로 만들 수 있다. 

lists = [elem for elem in re.sub(r"[\W]", " ", string).lower().split() if elem not in banned_list]

 

해당 코드에서는 string의 문자 중 영문자가 아닌 문자들은 전부 공백으로 바꾸고, 모든 문자열을 소문자로 바꾼다. 이후 공백을 기준으로 나눠서 단어 리스트를 리턴한 뒤, 리스트 안의 각 단어가 banned_list에 없다면 lists에 추가하는 형태로 새 리스트를 만든다. 

 

lower() : 기본으로 제공되는 메소드로, 문자열의 모든 대문자를 소문자로 바꾼다. 

split(sep) : 문자열을 sep(문자)를 기준으로 나눠서 리스트 형태로 리턴한다. sep가 없을 경우 공백으로 나눈다. 

 

re.sub()에서 d, w, W 등 특정 문자는 \와 결합될 경우 단순 알파벳이 아니라 포괄적인 의미로 사용된다. 

\d : 모든 숫자

\w : 모든 영문자

\W : 영문자가 아닌 모든 문자(\w의 반대)

 

counter

Counter 객체는 dictionary의 하위 클래스이며 collections 라이브러리를 통해 사용할 수 있다.

dictionary 클래스를 상속받아서 대부분의 기능은 유사하지만, 몇 가지의 차이점이 있다. 

words = ["hi", "hello", "heee"]
counter = collections.Counter(words)

 

Counter 객체를 초기화할 때 매개변수로 리스트를 받을 수 있다. 그러면 리스트 내부의 각 단어가 key, 해당 단어의 등장 빈도 수가 value로 저장된다. 즉 단어 리스트를 Counter의 매개변수로 넣은 뒤 특정 단어의 value 값을 조회하면 해당 단어가 몇 번 등장했는지를 알 수 있다. 

 

또한 기존의 dictionary는 찾는 단어(key)가 없을 경우 ValueError을 발생시켰지만 collections.defaultdict()은 None을, collections.Counter()은 0을 리턴한다. 

 

또한 Counter의 most_common(n) 메소드를 사용하면 빈도수가 가장 높은 단어를 최대 n쌍 반환한다. 리스트 형태로 반환하며, 각 원소는 tuple 형태이다.

이를 이용해서 가장 빈도수가 높은 단어 하나를 리턴할 수 있다. 

return counter.most_common(1)[0][0]

 

 

참고한 포스트

https://docs.python.org/3/library/collections.html#collections.Counter

https://www.w3schools.com/python/python_lists_comprehension.asp

 

 

 

+ Recent posts