diff --git a/best-time-to-buy-and-sell-stock/taurus09318976.py b/best-time-to-buy-and-sell-stock/taurus09318976.py new file mode 100644 index 000000000..0a51bc818 --- /dev/null +++ b/best-time-to-buy-and-sell-stock/taurus09318976.py @@ -0,0 +1,50 @@ +''' +- 이 문제는 최저가에 한 주식을 사서, 그 이후에 최대가에 팔아야 하는 문제임 +- 만약 이익을 낼 수 없으면 0을 반환함 +조건 : +1) 반드시 한 번만 사고 한 번만 팔아야 하며, 팔기 전에 반드시 사야 함 +2) 팔 때는 구매한 날 이후의 날짜여야 합니다. + +Example 1. 의 경우 +날짜 (index)| 가격 (price) | min_price (최저가) | current_profit (현재 이익) | max_profit (최대 이익) +0 7 7(초기값) - 0(초기값) +1 1 7 -> 1 - 0 +2 5 1 5 - 1 = 4 0 -> 4 +3 3 1 3 - 1 = 2 4 +4 6 1 6 - 1 = 5 4 -> 5 +5 4 1 4 - 1 = 3 5 + +''' +class Solution: + def maxProfit(self, prices: List[int]): + # 입력 배열이 비어 있으면 주가가 없으므로 거래할 수 없음. 따라서 최대 이익은 0 + if not prices: + return 0 + + # 배열의 첫 번째 가격을 최저가로 초기화함. 이후 가격과 비교할 기준점이 됨 + min_price = prices[0] + # 이익이 없으면 0을 반환하기 위해 최대 이익을 0으로 초기화함 + max_profit = 0 + + for price in prices[1:]: # 두 번째 가격부터 순회. 첫번째 가격은 이미 min_price에 할당했으므로 제외함. + if price < min_price: # 현재 가격이 최저가보다 작으면 갱신. 더 싸게 살 수 있는 날을 찾는 과정임 + min_price = price + else: + profit = price - min_price # 현재 가격에서 최저가를 빼서, 지금 팔면 얻을 수 있는 이익 계산 + if profit > max_profit: # 계산한 이익이 기존의 최대 이익보다 크면 max_profit 갱신 + max_profit = profit + + return max_profit # 반복이 끝나면 최대 이익을 반환 + + + ''' + 시간 복잡도 : O(n) + 배열을 한 번만 순회하기 때문에 입력 크기 n에 비례함 + n은 prices 배열의 길이 + + 공간 복잡도 : O(1) + 추가로 사용하는 변수는 min_price, max_profit, profit 3개뿐임(상수 개수), + 입력 크기에 따라 달라지지 않음 + 입력 배열 외에 별도의 저장 공간이 필요하지 않음 + ''' + diff --git a/encode-and-decode-strings/taurus09318976.py b/encode-and-decode-strings/taurus09318976.py new file mode 100644 index 000000000..8c0c4f10d --- /dev/null +++ b/encode-and-decode-strings/taurus09318976.py @@ -0,0 +1,47 @@ +''' +목표 : 문자열 리스트를 하나의 문자열로 인코딩하고, 다시 원래 리스트로 디코딩하는 알고리듬을 만드는 문제임 +핵심 : 문자열에 어떤 문자가 들어 있더라도 정확히 인코딩/디코딩할 수 있어야 함 +해결법 : 각 문자열 앞에 길이를 붙여서 인코딩 함 + +Example 1. 단계별 설명 +입력: ["lint","code","love","you"] + +인코딩 과정: +"lint" → 4:lint +"code" → 4:code +"love" → 4:love +"you" → 3:you + +최종 인코딩 문자열: 4:lint4:code4:love3:you + +디코딩 과정: +i=0 → : 위치 1, 길이 4 → 문자열 lint (i=5) +i=5 → : 위치 6, 길이 4 → 문자열 code (i=10) +i=10 → : 위치 11, 길이 4 → 문자열 love (i=15) +i=15 → : 위치 16, 길이 3 → 문자열 you (i=19) + +최종 결과: ["lint","code","love","you"] + +동작 원리 요약 +인코딩: 각 문자열 앞에 길이를 붙여서 혼동 없이 디코딩 가능함 +디코딩: 길이 정보를 이용해 정확히 문자열을 추출함 +''' + + +class Solution: + def encode(self, strs): + encoded = [] + for s in strs: + encoded.append(f"{len(s)}:{s}") # 각 문자열 앞에 길이 + ":"을 붙임 + return "".join(encoded) # 모든 문자열을 하나로 합침 + + def decode(self, s): + decoded = [] + i = 0 + while i < len(s): + colon = s.find(":", i) # s.find(":", i) : 현재 위치(i)부터 처음 나오는 :의 위치를 찾기 + length = int(s[i:colon]) # int(s[i:colon]) : 앞의 숫자를 문자열 길이로 변환 + i = colon + 1 # 문자열 시작 위치로 이동 + decoded.append(s[i:i+length]) # s[i:i+length : 길이만큼 문자열을 잘라서 저장함. + i += length # 다음 문자열의 시작 위치로 이동 + return decoded diff --git a/group-anagrams/taurus09318976.py b/group-anagrams/taurus09318976.py new file mode 100644 index 000000000..c06e0527d --- /dev/null +++ b/group-anagrams/taurus09318976.py @@ -0,0 +1,43 @@ +''' +이 문제는 정렬된 문자열을 키(key)로 사용해 같은 애너그램끼리 그룹화함 +즉, 각 단어를 정렬했을 때 같은 결과가 나오면 같은 그룹임 + +["eat","tea","tan","ate","nat","bat"] +단어 | 정렬 결과 | 키 | groups 딕셔너리 변화 +eat aet aet {'aet': ['eat']} +tea aet aet {'aet': ['eat', 'tea']} +tan ant ant {'aet': [...], 'ant': ['tan']} +ate aet aet {'aet': ['eat','tea','ate'], ...} +nat ant ant {'ant': ['tan','nat'], ...} +bat abt abt {'abt': ['bat'], ...} + +''' +class Solution: + def groupAnagrams(self, strs: List[str]): + groups = defaultdict(list) # defaultdict : 키가 없어도 자동으로 빈 리스트 생성 + + for word in strs: # 각 단어를 하나씩 확인 + sorted_word = ''.join(sorted(word)) # 단어를 정렬해 키 생성 (예: "tea" → "aet") + groups[sorted_word].append(word) # 같은 키를 가진 단어를 그룹에 추가 + + return list(groups.values()) # 그룹들을 리스트로 변환해 반환 + +''' + 시간 복잡도: O(n × klogk) + O(n) = O(n × klogk) + n: 단어 개수 + k: 단어의 최대 길이 + 각 단어 정렬에 O(k log k) 시간 소요 (ex. 5글자 → 5 log 5 ≈ 11) + 단어 정렬: 각 단어를 정렬하는 데 O(k log k) 시간이 소요됩니다. + 파이썬의 sorted() 함수는 내부적으로 Timsort 알고리즘을 사용하며, + 이의 시간 복잡도는 평균적으로 O(k log k)임 + n개의 단어 처리: 모든 단어에 대해 정렬을 수행하므로 n × O(k log k) = O(n × k log k)610. + 딕셔너리 연산: 정렬된 키를 기반으로 단어를 그룹화하는 작업은 O(1) 시간에 이루어지며, + 전체적으로 O(n) 시간이 추가됩니다 + + 공간 복잡도: O(n × k) + O(n × k) = O(n × k) + 정렬된 키 저장: 각 단어를 정렬한 결과를 문자열로 저장함 + 단어 길이가 k일 때, 정렬된 문자열 저장에 O(k) 공간이 필요함 + n개의 단어에 대해 총 O(n × k) 공간이 사용됨 + 딕셔너리 값 저장: 원본 단어를 그룹별로 저장함. 모든 단어를 저장해야 하므로 + +''' diff --git a/implement-trie-prefix-tree/taurus09318976.py b/implement-trie-prefix-tree/taurus09318976.py new file mode 100644 index 000000000..a3713a3bf --- /dev/null +++ b/implement-trie-prefix-tree/taurus09318976.py @@ -0,0 +1,78 @@ +''' +이 문제는 문자열을 효율적으로 저장하고 검색하는 트리 구조임 (예 : 자동완성 기능) +주요 기능 : 단어 추가(insert), 단어 검색(search), 접두사 확인(startsWith) + +Example 1.의 단계별 동작 + +메서드 호출 순서 리스트 인자 리스트 실제 호출 예시 +"Trie" [] Trie() -> 빈 트라이를 만듬. 루트 노드가 생성되고, 아무 문자도 저장되어 있지 않음 +"insert" ["apple"] insert("apple") -> 'a' 노드가 없으면 새로 만들고 이동 + -> 'p' 노드가 없으면 새로 만들고 이동 + -> 또 'p' 노드가 없으면 새로 만들고 이동 + -> 'l' 노드가 없으면 새로 만들고 이동 + -> 'e' 노드가 없으면 새로 만들고 이동 + -> 마지막 'e' 노드에 is_end = True 표시 (여기까지가 "apple"이라는 단어임을 뜻함) + +"search" ["apple"] search("apple") -> 루트부터 'a' → 'p' → 'p' → 'l' → 'e' 노드까지 차례로 이동 + -> 마지막 'e' 노드가 is_end = True 이므로 True 반환 + +"search" ["app"] search("app") -> 루트부터 'a' → 'p' → 'p' 노드까지 이동 + -> 'p' 노드의 is_end 값이 False (아직 "app"이라는 단어가 완성되지 않았음) + -> 따라서 False 반환 +"startsWith" ["app"] startsWith("app") -> 루트부터 'a' → 'p' → 'p' 노드까지 이동 + -> 노드가 존재하므로 True 반환 (접두사는 존재함) + +"insert" ["app"] insert("app") -> 루트부터 'a' → 'p' → 'p' 노드까지 이미 존재하므로 새 노드 생성 안 함 + -> 마지막 'p' 노드에 is_end = True 표시 (이제 "app"도 완성된 단어임) + -> "app"은 처음에 is_end=False였기 때문에 검색 시 False가 나왔고, 삽입 후 True가 됨 + +"search" ["app"] search("app") -> 루트부터 'a' → 'p' → 'p' 노드까지 이동 + -> 마지막 'p' 노드가 is_end = True 이므로 True 반환 + +<트라이 구조> + +루트 + └─ 'a' + └─ 'p' + └─ 'p' (is_end=True) ← "app" + └─ 'l' + └─ 'e' (is_end=True) ← "apple" + + +''' + +class TrieNode: # 트라이의 각 노드를 표현하는 클래스 + def __init__(self): + self.children = {} # 현재 노드의 자식들을 {문자:노드} 형태로 저장 + self.is_end = False # 현재 노드가 단어의 끝인지 표시(기본값 False) + +class Trie: + def __init__(self): + self.root = TrieNode() # 트라이의 시작점인 빈 루트 노드 생성 + + def insert(self, word: str) -> None: + node = self.root # 루트 노드에서 시작 + for char in word: # 단어의 각 문자를 하나씩 처리 + if char not in node.children: # 현재 노드의 자식에 문자가 없으면 + node.children[char] = TrieNode() # 새로운 노드 생성 후 자식에 추가 + node = node.children[char] # 다음 문자 노드로 이동 + node.is_end = True # 단어의 마지막 문자 노드에 끝 표시 + + def search(self, word: str) -> bool: + node = self.root + for char in word: # 단어의 각 문자를 따라 이동 + if char not in node.children: # 문자가 없으면 단어 존재 X -> False + return False + node = node.children[char] + return node.is_end # 모든 문자를 통과했다면, 마지막 노드가 단어 끝인지 확인 + + + def startsWith(self, prefix: str) -> bool: + node = self.root + for char in prefix: # 접두사의 각 문자를 따라 이동 + if char not in node.children: # 문자 없으면 접두사 존재 X -> False + return False # 모든 문자가 존재 -> 접두사 O + node = node.children[char] + return True # 7. 접두사 존재 + + diff --git a/word-break/taurus09318976.py b/word-break/taurus09318976.py new file mode 100644 index 000000000..e90f4dfb4 --- /dev/null +++ b/word-break/taurus09318976.py @@ -0,0 +1,35 @@ +''' +주어진 문자열 s를 공백으로 구분된 단어 시퀀스로 분할할 수 있는지 확인하는 문제임 +이때 모든 단어는 주어진 사전(wordDict)에 포함되어야 함 + +Example 1.의 예를 들면 + dp = [True, False, False, False, False, False, False, False, False] +인덱스: 0 1 2 3 4 5 6 7 8 +의미: "" "l" "le" "lee" "leet" "leetc" "leetco" "leetcod" "leetcode" +가능한 단어 길이: l=1~4 l=1~4 l=1~4 l=4 l=1~4 l=1~4 l=1~4 l=4 + +dp[i]가 True가 되는 조건: +dp[i-l]이 True이고, s[i-l:i]가 사전에 있는 단어일 때 +예시에서 i=4와 i=8에서 조건이 충족되어 True로 변경됨 + +''' + +class Solution: + def wordBreak(self, s: str, wordDict: List[str]) -> bool: + word_set = set(wordDict) # 단어 검색을 빠르게 하기 위해 집합으로 변환 + if not word_set: # 사전이 비어있으면 항상 False + return False + + max_len = max(len(word) for word in word_set) # 사전에서 가장 긴 단어의 길이 구분 + dp = [False] * (len(s) + 1) # dp[i]는 문자열 s의 첫 i글자가 분할 가능한지 여부를 저장 + dp[0] = True # 빈 문자열은 항상 분할 가능하므로 True + + for i in range(1, len(s) + 1): # 문자열의 모든 위치를 순회함 + for l in range(1, max_len + 1): # 단어 길이를 1부터 max_len까지 확인함 + if i < l: # 현재 위치보다 단어 길이가 길면 패스 + break + if dp[i - l] and s[i-l:i] in word_set: # 이전 위치가 True이고, 현재 부분 문자열이 사전에 있으면 True로 표시함 + dp[i] = True + break # 하나라도 성공하면 더 확인할 필요 없음 + + return dp[-1] # 최종 결과 반환