diff --git a/best-time-to-buy-and-sell-stock/lledellebell.js b/best-time-to-buy-and-sell-stock/lledellebell.js new file mode 100644 index 000000000..23809c742 --- /dev/null +++ b/best-time-to-buy-and-sell-stock/lledellebell.js @@ -0,0 +1,43 @@ +/** + * + * @problem + * 주어진 주식 가격 배열에서 한 번의 매수와 한 번의 매도를 통해 얻을 수 있는 최대 이익을 계산합니다. + * 매수는 매도보다 반드시 먼저 이루어져야 합니다. + * + * @constraints + * - 1 <= prices.length <= 105 + * - 0 <= prices[i] <= 104 + * + * @param {number[]} prices - 각 날짜별 주식 가격을 나타내는 배열 + * @returns {number} 최대 이익 (이익을 낼 수 없는 경우 0 반환) + * + * @example + * maxProfit([7, 1, 5, 3, 6, 4]); // 5 (2일차에 매수하고 5일차에 매도) + * maxProfit([7, 6, 4, 3, 1]); // 0 (이익을 낼 수 없음) + * + * @complexity + * - 시간 복잡도: O(n) + * 배열을 한 번 순회하며 각 요소에 대해 상수 시간 연산만 수행합니다. + * - 공간 복잡도: O(1) + * 추가적인 배열이나 데이터 구조를 사용하지 않고, 두 개의 변수만 사용합니다. + */ +function maxProfit(prices) { + let min_price = Infinity; // 현재까지의 최소 매수 가격 (초기값은 무한대) + let max_profit = 0; // 현재까지의 최대 이익 (초기값은 0) + + // 배열을 순회하며 최소 매수 가격과 최대 이익을 계산 + for (let price of prices) { + if (price < min_price) { + // 현재 주식 가격이 최소 매수 가격보다 작으면 최소 매수 가격 갱신 + min_price = price; + } else if (price - min_price > max_profit) { + // 현재 주식 가격에서 최소 매수 가격을 뺀 값(현재 이익)이 최대 이익보다 크면 최대 이익 갱신 + max_profit = price - min_price; + } + } + + return max_profit; // 최대 이익 반환 +} + +console.log(maxProfit([7, 1, 5, 3, 6, 4])); // 5 +console.log(maxProfit([7, 6, 4, 3, 1])); // 0 diff --git a/encode-and-decode-strings/lledellebell.js b/encode-and-decode-strings/lledellebell.js new file mode 100644 index 000000000..09346cf3b --- /dev/null +++ b/encode-and-decode-strings/lledellebell.js @@ -0,0 +1,67 @@ +/** + * + * @problem + * 문자열 배열을 단일 문자열로 인코딩하고, + * 다시 원래의 문자열 배열로 디코딩하는 기능을 만들어야 합니다. + * + * @example + * const encoded = encode(["hello", "world"]); + * console.log(encoded); // "5?hello5?world" + * const decoded = decode(encoded); + * console.log(decoded); // ["hello", "world"] + * + * @complexity + * - 시간 복잡도: + * ㄴ encode: O(n) (n은 문자열 배열의 총 길이) + * ㄴ decode: O(n) (n은 인코딩된 문자열의 길이) + * - 공간 복잡도: + * ㄴ encode: O(1) (추가 메모리 사용 없음) + * ㄴ decode: O(1) (결과 배열을 제외한 추가 메모리 사용 없음) + */ + +/** + * @param {string[]} strs - 인코딩할 문자열 배열 + * @returns {string} - 인코딩된 문자열 + */ +const encode = (strs) => { + let encoded = ''; + for (const str of strs) { + // 문자열을 "길이?문자열" 형식으로 추가 + encoded += `${str.length}?${str}`; + } + return encoded; +}; + +/** + * @param {string} s - 인코딩된 문자열 + * @returns {string[]} - 디코딩된 문자열 배열 + */ +const decode = (s) => { + const result = []; + let i = 0; + + while (i < s.length) { + // 현재 위치에서 숫자(길이)를 읽음 + let length = 0; + while (s[i] !== '?') { + length = length * 10 + (s[i].charCodeAt(0) - '0'.charCodeAt(0)); // 숫자 계산 + i++; + } + + // '?' 이후의 문자열을 추출 + i++; // '?'를 건너뜀 + const str = s.substring(i, i + length); + result.push(str); + + // 다음 문자열로 이동 + i += length; + } + + return result; +}; + +const encoded = encode(["hello", "world"]); +console.log(encoded); // "5?hello5?world" + +const decoded = decode(encoded); +console.log(decoded); // ["hello", "world"] diff --git a/group-anagrams/lledellebell.ts b/group-anagrams/lledellebell.ts new file mode 100644 index 000000000..9b55c7b0a --- /dev/null +++ b/group-anagrams/lledellebell.ts @@ -0,0 +1,52 @@ +/** + * + * @problem + * 문자열 배열이 주어졌을 때, 애너그램끼리 그룹화해야 합니다. + * + * (참고) + * 애너그램(Anagram)이란 단어를 구성하는 문자의 순서를 바꿔서 다른 단어를 만드는 것을 의미합니다. + * 예를 들어, "eat", "tea", "ate"는 모두 같은 문자로 구성되어 있으므로 애너그램입니다. + * + * @constraints + * - 1 <= strs.length <= 104 + * - 0 <= strs[i].length <= 100 + * - strs[i]는 소문자 알파벳으로만 구성됩니다. + * + * @param {string[]} strs - 입력 문자열 배열 + * @returns {string[][]} 애너그램 그룹으로 묶인 2차원 문자열 배열 + * + * @example + * groupAnagrams(["eat", "tea", "tan", "ate", "nat", "bat"]); // [["bat"], ["nat", "tan"], ["ate", "eat", "tea"]] + * groupAnagrams([""]); // [[""]] + * groupAnagrams(["a"]); // [["a"]] + * + * @description + * - 각 문자열을 정렬하여 동일한 문자를 가진 문자열들을 그룹화합니다. + * - 정렬된 문자열을 키로 사용하여 해시맵(Map)에 저장합니다. + * - 최종적으로 해시맵의 값들만 추출하여 반환합니다. + * + * @complexity + * - 시간 복잡도: O(N * K log K) + * ㄴ N: 입력 문자열 배열의 길이 + * ㄴ K: 각 문자열의 평균 길이 + * 각 문자열을 정렬하는 데 O(K log K)의 시간이 소요되며, 이를 N번 반복합니다. + * - 공간 복잡도: O(N * K) + * 해시맵에 저장되는 키와 값의 총 길이에 비례합니다. + */ +function groupAnagrams(strs: string[]): string[][] { + const anagrams = new Map(); + + for (const str of strs) { + const key = str.split('').sort().join(''); + if (!anagrams.has(key)) { + anagrams.set(key, []); + } + anagrams.get(key)!.push(str); + } + + return Array.from(anagrams.values()); +} + +console.log(groupAnagrams(["eat", "tea", "tan", "ate", "nat", "bat"])); // [["bat"], ["nat", "tan"], ["ate", "eat", "tea"]] +console.log(groupAnagrams([""])); // [[""]] +console.log(groupAnagrams(["a"])); // [["a"]] diff --git a/implement-trie-prefix-tree/lledellebell.js b/implement-trie-prefix-tree/lledellebell.js new file mode 100644 index 000000000..5b0b061ae --- /dev/null +++ b/implement-trie-prefix-tree/lledellebell.js @@ -0,0 +1,120 @@ +/** + * @problem + * Trie를 구현합니다. + * - 메모리 사용량을 최적화하면서 정확한 검색 결과를 보장해야 합니다. + * + * @constraints + * - word와 prefix의 길이는 최소 1, 최대 2000입니다. + * - word와 prefix는 소문자 영어 알파벳(a-z)만으로 구성됩니다. + * - insert, search, startsWith 함수 호출은 총 30,000번을 넘지 않습니다. + * + * @example + * const trie = new Trie(); + * trie.insert("apple"); // undefined + * trie.search("apple"); // true + * trie.search("app"); // false + * trie.startsWith("app"); // true + * trie.insert("app"); // undefined + * trie.search("app"); // true + * + * @complexity + * - 시간복잡도: + * ㄴ insert: O(m) (m은 단어 길이) + * ㄴ search: O(m) (m은 단어 길이) + * ㄴ startsWith: O(m) (m은 접두사 길이) + * - 공간복잡도: O(ALPHABET_SIZE * m * n) + * ㄴ ALPHABET_SIZE: 문자열의 알파벳 수 (영문의 경우 26) + * ㄴ m: 단어의 평균 길이 + */ +class TrieNode { + constructor() { + // 각 문자를 키로 하고 자식 노드를 값으로 하는 객체 + this.children = {}; + // 현재 노드가 단어의 마지막 문자인지 표시하는 플래그 + this.isEndOfWord = false; + } +} + +class Trie { + constructor() { + // 빈 문자열을 나타내는 루트 노드 생성 + this.root = new TrieNode(); + } + + /** + * 단어를 Trie에 삽입하는 메서드 + * @param {string} word - 삽입할 단어 + */ + insert(word) { + // 루트 노드부터 시작 + let node = this.root; + + // 단어의 각 문자를 순회 + for (let i = 0; i < word.length; i++) { + const char = word[i]; + // 현재 문자에 대한 노드가 없으면 새로 생성 + if (!(char in node.children)) { + node.children[char] = new TrieNode(); + } + // 다음 문자를 처리하기 위해 자식 노드로 이동 + node = node.children[char]; + } + // 단어의 마지막 문자임을 표시 + node.isEndOfWord = true; + } + + + /** + * 정확한 단어가 존재하는지 검색하는 메서드 + * @param {string} word - 검색할 단어 + * @returns {boolean} - 단어 존재 여부 + */ + search(word) { + // 단어를 찾아 마지막 노드를 반환 + const node = this._traverse(word); + // 단어가 존재하고 해당 노드가 단어의 끝인 경우에만 true 반환 + return node !== null && node.isEndOfWord; + } + + /** + * 주어진 접두사로 시작하는 단어가 존재하는지 검색하는 메서드 + * @param {string} prefix - 검색할 접두사 + * @returns {boolean} - 접두사로 시작하는 단어 존재 여부 + */ + startsWith(prefix) { + // 접두사에 해당하는 노드가 존재하는지만 확인 + return this._traverse(prefix) !== null; + } + + /** + * 문자열을 따라가며 마지막 노드를 반환하는 내부 헬퍼 메서드 + * @param {string} str - 탐색할 문자열 + * @returns {TrieNode|null} - 마지막 문자에 해당하는 노드 또는 null + * @private + */ + _traverse(str) { + // 루트 노드부터 시작 + let node = this.root; + + // 문자열의 각 문자를 순회 + for (let i = 0; i < str.length; i++) { + const char = str[i]; + // 현재 문자에 대한 노드가 없으면 null 반환 + if (!(char in node.children)) { + return null; + } + // 다음 문자를 처리하기 위해 자식 노드로 이동 + node = node.children[char]; + } + // 마지막 노드 반환 + return node; + } +} + +const trie = new Trie(); +console.log(trie.insert("apple")); // undefined +console.log(trie.search("apple")); // true +console.log(trie.search("app")); // false +console.log(trie.startsWith("app")); // true +console.log(trie.insert("app")); // undefined +console.log(trie.search("app")); // true diff --git a/word-break/lledellebell.js b/word-break/lledellebell.js new file mode 100644 index 000000000..1d090875f --- /dev/null +++ b/word-break/lledellebell.js @@ -0,0 +1,74 @@ +/** + * @problem + * 주어진 문자열 s가 단어 사전 wordDict에 포함된 단어들로만 구성될 수 있는지 확인하는 문제입니다. + * 단어 사전의 단어들은 여러 번 사용할 수 있으며, 문자열 s를 완전히 나눌 수 있어야 합니다. + * + * @constraints + * - 1 <= s.length <= 300 + * - 1 <= wordDict.length <= 1000 + * - 1 <= wordDict[i].length <= 20 + * - s와 wordDict[i]는 모두 소문자 알파벳으로만 구성됩니다. + * + * @param {string} s - 주어진 문자열 + * @param {string[]} wordDict - 단어 사전 + * @returns {boolean} 문자열 s가 단어 사전의 단어들로만 나눌 수 있는지 여부 + * + * @example + * - 예제 1: + * ㄴ Input: s = "leetcode", wordDict = ["leet", "code"] + * ㄴ Output: true + * ㄴ Explanation: "leetcode"는 "leet" + "code"로 나눌 수 있습니다. + * - 예제 2: + * ㄴ Input: s = "applepenapple", wordDict = ["apple", "pen"] + * ㄴ Output: true + * ㄴ Explanation: "applepenapple"는 "apple" + "pen" + "apple"로 나눌 수 있습니다. + * - 예제 3: + * ㄴ Input: s = "catsandog", wordDict = ["cats", "dog", "sand", "and", "cat"] + * ㄴ Output: false + * ㄴ Explanation: "catsandog"는 wordDict의 단어들로 나눌 수 없습니다. + * + * @complexity + * - 시간 복잡도: O(n^2) + * ㄴ 외부 반복문: 문자열 s의 길이 n에 대해 1부터 n까지 반복 (O(n)) + * ㄴ 내부 반복문: 각 i에 대해 최대 i번 반복 (O(n)) + * ㄴ substring 및 Set 검색: O(1) (substring은 내부적으로 O(k)이지만, k는 최대 단어 길이로 간주) + * ㄴ 결과적으로 O(n^2)의 시간 복잡도를 가짐 + * - 공간 복잡도: O(n + m) + * ㄴ dp 배열의 크기: s의 길이 n + 1 (O(n)) + * ㄴ wordSet: wordDict의 단어 개수에 비례 (O(m), m은 wordDict의 단어 수) + */ +function wordBreak(s, wordDict) { + // wordDict를 Set으로 변환하여 검색 속도를 O(1)로 만듦 + const wordSet = new Set(wordDict); + + // dp 배열 생성: dp[i]는 s의 처음부터 i번째 문자까지가 wordDict의 단어들로 나눌 수 있는지를 나타냄 + const dp = new Array(s.length + 1).fill(false); + dp[0] = true; // 빈 문자열은 항상 나눌 수 있음 + + // i는 문자열의 끝 인덱스를 나타냄 + for (let i = 1; i <= s.length; i++) { + // j는 문자열의 시작 인덱스를 나타냄 + for (let j = 0; j < i; j++) { + // dp[j]가 true이고, s[j:i]가 wordSet에 포함되어 있다면 + if (dp[j] && wordSet.has(s.substring(j, i))) { + dp[i] = true; // dp[i]를 true로 설정 + break; // 더 이상 확인할 필요 없음 + } + } + } + + // dp[s.length]가 true라면 문자열 s를 wordDict의 단어들로 나눌 수 있음 + return dp[s.length]; +} + +const s1 = "leetcode"; +const wordDict1 = ["leet", "code"]; +console.log(wordBreak(s1, wordDict1)); // true + +const s2 = "applepenapple"; +const wordDict2 = ["apple", "pen"]; +console.log(wordBreak(s2, wordDict2)); // true + +const s3 = "catsandog"; +const wordDict3 = ["cats", "dog", "sand", "and", "cat"]; +console.log(wordBreak(s3, wordDict3)); // false