Skip to content

[clara-shin] WEEK 06 solutions #1420

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 9, 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
42 changes: 42 additions & 0 deletions container-with-most-water/clara-shin.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
/**
* 두 선을 선택해 물을 담을 수 있는 최대 면적을 구하는 문제
*
* 가로 길이: 두 선 사이의 거리(인덱스 차이)
* 세로 길이: 두 선 중 더 짧은 높이(물은 낮은 쪽으로 넘치기 때문)
* 면적 = 가로 길이 × 세로 길이
*
* 양쪽 끝에서 시작하는 두 포인터를 사용
* 투 포인터 방식이 효율적인 이유: 항상 더 작은 높이를 가진 쪽을 이동시키면 최대 면적을 놓치지 않기 때문
* 더 큰 높이 쪽을 이동시키면 가로 길이는 줄어들고, 세로 길이는 같거나 더 작아져서 면적이 줄어들 수밖에 없음
Copy link
Contributor

Choose a reason for hiding this comment

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

문제 풀이 로직, 복잡도 분석, 그리고 코드 라인별 코멘트를 잘 정리하신 걸 보고 많이 배우고 갑니다...! 🤩

*
* 시간 복잡도: O(n) - 배열을 한 번만 순회
* 공간 복잡도: O(1) - 추가 공간 사용 없음
*/
/**
* @param {number[]} height
* @return {number}
*/
var maxArea = function (height) {
let left = 0;
let right = height.length - 1;
let maxWater = 0;

while (left < right) {
// 현재 두 선으로 만들 수 있는 물의 양 계산
const width = right - left;
const minHeight = Math.min(height[left], height[right]);
const water = width * minHeight;

// 최대값 업데이트
maxWater = Math.max(maxWater, water);

// 더 작은 높이를 가진 쪽의 포인터를 이동시킴
if (height[left] < height[right]) {
left++;
} else {
right--;
}
}

return maxWater;
};
130 changes: 130 additions & 0 deletions design-add-and-search-words-data-structure/clara-shin.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
/**
* 단어를 저장하고 검색할 수 있는 자료구조 만들기
*
* 트라이(Trie) 자료구조: 문자열 집합을 표현하는 트리 기반 자료구조, (문자열 검색에 효율적)
- 각 노드는 문자 하나를 나타냄
- 루트에서 특정 노드까지의 경로는 하나의 문자열을 나타냄
- 각 노드는 자식 노드들을 가리키는 링크(보통 해시맵)를 가짐
- 단어의 끝을 표시하는 플래그가 필요함
*
* 대부분의 검색은 와일드카드가 없는 경우일 테니까, 이것만 처리하는 별도 메서드로 뽑음(와일드카드 분기처리)
*
* 시간복잡도: O(m) (m: 단어의 길이)
* 공간복잡도: O(n) (n: 단어의 개수)
*/

var WordDictionary = function () {
// 트라이 노드 클래스 정의
this.TrieNode = function () {
this.children = {}; // 자식 노드들을 저장하는 해시맵
this.isEnd = false; // 단어의 끝을 표시하는 플래그
};

this.root = new this.TrieNode(); // 루트 노드 생성
};

/**
* 단어를 트라이에 추가
* @param {string} word
* @return {void}
*/
WordDictionary.prototype.addWord = function (word) {
let node = this.root;

// 단어의 각 문자를 순회하며 트라이에 추가
for (let i = 0; i < word.length; i++) {
const char = word[i];

// 현재 문자에 해당하는 자식 노드가 없으면 생성
if (!node.children[char]) {
node.children[char] = new this.TrieNode();
}

// 다음 레벨로 이동
node = node.children[char];
}

// 단어의 끝 표시
node.isEnd = true;
};

/**
* 트라이에서 단어 검색 (와일드카드 '.' 지원)
* @param {string} word
* @return {boolean}
*/
WordDictionary.prototype.search = function (word) {
// 와일드카드가 없는 경우
if (!word.includes('.')) {
return this.searchExact(word);
}

return this.searchWithWildcard(word, 0, this.root);
};

/**
* 와일드카드 없이 정확한 단어 검색
* @param {string} word
* @return {boolean}
*/
WordDictionary.prototype.searchExact = function (word) {
let node = this.root;

for (let i = 0; i < word.length; i++) {
const char = word[i];

// 해당 문자의 자식 노드가 없으면 false
if (!node.children[char]) {
return false;
}

node = node.children[char];
}

// 단어의 끝에 도달했을 때 isEnd 플래그 확인
return node.isEnd;
};

/**
* 와일드카드를 포함한 단어 검색 (재귀적)
* @param {string} word - 검색할 단어
* @param {number} index - 현재 검색중인 문자 인덱스
* @param {object} node - 현재 검색중인 노드
* @return {boolean}
*/
WordDictionary.prototype.searchWithWildcard = function (word, index, node) {
// 단어의 모든 문자를 검사했으면
if (index === word.length) {
return node.isEnd;
}

const char = word[index];

// 와일드카드('.')인 경우
if (char === '.') {
// 현재 노드의 모든 자식에 대해 재귀적으로 검색
for (const key in node.children) {
if (this.searchWithWildcard(word, index + 1, node.children[key])) {
return true;
}
}
return false;
}
// 일반 문자인 경우
else {
// 해당 문자에 대한 자식 노드가 없으면 false
if (!node.children[char]) {
return false;
}

// 다음 문자 검색
return this.searchWithWildcard(word, index + 1, node.children[char]);
}
};

/**
* Your WordDictionary object will be instantiated and called as such:
* var obj = new WordDictionary()
* obj.addWord(word)
* var param_2 = obj.search(word)
*/
71 changes: 71 additions & 0 deletions longest-increasing-subsequence/clara-shin.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
/**
* 최장 증가 부분 수열(Longest Increasing Subsequence, LIS)'을 구하기
* 부분 수열(Subsequence): 원래 수열에서 몇 개의 원소를 골라서 순서를 바꾸지 않고 나열한 것
* 증가 부분 수열(Increasing Subsequence): 부분 수열의 원소들이 오름차순으로 정렬된 것
*
* 접근방법: 동적계획법(DP) 또는 이진탐색(Binary Search)
* 1. DP를 이용한 방법: 시간복잡도 O(n^2)
* 2. 이진탐색을 이용한 방법: 시간복잡도 O(n log n) ✅ follow-up 고려
*/

/** 동적계획법(DP)으로 접근
* @param {number[]} nums
* @return {number}
*/
var lengthOfLIS = function (nums) {
if (nums.length === 0) return 0;

// dp[i]는 인덱스 i까지의 가장 긴 증가 부분 수열의 길이
const dp = Array(nums.length).fill(1);

// 모든 위치에 대해 검사
for (let i = 1; i < nums.length; i++) {
// 현재 위치보다 이전의 모든 위치를 검사
for (let j = 0; j < i; j++) {
// 현재 값이 이전 값보다 크면, 이전 위치의 LIS에 현재 값을 추가할 수 있음
if (nums[i] > nums[j]) {
// 기존 값과 (이전 위치의 LIS 길이 + 1) 중 더 큰 값을 선택
dp[i] = Math.max(dp[i], dp[j] + 1);
}
}
}

// dp 배열에서 가장 큰 값이 LIS의 길이
return Math.max(...dp);
};

/** 이진탐색(Binary Search)으로 접근
* @param {number[]} nums
* @return {number}
*/
var lengthOfLIS = function (nums) {
if (nums.length === 0) return 0;

// tails[i]는 길이가 i+1인 증가 부분 수열의 마지막 원소 중 가장 작은 값
const tails = [];

for (let num of nums) {
// 이진 탐색으로 num이 들어갈 위치 찾기
let left = 0;
let right = tails.length;

while (left < right) {
const mid = Math.floor((left + right) / 2);
if (tails[mid] < num) {
left = mid + 1;
} else {
right = mid;
}
}

// 찾은 위치에 num 삽입 또는 대체
if (left === tails.length) {
tails.push(num); // 새로운 최장 길이 발견
} else {
tails[left] = num; // 기존 값 갱신
}
}

// tails 배열의 길이가 LIS의 길이
return tails.length;
};
67 changes: 67 additions & 0 deletions spiral-matrix/clara-shin.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
/**
* 오른쪽 -> 아래쪽 -> 왼쪽 -> 위쪽
* --- 행렬의 경계 ---
* top: 위쪽 경계
* bottom: 아래쪽 경계
* left: 왼쪽 경계
* right: 오른쪽 경계
* ----------------
* 각 방향으로 한 바퀴를 돌 때마다 경계를 하나씩 줄여가며 모든 요소를 방문
*/

/**
* @param {number[][]} matrix
* @return {number[]}
*/
var spiralOrder = function (matrix) {
if (!matrix.length || !matrix[0].length) return []; // 빈 행렬 체크

const result = []; // 결과를 저장할 배열 초기화

let top = 0;
let bottom = matrix.length - 1;
let left = 0;
let right = matrix[0].length - 1;

// 아직 처리할 요소가 남아있는 동안 계속 순회
// top > bottom 또는 left > right가 되면 모든 요소를 방문한 것
while (top <= bottom && left <= right) {
// 1. 위쪽 행: 왼쪽 → 오른쪽 이동
for (let i = left; i <= right; i++) {
result.push(matrix[top][i]);
}
// 위쪽 행을 처리했으므로 top 인덱스를 1 증가
top++;

// 2. 오른쪽 열: 위 → 아래 이동
for (let i = top; i <= bottom; i++) {
result.push(matrix[i][right]);
}
// 오른쪽 열을 처리했으므로 right 인덱스를 1 감소
right--;

// 3. 아래쪽 행: 오른쪽 → 왼쪽 이동
// 이미 top이 bottom을 초과한 경우, 아래쪽 행이 존재하지 않으므로 처리하지 않음
if (top <= bottom) {
// 현재 bottom 행에서 right부터 left까지의 모든 요소를 역순으로 순회
for (let i = right; i >= left; i--) {
result.push(matrix[bottom][i]);
}
// 아래쪽 행을 처리했으므로 bottom 인덱스를 1 감소
bottom--;
}

// 4. 왼쪽 열: 아래 → 위 이동
// 이미 left가 right를 초과한 경우, 왼쪽 열이 존재하지 않으므로 처리하지 않음
if (left <= right) {
// 현재 left 열에서 bottom부터 top까지의 모든 요소를 역순으로 순회
for (let i = bottom; i >= top; i--) {
result.push(matrix[i][left]);
}
// 왼쪽 열을 처리했으므로 left 인덱스를 1 증가
left++;
}
}

return result;
};
37 changes: 37 additions & 0 deletions valid-parentheses/clara-shin.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
/**
* 괄호 문자열 유효성 검사
*
* 스택 자료구조 활용
* 1. 괄호 쌍을 매핑하는 객체를 생성하고 조건을 확인
* 2. 열린 괄호를 만나면 해당하는 닫힌 괄호를 스택에 직접 push
* 3. 닫는 괄호를 만났을 때, 스택이 비어있거나 짝이 맞지 않으면 false
* 4. 문자열을 모두 처리한 후, 스택이 비어있어야(문자열 길이가 0이어야) 모든 괄호가 올바르게 짝지어진 것(true)
*/

/**
* @param {string} s
* @return {boolean}
*/
var isValid = function (s) {
// 빈 문자열이나 홀수 길이는 유효하지 않음
if (s.length === 0 || s.length % 2 !== 0) return false;
Copy link
Contributor

Choose a reason for hiding this comment

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

이렇게 빠르게 종료할 수 있는 조건을 추가해두면 런타임 측면에서 확실히 좋아질 것 같네요!


const stack = [];

for (let i = 0; i < s.length; i++) {
const char = s[i];

if (char === '(') {
stack.push(')');
} else if (char === '{') {
stack.push('}');
} else if (char === '[') {
stack.push(']');
Copy link
Contributor

Choose a reason for hiding this comment

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

if문부터 두 번째 else if문까지 여는 괄호를 판단하는 동일한 조건과 stack.push()라는 동일한 동작을 구현하고 있기 때문에, 다음과 같이 해시맵을 사용한다면 반복되는 분기문 코드를 조금 더 줄일 수도 있을 것 같습니다~! (다만, 제가 자바스크립트를 잘 몰라서 문법에 오류가 있을 수 있습니다.. 😭)

const mapping = {
  '(': ')',
  '{': '}',
  '[': ']'
};

...

for (let char of s) {
  if (char in mapping) {
    stack.push(mapping[char]);
  } else {
    ...
    }
  }
}

} else if (stack.length === 0 || stack.pop() !== char) {
// 닫는 괄호를 만났을 때, 스택이 비어있거나 짝이 맞지 않음
return false;
}
}

return stack.length === 0;
};