Skip to content
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

[dd_._._bb] Week 5 #852

Open
wants to merge 10 commits into
base: main
Choose a base branch
from
43 changes: 43 additions & 0 deletions best-time-to-buy-and-sell-stock/lledellebell.js
Original file line number Diff line number Diff line change
@@ -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
67 changes: 67 additions & 0 deletions encode-and-decode-strings/lledellebell.js
Original file line number Diff line number Diff line change
@@ -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"]
52 changes: 52 additions & 0 deletions group-anagrams/lledellebell.ts
Original file line number Diff line number Diff line change
@@ -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<string, string[]>();

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"]]
120 changes: 120 additions & 0 deletions implement-trie-prefix-tree/lledellebell.js
Original file line number Diff line number Diff line change
@@ -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
74 changes: 74 additions & 0 deletions word-break/lledellebell.js
Original file line number Diff line number Diff line change
@@ -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
Loading