
알고리즘 문제를 이미 배운 개념들을 적극적으로 활용하여 풀었다. 개념을 배우고 실전에 적용해보니까 기억에도 오래 남을 것 같고 앞으로 문제푸는데 나만의 가이드라인이 잡혀가는 것 같다.
실습 설명
마이크로하드에서 일하는 영훈이는 워드 프로세서 팀의 개발자로 일하고 있습니다. 어느날 샘 게이츠 사장님은 영훈이에게 워드 프로세서에서 이용자가 작성한 글에 짝이 없는 모든 괄호를 찾아내서 이용자에게 출력할 수 있는 기능을 추가하라는 특명을 받았습니다. 이 기능은 함수로 만들어 주라고 하는데요.
함수 parentheses_checker()는 인풋으로 문자열 파라미터 string을 받습니다.
parentheses_checker()는 string 안에 있는 모든 짝이 없는 괄호를 찾아내서 이 괄호들이 문자열 어디 위치에 있는지를 출력합니다. 만약 문자열 안에 모든 괄호가 짝이 있으면 함수는 아무 내용도 출력하지 않습니다.
스택이란 가장 뒤에 데이터를 추가할 수 있고 가장 마지막 데이터를 삭제할 수 있는 추상 자료형이라고 배웠죠. 한 번 파이썬 deque를 추상 자료형 스택으로 이용해서 이 함수를 작성해 봅시다!
실습 결과
테스트하는 문자열: (1+2)*(3+5)
테스트하는 문자열: ((3*12)/(41-31))
테스트하는 문자열: ((1+4)-(3*12)/3
문자열 0 번째 위치에 있는 괄호가 닫히지 않았습니다
테스트하는 문자열: (12-3)*(56/3))
문자열 13 번째 위치에 있는 닫는 괄호에 맞는 열리는 괄호가 없습니다
테스트하는 문자열: )1+14)/3
문자열 0 번째 위치에 있는 닫는 괄호에 맞는 열리는 괄호가 없습니다
문자열 5 번째 위치에 있는 닫는 괄호에 맞는 열리는 괄호가 없습니다
테스트하는 문자열: (3+15(*3
문자열 5 번째 위치에 있는 괄호가 닫히지 않았습니다
문자열 0 번째 위치에 있는 괄호가 닫히지 않았습니다
나의 풀이(로직)
1. 스트링을 돌면서 (문자가 나오면 스택에 push한다.
2. )문자가 나오면 스택을 확인한다.
3. 스택이 비어있으면 오류(여는 괄호 없이 닫는 괄호가 먼저 나왔다는 뜻)
4. 스택이 비어있지 않다면 pop하여 맞는 짝을 없애준다
5. 문자열 순회가 끝나고, 스택이 빌 때까지 pop하면서 닫는 괄호가 없는 여는 괄호들을 처리해준다.
def parentheses_checker(string):
"""주어진 문자열 인풋의 모든 괄호가 짝이 있는지 확인해주는 메소드"""
print(f"테스트하는 문자열: {string}")
stack = deque([]) # 사용할 스택 정의
# 여기에 코드를 작성하세요
for i in range(len(string)):
if string[i] == "(":
stack.append(i)
elif string[i] == ")":
if stack:
_ = stack.pop()
else:
print(f"문자열 {i} 번째 위치에 있는 닫는 괄호에 맞는 열리는 괄호가 없습니다")
while stack:
i = stack.pop()
print(f"문자열 {i} 번째 위치에 있는 괄호가 닫히지 않았습니다")
key-value 쌍을 저장하는 추상 자료형
grades = {} # 딕셔너리 생성
# key-value 데이터 삽입
grades["현승"] = 80
grades["태호"] = 60
grades["영훈"] = 90
grades["지웅"] = 70
grades["동욱"] = 96
# 딕셔너리 출력
print(grades)
# 값 update
grades["태호"] = 100
# key를 이용한 탐색
print(grades["동욱"])
# key를 이용한 삭제
grades.pop("태호")
grades.pop("지웅") 
Set는 중복을 허용하지 않는 데이터들을 저장하는 추상 자료형
삽입: 데이터를 저장(중복 허용하지 않음)
탐색: 데이터가 set에 저장되어 있는지 확인할 수 있다
삭제: 저장한 데이터를 지울 수 있다
데이터간 순서 관계를 약속하지 않는다
파이썬의 set은 다음과 같이 사용한다.
finished_class = set()
# 데이터 저장
finished_class.add("자료 구조")
finished_class.add("알고리즘")
finished_class.add("프로그래밍 기초")
finished_class.add("인터랙티브 웹")
finished_class.add("데이터 사이언스")
print(finished_class)
# 데이터 탐색
print("컴퓨터 개론" in finished_class) # False
print("자료 구조" in finished_class) # True
# 데이터 삭제
finished_class.remove("자료 구조")
finished_class.remove("알고리즘")

set의 구현
set은 보통 해시테이블을 이용하여 구현한다. 데이터를 저장할 때 처음부터 key만 저장하면, 중복을 허용하지 않고 데이터를 저장할 수 있다. value를 저장하지 않아도 아무 문제가 없다. 탐색, 삭제, 삽입 등은 key를 가지고 하는 연산이므로 이 역시 문제가 없다.
문제 설명
태호는 주식 분석이 취미입니다.
요즘 제일 핫한 종목은 삼송전자인데요. 삼송전자의 주식을 딱 한 번 사고 팔았다면 최대 얼마의 수익이 가능했을지 궁금합니다. 그것을 계산해 주는 함수 max_profit()을 작성하세요.
max_profit()은 파라미터로 일별 주식 가격이 들어 있는 리스트 stock_prices를 받고 최대 수익을 리턴합니다. 주식은 딱 한 번만 사고 한 번만 팝니다. 그리고 사는 당일에 팔 수는 없습니다.
하나의 예시로, 지난 6일간 삼송전자의 주식 가격이 이렇다고 가정합시다.
max_profit([7, 1, 5, 3, 6, 4])
이 기간 동안 낼 수 있는 최대 수익은 얼마일까요? 둘째 날 11에 사서 다섯째 날 66에 팔면 총 55의 수익이 생깁니다.
또 다른 예시를 봅시다.
max_profit([7, 6, 4, 3, 1])
이 기간 동안 가능한 최대 수익은 얼마일까요? 이번에는 주식이 계속 떨어지기만 하네요. 하지만 꼭 한 번은 사고 팔아야 하기 때문에, 첫 날 77에 사고 둘째 날 66에 팔아서 나오는 −1−1이 최대 수익입니다.
나의 풀이 1 - 재귀함수 활용
base case
로직
코드
def max_profit(stock_list):
if len(stock_list) == 3:
case_1 = stock_list[1] - stock_list[0]
case_2 = stock_list[2] - stock_list[0]
case_3 = stock_list[2] - stock_list[1]
return max(case_1, case_2, case_3)
if len(stock_list) == 2:
return stock_list[1] - stock_list[0]
if len(stock_list) <= 1:
return 0
mid = len(stock_list) // 2
left = stock_list[:mid]
right = stock_list[mid:]
profit = max(right) - min(left)
return max(profit, max_profit(left), max_profit(right))
모범 답안 -
로직
코드
def max_profit(stock_list):
# 여기에 코드를 작성하세요
min_so_far = stock_list[0]
max_profit_so_far = stock_list[1] - stock_list[0]
for i in range(1, len(stock_list)):
max_profit_so_far = max(max_profit_so_far, stock_list[i] - min_so_far)
min_so_far = min(min_so_far, stock_list[i])
return max_profit_so_far
실습 설명
영훈이는 출근할 때 계단을 통해 사무실로 가는데요. 급할 때는 두 계단씩 올라가고 여유 있을 때는 한 계단씩 올라 갑니다.
어느 날 문득, 호기심이 생겼습니다. 한 계단 또는 두 계단씩 올라가서 끝까지 올라가는 방법은 총 몇 가지가 있을까요?
계단 4개를 올라간다고 가정하면, 이런 방법들이 있습니다.
1, 1, 1, 1
2, 1, 1
1, 2, 1
1, 1, 2
2, 2
총 5개 방법이 있네요.
함수 staircase()는 파라미터로 계단 수 n을 받고, 올라갈 수 있는 방법의 수를 효율적으로 찾아서 리턴합니다.
print(staircase(0)) # => 1
print(staircase(1)) # => 1
print(staircase(4)) # => 5
나의 풀이 ()
로직
코드
def staircase(n):
# 여기에 코드를 작성하세요
dp = [0,1,2]
if n <= 2:
return dp[n]
dp.extend([0] * (n-2))
for i in range(3, n+1):
dp[i] = dp[i - 1] + dp[i - 2]
return dp[n]
답이 틀렸다고 나온다. 알고 보니 staircase(0)은 1이 나와야 한다고 한다..
실습 설명
영훈이는 출근할 때 계단을 통해 사무실로 가는데요. 급할 때는 두 계단씩 올라가고 여유 있을 때는 한 계단씩 올라갑니다. 결국 계단을 오를 수 있는 모든 방법으로 계단을 올라갔는데요.
이제 다르게 계단을 올라갈 수는 없을까 고민하던 영훈이는 특이한 방법으로 계단을 오르려고 합니다.
가령 계단을 한 번에 1, 2, 4 칸씩 올라가 보는 건데요. 예를 들어서 계단을 4개를 올라가야 되면:
1, 1, 1, 1
2, 1, 1
1, 2, 1
1, 1, 2
2, 2
4
총 6개 방법이 있네요.
함수 staircase()는 파라미터로 총 계단 수 n 그리고 한 번에 올라갈 수 있는 계단 수를 possible_steps로 받고, 올라갈 수 있는 방법의 수를 효율적으로 찾아서 리턴합니다.
그러니까 n이 3, possible_steps 가 [1, 2, 3]이면, 계단 총 3칸을 1, 2, 3칸씩 갈 수 있을 때 오르는 방법의 수를 구하는 거죠. 단, possible_steps에는 항상 1이 포함된다고 가정합니다.
나의 풀이 ()
로직
코드
# 높이 n개의 계단을 올라가는 방법을 리턴한다
def staircase(n, possible_steps):
# 여기에 코드를 작성하세요
dp = [1, 1]
if n <= 1:
return dp[n]
dp.extend([0] * (n - 1))
for i in range(2, n+1):
for step in possible_steps:
dp[i] += dp[i-step] if step <= i else 0
return dp[n]
트리란 데이터간에 계층적 관계가 있을 때 데이터 자체와 더불어 계층적 관계를 함께 저장할 수 있는 자료구조이다. 트리는 링크드 리스트와 마찬가지로 데이터가 노드 형태로 저장된다. 노드는 다음과 같이 자식 노드에 대한 레퍼런스를 가진다.


각 노드마다 자식 노드를 2개 이하만 가지는 트리
구현
아래의 트리를 만들어 본다.

class Node:
def __init__(self, data):
self.data = data
self.left_child = None
self.right_child = None
# 노드 인스턴스 생성
root_node = Node(2)
node_B = Node(3)
node_C = Node(5)
node_D = Node(7)
node_E = Node(11)
# B와 C노드를 루트 노드의 자식으로 지정
root_node.left_child = node_B
root_node.right_child = node_C
# D와 E를 B노드의 자식으로 지정
node_B.left_child = node_D
node_B.right_child = node_E
이진 트리 종류
모든 노드가 0개 혹은 2개의 자식을 갖는 트리

마지막을 레벨을 제외한 모든 레벨이 꽉 차있고, 마지막 레벨은 왼쪽 부터 오른쪽 방향으로 차 있는 트리

모든 레벨이 빠짐없이 다 노드로 채워져있는 이진 트리

완전 이진 트리 구현
배열로 표현한 완전 이진 트리의 기능들을 구현해본다.
get_parent_index: 부모 노드의 인덱스 리턴
get_left_child_index: 왼쪽 자식 인덱스 리턴
get_right_child_index: 오른쪽 자식 인덱스 리턴
def get_parent_index(complete_binary_tree, index):
"""배열로 구현한 완전 이진 트리에서 index번째 노드의 부모 노드의 인덱스를 리턴하는 함수"""
# 여기에 코드를 작성하세요
if index == 1:
return None
return index // 2 if complete_binary_tree[index // 2] is not None else None
def get_left_child_index(complete_binary_tree, index):
"""배열로 구현한 완전 이진 트리에서 index번째 노드의 왼쪽 자식 노드의 인덱스를 리턴하는 함수"""
# 여기에 코드를 작성하세요
try:
if complete_binary_tree[index * 2]:
return index * 2
except:
return None
def get_right_child_index(complete_binary_tree, index):
"""배열로 구현한 완전 이진 트리에서 index번째 노드의 오른쪽 자식 노드의 인덱스를 리턴하는 함수"""
# 여기에 코드를 작성하세요
try:
if complete_binary_tree[index * 2 + 1]:
return index * 2 + 1
except:
return None
정의: 트리를 순회하며 모든 데이터를 한 번씩 거쳐가는 것
호출: traverse(root_node)
트리 순회에는 다음과 같이 3가지 방법이 있다.
순회 방식에 따라 각 노드의 방문 순서가 달라지며, 저장된 구조에 따라 선형적인 관계또한 사용가능하다. 예를 들어 다음과 같은 트리를 in-order방식으로 순회하면 알파벳 순서대로 노드를 방문하게 된다.

다음은 힙의 조건이자 속성이다.
힙의 특성을 이용하면 정렬문제를 효율적으로 풀 수 있다. 정렬되지 않은 데이터들을 완전 이진 트리 형태로 저장 후 해당 트리를 힙으로 만드는 것을 통해 시간 복잡도 로 정렬할 수 있다.
완전 이진 트리에 속한 하나의 노드에 대해서 힙 속성을 지키도록 자식들과 비교하며 자기 자신의 위치를 변경하는 함수
코드
def swap(tree, index_1, index_2):
"""완전 이진 트리의 노드 index_1과 노드 index_2의 위치를 바꿔준다"""
temp = tree[index_1]
tree[index_1] = tree[index_2]
tree[index_2] = temp
def heapify(tree, index, tree_size):
"""heapify 함수"""
def get_max_index(i, l_i, r_i):
_max = (i, tree[i])
for index in [l_i, r_i]:
if tree[index] > _max[1]:
_max = (index, tree[index])
return _max[0]
# 왼쪽 자식 노드의 인덱스와 오른쪽 자식 노드의 인덱스를 계산
left_child_index = 2 * index
right_child_index = 2 * index + 1
# 여기에 코드를 작성하세요
while True:
if left_child_index > tree_size-1: #자식이 없음
break
elif right_child_index > tree_size-1: # 왼쪽 자식만 있음
if tree[index] > tree[left_child_index]:
swap(tree, index, left_child_index)
break
else:
max_index = get_max_index(index, left_child_index, right_child_index)
if max_index == index:
break
swap(tree, index, max_index)
index = max_index
left_child_index = index * 2
right_child_index = index * 2 + 1
힙을 만든다
root와 마지막 노드를 바꾼다
바꾼 노드는 없는 인덱스 취급을 한다
새로운 노드가 힙 속성을 지킬 수 있게(자리를 찾을 수 있게) heapify를 호출한다
def swap(tree, index_1, index_2):
"""완전 이진 트리의 노드 index_1과 노드 index_2의 위치를 바꿔준다"""
temp = tree[index_1]
tree[index_1] = tree[index_2]
tree[index_2] = temp
def heapify(tree, index, tree_size):
"""heapify 함수"""
# 왼쪽 자식 노드의 인덱스와 오른쪽 자식 노드의 인덱스를 계산
left_child_index = 2 * index
right_child_index = 2 * index + 1
largest = index # 일단 부모 노드의 값이 가장 크다고 설정
# 왼쪽 자식 노드의 값과 비교
if 0 < left_child_index < tree_size and tree[largest] < tree[left_child_index]:
largest = left_child_index
# 오른쪽 자식 노드의 값과 비교
if 0 < right_child_index < tree_size and tree[largest] < tree[right_child_index]:
largest = right_child_index
if largest != index: # 부모 노드의 값이 자식 노드의 값보다 작으면
swap(tree, index, largest) # 부모 노드와 최댓값을 가진 자식 노드의 위치를 바꿔준다
heapify(tree, largest, tree_size) # 자리가 바뀌어 자식 노드가 된 기존의 부모 노드를대상으로 또 heapify 함수를 호출한다
def heapsort(tree):
"""힙 정렬 함수"""
tree_size = len(tree)
# 힙으로 만들기
for i in range(tree_size-1, 0, -1):
heapify(tree, i, tree_size)
# 마지막 노드부터 순서대로 루트노드와 맞바꾼뒤 heapify
for i in range(1, tree_size-1):
# print(tree_size- i)
swap(tree, 1, tree_size - i)
heapify(tree, 1, tree_size - i)
힙에 마지막 인덱스에 데이터를 삽입한다
삽입한 데이터와 부모 노드의 데이터를 비교한다
부모 노드의 데이터가 더 작으면 둘의 위치를 바꿔준다
새로 삽입한 노드가 제 위치를 찾을 때까지 반복한다
def reverse_heapify(tree, index):
"""삽입된 노드를 힙 속성을 지키는 위치로 이동시키는 함수"""
parent_index = index // 2 # 삽입된 노드의 부모 노드의 인덱스 계산
# 여기에 코드를 작성하세요
while True:
if index <= 1: # 루트 노드인 경우(부모 노드가 없는 경우)
break
else:
if tree[index] > tree[parent_index]:
swap(tree, index, parent_index)
index = parent_index
parent_index //= 2
class PriorityQueue:
"""힙으로 구현한 우선순위 큐"""
def __init__(self):
self.heap = [None] # 파이썬 리스트로 구현한 힙
def insert(self, data):
"""삽입 메소드"""
# 여기에 코드를 작성하세요
self.heap.append(data)
index = len(self.heap) - 1
reverse_heapify(self.heap, index)