diff --git a/best-time-to-buy-and-sell-stock/radiantchoi.py b/best-time-to-buy-and-sell-stock/radiantchoi.py new file mode 100644 index 000000000..6b861f6f4 --- /dev/null +++ b/best-time-to-buy-and-sell-stock/radiantchoi.py @@ -0,0 +1,24 @@ +class Solution: + def maxProfit(self, prices: List[int]) -> int: + # profit: 최대 이익 + profit = 0 + + # current_minimum: 현재까지의 최소 가격 + current_minimum = prices[0] + + # current_profit: 현재까지의 최대 이익 + current_profit = 0 + + # 전체 배열에서 최솟값과, 최솟값 인덱스 이후 최댓값을 이익으로 계산하는 반복문 + for i in range(1, len(prices)): + # 이익이 발생할 경우 현재 이익 갱신 + # 현재 이익을 갱신할 때마다 지금까지의 최대 이익도 갱신 + if prices[i] > current_minimum: + current_profit = max(current_profit, prices[i] - current_minimum) + profit = max(profit, current_profit) + # 이익이 발생하지 않을 경우 최솟값 갱신 + else: + current_minimum = prices[i] + + # 최대 이익만 저장되어 반환 + return profit diff --git a/group-anagrams/radiantchoi.swift b/group-anagrams/radiantchoi.swift new file mode 100644 index 000000000..631370758 --- /dev/null +++ b/group-anagrams/radiantchoi.swift @@ -0,0 +1,47 @@ +class Solution { + func groupAnagrams(_ strs: [String]) -> [[String]] { + // 애너그램을 저장할 딕셔너리 + var groups = [String: [String]]() + + for str in strs { + // 각각의 문자열별로 등장하는 글자의 갯수를 저장할 딕셔너리 + var occurences = [Character: Int]() + + // 등장하는 글자의 갯수를 계산 + // Swift에서는 글자를 다루기 위해 배열로 변환하는 것이 좋음 + for letter in Array(str) { + if let val = occurences[letter] { + occurences[letter] = val + 1 + } else { + occurences[letter] = 1 + } + } + + let anagramKey = makeAnagramKey(for: occurences) + + // 딕셔너리에 이미 같은 애너그램 키가 존재할 경우, 해당 키에 대한 값에 현재 문자열 추가 + if let group = groups[anagramKey] { + groups[anagramKey]!.append(str) + } else { + // 딕셔너리에 키가 존재하지 않을 경우, 키를 추가하고 현재 문자열을 값으로 추가 + groups[anagramKey] = [str] + } + } + + // 딕셔너리의 값들을 배열로 반환 - 반환 순서는 상관없다고 문제에 명시 + // 반환 순서가 필요할 경우 애너그램 키의 등장 순서를 저장하는 딕셔너리를 새로 만들 수 있음 + return Array(groups.values) + } + + // 등장하는 글자의 갯수에 따라 딕셔너리의 키를 만드는 함수 + func makeAnagramKey(for occurences: [Character: Int]) -> String { + // 같은 애너그램을 가질 때 같은 키를 반환하게 하기 위해, 정렬 적용 + return occurences.keys.sorted().reduce("") { (previous, key) in + var previous = previous + previous += String(key) + previous += String(occurences[key]!) + + return previous + } + } +} diff --git a/implement-trie-prefix-tree/radiantchoi.swift b/implement-trie-prefix-tree/radiantchoi.swift new file mode 100644 index 000000000..ac219b1d6 --- /dev/null +++ b/implement-trie-prefix-tree/radiantchoi.swift @@ -0,0 +1,79 @@ +class Trie { + private let root: TrieNode + + init() { + self.root = TrieNode() + } + + func insert(_ word: String) { + var node = root // 루트 노드의 포인터를 가져오고 + + // 단어의 각 글자를 순회하며, 이미 있다면 해당 노드로 이동하고, + // 그렇지 않다면 새로운 노드를 생성하여 연결 + for letter in word { + if let childNode = node.children[letter] { + node = childNode + } else { + let newNode = TrieNode() + node.children[letter] = newNode + node = newNode + } + } + + // 단어의 마지막 글자 노드에 isTerminating을 true로 설정 + node.isTerminating = true + } + + func search(_ word: String) -> Bool { + var node = root + + // 노드 포인터만 제공하여 순회 로직을 수행하고, + // 마지막 노드가 isTerminating이 true인지 확인 + guard traverse(startsWith: &node, quote: word) else { return false } + guard node.isTerminating else { return false } + + return true + } + + func startsWith(_ prefix: String) -> Bool { + var node = root + // 노드 포인터만 제공하여 순회 로직을 수행하고 그대로 반환 + return traverse(startsWith: &node, quote: prefix) + } + + // 트리 탐색 로직 + // 노드 포인터를 받아서, 순회 로직을 수행하고 포인터를 교체 + private func traverse(startsWith node: inout TrieNode, quote: String) -> Bool { + for letter in quote { + guard let childNode = node.children[letter] else { + return false + } + + node = childNode + } + + return true + } +} + +// 트라이의 각각의 노드 구현 +// 자식 노드 딕셔너리와, 이 글자로 끝나는 단어가 있는지 여부를 저장 +// 글자 자체는 부모 노드 딕셔너리의 키로 저장되므로 따로 저장하지 않음 +// 루트에는 아무 글자도 들어가지 않는다는 Trie의 조건도 자동으로 만족 +class TrieNode { + var children: [Character: TrieNode] + var isTerminating: Bool + + init(children: [Character: TrieNode] = [:], isTerminating: Bool = false) { + self.children = children + self.isTerminating = isTerminating + } +} + +/** + * Your Trie object will be instantiated and called as such: + * let obj = Trie() + * obj.insert(word) + * let ret_2: Bool = obj.search(word) + * let ret_3: Bool = obj.startsWith(prefix) + */ diff --git a/word-break/radiantchoi.py b/word-break/radiantchoi.py new file mode 100644 index 000000000..9f3de2e7a --- /dev/null +++ b/word-break/radiantchoi.py @@ -0,0 +1,32 @@ +class Solution: + def traverse(self, s: str, wordDict: List[str], checked: int, memo: Dict[int,bool]) -> bool: + # checked의 의미: 현재까지 체크한 문자열의 길이 - 아직 여기서부터 시작해서 끝까지 갈 수 있다고 장담할 수 없음 + # 시작 인덱스가 문자열의 길이와 같은 경우, 이 문자열에 대해 검사를 마쳤다는 뜻 (남은 길이가 0) + if checked == len(s): + return True + + # 지금 다루려는 인덱스를 이미 체크했다면, 후속 계산을 하지 않고 바로 반환 + if checked in memo: + return memo[checked] + + for word in wordDict: + # 슬라이싱 대신 startswith를 쓰는 것이 성능상 이점이 있다 + # 지금 보려는 단어가 문자열의 prefix인 경우, 즉 다음 탐색을 수행해 볼 여건이 되는 경우 + # 여기서 시작해서 끝까지 갔을 때 문장이 완성이 되니? 라는 것 + + # 만약 가능할 경우, 거쳐온 모든 path에 대해 memo 딕셔너리에는 True가 저장되게 된다. + # 또한 early return을 통해 후속 케이스를 계산하지 않는다. + if s.startswith(word, checked): + if self.traverse(s, wordDict, checked + len(word), memo): + memo[checked] = True + return True + + # 위의 for문에서 만족하는 게 없다면, 여기서 시작해가지고서는 끝까지 갈 수 없다는 것 + memo[checked] = False + return False + + + def wordBreak(self, s: str, wordDict: List[str]) -> bool: + memo = {} + + return self.traverse(s, wordDict, 0, memo)