Skip to content

[river20s] WEEK 06 solutions #1419

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 5 commits into from
May 10, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 34 additions & 0 deletions container-with-most-water/river20s.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
class Solution:
def maxArea(self, height: List[int]) -> int:
# 시작과 끝 선분을 포인터로 두고
# 두 선분으로 만들 수 있는 넓이:
# 너비 = right - left
# 높이 = min(height[left], height[right])
# 넓이 = 너비 * 높이의 최대 값을 구하는 문제
# Time Complexity: O(n)
# 두 포인터가 한 칸씩만 이동하며 서로 만날 때 루프 종료
# Space Complexity: O(1)
n = len(height)
left = 0 # 왼쪽(시작) 포인터
right = n - 1 # 오른쪽(끝) 포인터
max_area = 0

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

문제풀이에 대한 주석을 상세히 적어주셔서 코드를 이해하기 쉬웠습니다. 👍

while left < right:
# 현재 높이는 두 직선 중 낮은 쪽
current_height = min(height[left], height[right])
# 현재 너비는 오른쪽 점과 왼쪽 점의 차
current_width = right - left
# 넓이 = 높이 * 너비
current_area = current_height * current_width
# 최대 넓이라면 업데이트
max_area = max(max_area, current_area)
# 포인터 이동 후 탐색
# 둘 중 더 낮은 쪽의 포인터를 안으로 움직여서 넓이 계산
# 더 큰 넓이를 찾는 것이 목표, 포인터를 안으로 움직이면 너비는 무조건 감소
# 높이라도 증가할 가능성이 있어야 하므로 기존 낮은 높이가 늘어날 가능성에 배팅
# 둘이 같다면 오른쪽 이동(아무쪽이나 가능)
if height[left] < height[right]:
left += 1
else:
right -= 1
return max_area
57 changes: 57 additions & 0 deletions design-add-and-search-words-data-structure/river20s.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
class TrieNode:
def __init__(self):
self.children = {}
self.isEndOfWord = False
# <<<--- 트라이 자료 구조 사용을 위한 구현 ---<<<
# >>>----------------- 답안 ---------------->>>
class WordDictionary:

def __init__(self):
self.root = TrieNode()

def addWord(self, word: str) -> None:
# 추가: word를 자료 구조에 추가
# 일반적인 트라이 삽입 로직과 같음
# Time Complexity: O(L), Space Complexity: O(L)
# L은 추가할 단어 길이
currentNode = self.root
for char in word:
if char not in currentNode.children:
currentNode.children[char] = TrieNode()
currentNode = currentNode.children[char]
currentNode.isEndOfWord = True

def search(self, word: str) -> bool:
# 검색: 자료 구조에 word와 일치하는 문자열이 있으면 True
# 그렇지 않으면 False 반환
# 와일드카드 '.'는 어떤 글자도 될 수 있음
# Time Coplexity: O(M), Space Complexity: O(M)
# M은 검색할 단어 길이

def _dfs_search(node: TrieNode, index_in_search_word: int) -> bool:
# 재귀를 위한 헬퍼 함수
# --- 베이스 케이스 ---
# 검색에 끝에 도달했을 때 트라이 노드가 마지막 노드라면 검색 성공
if index_in_search_word == len(word):
return node.isEndOfWord

# 트라이의 현재 노드와 비교해야할 검색어 word의 문자
char_to_match = word[index_in_search_word]

# 현재 문자가 알파벳인 경우
if char_to_match != ".":
# 자식 노드에 비교할 문자가 없는 경우
if char_to_match not in node.children:
return False
# 다음 문자에 대한 재귀 호출
return _dfs_search(node.children[char_to_match], index_in_search_word + 1)

# 현재 문자가 '.'인 경우
else:
# 가능한 모든 자식 노드에 대해 재귀 호출
for child_node in node.children.values():
if _dfs_search(child_node, index_in_search_word + 1):
return True # 하나라도 성공하면 True
return False
# 최초 재귀 호출
return _dfs_search(self.root, 0)
82 changes: 82 additions & 0 deletions longest-increasing-subsequence/river20s.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
# import bisect
from typing import List
class Solution():
def _my_bisect_left(self, a: List[int], x:int) -> int:
"""
정렬된 리스트 a에 x를 삽입할 가장 왼쪽 인덱스 반환
"""
# x보다 크거나 같은 첫 번째 원소의 인덱스 찾아주기
# bisect.bisect_left(a, x)를 직접 구현
low = 0 # 시작 인덱스, 0으로 초기화
high = len(a) # 끝 인덱스, 리스트의 길이로 초기화

while low < high:
mid = low + (high - low) // 2
if a[mid] < x:
low = mid + 1
else: # a[mid] >= x
high = mid
return low # low가 삽입 지점

def lengthOfLIS(self, nums: List[int]) -> int:
"""
"""
if not nums: # nums가 빈 배열이라면 먼저 0 반환
return 0
tails = [] # 각 길이의 LIS를 만들 수 있는 가장 작은 마지막 값을 저장할 리스트
for num in nums:
# num을 삽입할 수 있는 가장 왼쪽 위치 찾기
# -> tails 리스트에서 num 보다 크거나 같은 첫 번째 요소 인덱스 찾기
# -> 만약 tails 모든 값보다 크다면 추가할 위치를 반환함
idx = self._my_bisect_left(tails, num)

# 만약 idx가 tails 현재 길이와 같다면
# -> num이 tails의 모든 요소보다 크다
# -> tails에 num "추가"
if idx == len(tails):
tails.append(num)
# 만약 idx가 tails 현재 길이보다 작다면
# -> 가능성 발견, tails[idx]를 num으로 "교체"
# -> LIS를 개선
else:
tails[idx] = num

# 모든 숫자 처리 후, tails 길이가 LIS길이가 됨
return len(tails)


"""
def lengthOfLIS_bruteforce(self, nums: List[int]) -> int:
# 초기에 구현한 탐욕 기반 알고리즘
# --- 복잡도 ---
# Time Complexity: O(n^2)
# Space Complexity: O(1)
# --- 한계 ---
# [지금 당장 가장 좋아보이는 선택이 전체적으로는 최적이 아닌 경우]
# nums = [0,1,0,3,2,3] 같은 경우 답을 찾지 못함 (예측 값: 4, 실제 값: 3)
# 각 요소에서 시작하여 탐욕적으로 다음 큰 수를 찾아가는 알고리즘으로 모든 LIS를 찾을 수 없음
# 각 시작점에서 하나의 증가 부분 수열을 찾는 것, 항상 긴 것을 보장할 수 없음
# 이미 찾은 LIS 뒤에 더 긴 LIS가 있다면 찾을 수 없음
n = len(nums) # n은 nums의 길이(개수)
if n == 0: # 길이가 0이면 우선적으로 0을 반환
return 0

max_overall_length = 0 # 전체 최대 길이, 0으로 초기화

for idx_i in range(n): # 0부터 n까지 순회하는 외부 루프
current_length = 1 # 현재(index: idx_i) 부분 수열의 길이를 1로 초기화
last_element_in_subsequence = nums[idx_i] # 부분 수열의 마지막 숫자를 현재 숫자로 초기화

for idx_j in range(idx_i + 1, n): # 다음 요소의 인덱스를 idx_j로 하여 끝까지 순회
# 만약 다음 요소가 부분 수열의 마지막 숫자보다 크다면
if nums[idx_j] > last_element_in_subsequence:
# 마지막 숫자를 다음 요소로 갱신
last_element_in_subsequence = nums[idx_j]
# 현재 부분 수열의 길이를 1 증가
current_length += 1

# 현재 부분 수열 길이가 전체 부분 수열 길이보다 큰 경우 갱신
max_overall_length = max(max_overall_length, current_length)

return max_overall_length
"""
57 changes: 57 additions & 0 deletions valid-parentheses/river20s.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
class Stack:
def __init__(self):
self.data = []

def is_empty(self):
return len(self.data) == 0

def push(self, element):
self.data.append(element)

def pop(self):
if not self.is_empty():
return self.data.pop()
else:
return None

def peek(self):
if not self.is_empty():
return self.data[-1]
else:
return None
# <<<--- Stack 구현 ---<<<
# >>>--- 답안 Solution --->>>
class Solution:
# 스택을 활용해 괄호 유효 검사
# Time Complexity: O(n)
# Space Complexity: O(n)
def __init__(self):
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Solution 클래스의 __init__ 메서드에서 정의한 괄호 맵핑은 클래스 변수로 선언해도 좋을 것 같습니다.

self._opening_brackets = '([{'
self._closing_brackets = ')]}'
self._matching_map = {')': '(', ']': '[', '}': '{'}

def isValid(self, s: str) -> bool:
stack = Stack()
for char in s:
# 여는 괄호라면 스택에 push
if self._is_opening(char):
stack.push(char)
# 닫는 괄호라면
# 마지막 열린 괄호와 유형 일치 확인
elif self._is_closing(char):
if stack.is_empty():
# 스택이 비어 있으면 False 반환
return False
last_open_bracket = stack.pop()
if not self._is_match(last_open_bracket, char):
return False
return stack.is_empty()

def _is_opening(self, char: str) -> bool:
return char in self._opening_brackets

def _is_closing(self, char: str) -> bool:
return char in self._closing_brackets

def _is_match(self, open_bracket: str, close_bracket: str) -> bool:
return self._matching_map.get(close_bracket) == open_bracket
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

코드가 체계적이고 객체지향적인 방식으로 잘 작성하신것 같아요. Stack 클래스를 직접 구현하고, Solution 클래스에서 이를 활용한 점이 인상적입니다. 👍

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

_is_match 메서드에서 get 메서드를 사용했는데, 이미 _is_closing에서 유효한 괄호임을 확인했다면 직접 접근해도 안전할 것 같습니다. 😀