diff --git a/wjsdncl/100_same_tree.js b/wjsdncl/100_same_tree.js new file mode 100644 index 0000000..10dbb90 --- /dev/null +++ b/wjsdncl/100_same_tree.js @@ -0,0 +1,24 @@ +/** + * 24m40s 소요 + * + * 시간복잡도: O(n) n = 트리의 노드 개수 + * + * & 비트 연산자를 사용하여 두 트리의 노드를 비교합니다. + * 이진 트리를 처음 알게 되었는데 이진 트리의 구조를 이해하는 데 도움이 되었습니다. + * val은 현재 노드의 값이고 left와 right는 왼쪽과 오른쪽 자식 노드를 가리킵니다. + * + * 두 트리가 모두 null인 경우 두 트리는 동일합니다. + * 둘 중 하나가 null이거나 노드의 값이 다르면 트리는 동일하지 않습니다. + * 현재 노드가 같다면 왼쪽과 오른쪽 자식을 재귀적으로 비교합니다. + * 두 트리가 동일하다면 true를 반환하고 그렇지 않다면 false를 반환합니다. + */ +var isSameTree = function (p, q) { + // 둘 다 null인 경우 두 트리는 동일 + if (!p && !q) return true; + + // 둘 중 하나가 null이거나 노드의 값이 다르면 트리는 동일하지 않음 + if (!p || !q || p.val !== q.val) return false; + + // 현재 노드가 같다면 왼쪽과 오른쪽 자식을 재귀적으로 비교 (O(n)) + return isSameTree(p.left, q.left) && isSameTree(p.right, q.right); +}; diff --git a/wjsdncl/102_binary_tree_level_order_traversal.js b/wjsdncl/102_binary_tree_level_order_traversal.js new file mode 100644 index 0000000..c99cbcb --- /dev/null +++ b/wjsdncl/102_binary_tree_level_order_traversal.js @@ -0,0 +1,35 @@ +/** + * 32m13s 소요 + * + * 시간복잡도: O(n) + * + * 이진 트리의 노드 값을 레벨 순서대로 반환하는 문제입니다. + * + * 큐를 이용하여 레벨 순서대로 노드 값을 저장하고, 결과를 반환합니다. + * 빈 트리인 경우 빈 배열을 반환합니다. + */ +var levelOrder = function (root) { + if (!root) return []; // 빈 트리인 경우 빈 배열 반환 + + const result = []; + const queue = [root]; // 루트 노드를 큐에 넣는다. + + while (queue.length) { + const level = []; + const len = queue.length; + + // 큐에 있는 노드를 모두 빼내서 level 배열에 넣는다. + for (let i = 0; i < len; i++) { + const node = queue.shift(); + level.push(node.val); + + // 왼쪽 자식 노드와 오른쪽 자식 노드를 큐에 넣는다. + if (node.left) queue.push(node.left); + if (node.right) queue.push(node.right); + } + + result.push(level); + } + + return result; +}; diff --git a/wjsdncl/1046_last_stone_weight.js b/wjsdncl/1046_last_stone_weight.js new file mode 100644 index 0000000..8fec0bf --- /dev/null +++ b/wjsdncl/1046_last_stone_weight.js @@ -0,0 +1,26 @@ +/** + * 21m44s 소요 + * + * 1. sort()로 내림차순 정렬한다. + * 2. 가장 큰 두개를 빼고 뺀 값이 0이 아니면 다시 배열에 넣는다. + * 3. 2를 반복한다. 배열의 길이가 1이하면 종료한다. + * 4. 배열의 길이가 0이면 0을 반환하고 아니면 배열의 첫번째 값을 반환한다. + */ +var lastStoneWeight = function (stones) { + // 내림차순 정렬 후 가장 큰 두개를 빼고 뺀 값이 0이 아니면 다시 배열에 넣는다. (O(n^2)) + while (stones.length > 1) { + // 내림차순 정렬 (O(n log n)) + stones.sort((a, b) => b - a); + + // 가장 큰 두개를 빼고 뺀 값이 0이 아니면 다시 배열에 넣는다. (O(n)) + const first = stones.shift(); + const second = stones.shift(); + + // 뺀 값이 0이 아니면 다시 배열에 넣는다. + if (first !== second) { + stones.push(first - second); + } + } + + return stones.length ? stones[0] : 0; +}; diff --git a/wjsdncl/104_maximum_depth_of_binary_tree.js b/wjsdncl/104_maximum_depth_of_binary_tree.js new file mode 100644 index 0000000..cf5e10d --- /dev/null +++ b/wjsdncl/104_maximum_depth_of_binary_tree.js @@ -0,0 +1,22 @@ +/** + * 38m21s + * + * 시간 복잡도: O(n) + * + * 이진 트리의 최대 깊이를 구하는 문제입니다. + * 재귀적으로 왼쪽 서브트리와 오른쪽 서브트리의 최대 깊이를 구하고 더 큰 값에 1을 더해 반환합니다. + * 루트가 null인 경우 깊이는 0입니다. + */ +var maxDepth = function (root) { + // 루트가 null인 경우, 깊이는 0 + if (root === null) { + return 0; + } + + // 왼쪽과 오른쪽 서브트리의 최대 깊이를 구함 + const leftDepth = maxDepth(root.left); + const rightDepth = maxDepth(root.right); + + // 더 큰 깊이에 1을 더해 반환 + return Math.max(leftDepth, rightDepth) + 1; +}; diff --git a/wjsdncl/110_balanced_binary_tree.js b/wjsdncl/110_balanced_binary_tree.js new file mode 100644 index 0000000..b96cc68 --- /dev/null +++ b/wjsdncl/110_balanced_binary_tree.js @@ -0,0 +1,29 @@ +/** + * 35m 33s 소요 + * + * 시간복잡도: O(n) + * + * 이진 트리 공부좀 더 해야할 거 같습니다 잘 모르겠어서 풀이를 참고했습니다. + * + * 1. 재귀를 이용하여 트리의 높이를 구합니다. + * 2. 재귀적으로 왼쪽과 오른쪽 자식 노드의 높이를 비교합니다. + * 3. 높이 차이가 1 이상이면 false를 반환합니다. + * 4. 높이 차이가 1 이하이면 현재 노드의 높이를 반환합니다. + */ +var isBalanced = function (root) { + const checkHeight = (node) => { + if (node === null) return 0; + + const leftHeight = checkHeight(node.left); + if (leftHeight === -1) return -1; + + const rightHeight = checkHeight(node.right); + if (rightHeight === -1) return -1; + + if (Math.abs(leftHeight - rightHeight) > 1) return -1; + + return Math.max(leftHeight, rightHeight) + 1; + }; + + return checkHeight(root) !== -1; +}; diff --git a/wjsdncl/121_best_time_to_buy_and_sell_stock.js b/wjsdncl/121_best_time_to_buy_and_sell_stock.js new file mode 100644 index 0000000..e692356 --- /dev/null +++ b/wjsdncl/121_best_time_to_buy_and_sell_stock.js @@ -0,0 +1,26 @@ +/* +/ 16분 소요 +/ +/ 시간복잡도: O(n) +/ +/ 최소값과 최대 이익을 저장해두고 배열을 순회하며 최소값과 최대 이익을 갱신합니다. +/ 마지막 최대 이익을 반환합니다. +*/ +var maxProfit = function (prices) { + let minPrice = prices[0]; + let maxProfit = 0; + + // 배열 순회 (O(n)) + for (let i = 1; i < prices.length; i++) { + // 최소값 갱신 + if (prices[i] < minPrice) { + minPrice = prices[i]; + } + // 최대 이익 갱신 + else if (prices[i] - minPrice > maxProfit) { + maxProfit = prices[i] - minPrice; + } + } + + return maxProfit; +}; diff --git a/wjsdncl/128_longest_consecutive_sequence.js b/wjsdncl/128_longest_consecutive_sequence.js new file mode 100644 index 0000000..64d9f25 --- /dev/null +++ b/wjsdncl/128_longest_consecutive_sequence.js @@ -0,0 +1,59 @@ +/** + * 26m13s 소요 + * + * 시간복잡도: O(n) + * + * 배열이 주어졌을 때 연속된 숫자로 이루어진 가장 긴 부분 배열의 길이를 구하는 문제입니다. + * 기존에 풀었던 방식은 O(n log n)이 소요되는데 O(n)으로 줄일 방법을 잘 모르겠어서 풀이를 참고했습니다. + */ +var longestConsecutive = function (nums) { + if (nums.length === 0) return 0; + + // 중복된 숫자를 제거합니다. + let set = new Set(nums); + let max = 1; + + for (let num of set) { + // num - 1이 set에 없다면 num을 시작으로 연속된 숫자들을 찾습니다. + if (!set.has(num - 1)) { + let currentMax = 1; + + // num + 1이 set에 있다면 연속된 숫자를 찾습니다. + while (set.has(num + 1)) { + num++; + currentMax++; + } + + // 최대길이를 구합니다. + max = Math.max(max, currentMax); + } + } + + return max; +}; + +/* +기존 풀이 방법 + +var longestConsecutive = function (nums) { + if (nums.length === 0) return 0; + + 중복된 숫자를 제거하고 오름차순으로 정렬합니다. + const arr = Array.from(new Set(nums)).sort((a, b) => a - b); + let count = 1; + let long = 1; + + for (let i = 0; i < arr.length; i++) { + 현재 숫자가 이전 숫자보다 1 크다면 연속된 숫자입니다. + if (arr[i] === arr[i - 1] + 1) { + count++; + } else { + 연속된 숫자가 아니라면 최대값을 갱신합니다. + long = Math.max(long, count); + count = 1; + } + } + + return Math.max(long, count); +}; +*/ diff --git a/wjsdncl/136_single_number.js b/wjsdncl/136_single_number.js new file mode 100644 index 0000000..d83442f --- /dev/null +++ b/wjsdncl/136_single_number.js @@ -0,0 +1,19 @@ +/** + * 12m 21s 소요 + * + * 시간복잡도: O(n) + * + * 1. XOR 연산을 이용한다. + * 2. XOR 연산은 두 값이 같으면 0, 다르면 1을 반환한다. + * 3. 배열의 모든 요소를 XOR 연산하면 한번만 나오는 요소만 남는다. + * 4. 남은 요소를 반환한다. + */ +var singleNumber = function (nums) { + let result = 0; + + for (let num of nums) { + result ^= num; + } + + return result; +}; diff --git a/wjsdncl/139_word_break.js b/wjsdncl/139_word_break.js new file mode 100644 index 0000000..5dcb8c9 --- /dev/null +++ b/wjsdncl/139_word_break.js @@ -0,0 +1,33 @@ +/** + * 13m38s 소요 + * + * 시간복잡도: O(n^2) + * + * 단어 s가 주어지고, 단어 사전 wordDict가 주어집니다. + * 단어 s를 단어 사전을 이용하여 구성할 수 있는지 여부를 반환하는 문제입니다. + * + * dp를 사용하여 풀 수 있습니다. + * dp[i]는 s의 0부터 i까지의 부분 문자열이 단어 사전을 이용하여 구성 가능한지 여부를 나타냅니다. + * dp[0]은 빈 문자열이므로 true로 초기화합니다. + * dp[i]를 구하기 위해 dp[j]를 검사합니다. (0 <= j < i) + * dp[j]가 true이고, s의 j부터 i까지의 부분 문자열이 단어 사전에 있는 경우 dp[i]를 true로 설정합니다. + * dp[s.length]를 반환합니다. + */ +var wordBreak = function (s, wordDict) { + const wordSet = new Set(wordDict); // 빠른 검색을 위한 Set 생성 + const dp = new Array(s.length + 1).fill(false); + dp[0] = true; // 빈 문자열은 항상 구성 가능 + + // dp[i]를 구하기 위해 dp[j]를 검사 + for (let i = 1; i <= s.length; i++) { + // dp[j]가 true이고, s의 j부터 i까지의 부분 문자열이 단어 사전에 있는 경우 dp[i]를 true로 설정 + for (let j = 0; j < i; j++) { + if (dp[j] && wordSet.has(s.slice(j, i))) { + dp[i] = true; + break; // 가능한 경우를 찾으면 더 이상 탐색하지 않음 + } + } + } + + return dp[s.length]; +}; diff --git a/wjsdncl/141_linked_list_cycle.js b/wjsdncl/141_linked_list_cycle.js new file mode 100644 index 0000000..4c46aff --- /dev/null +++ b/wjsdncl/141_linked_list_cycle.js @@ -0,0 +1,30 @@ +/** + * 32m15s + * + * 시간 복잡도: O(n) + * + * 찾아보니 토끼와 거북이 알고리즘으로 풀 수 있는 문제였습니다. + * + * 두 포인터를 이용하여 사이클이 존재하는지 확인합니다. + * slow는 한 칸씩 fast는 두 칸씩 이동합니다. + * 만약 slow와 fast가 만나면 사이클이 존재합니다. + * 만나지 않으면 사이클이 존재하지 않습니다. + */ +var hasCycle = function (head) { + let slow = head; + let fast = head; + + // fast와 fast.next가 null이 아닌 동안 반복 + while (fast !== null && fast.next !== null) { + slow = slow.next; // slow는 한 칸 이동 + fast = fast.next.next; // fast는 두 칸 이동 + + // slow와 fast가 만나는 경우 사이클 존재 + if (slow === fast) { + return true; + } + } + + // 반복이 끝날 때까지 만나지 않으면 사이클이 없음 + return false; +}; diff --git a/wjsdncl/150_evaluate_reverse_polish_notation.js b/wjsdncl/150_evaluate_reverse_polish_notation.js new file mode 100644 index 0000000..ea15c9d --- /dev/null +++ b/wjsdncl/150_evaluate_reverse_polish_notation.js @@ -0,0 +1,43 @@ +/** + * 19m26s 소요 + * + * 시간복잡도 : O(n) + * + * 배열로 주어진 reverse polish notation을 계산하는 문제입니다. + * reverse polish notation를 보고 어려운 줄 알았는데 그냥 순서대로 계산만 하면 되는 문제였습니다. + * + * 숫자인 경우 스택에 push하고, 연산자인 경우 스택에서 두 숫자를 pop하여 연산한 결과를 다시 스택에 push합니다. + * 최종적으로 스택에 남아 있는 값을 반환합니다. + */ +var evalRPN = function (tokens) { + const stack = []; + + for (let token of tokens) { + if (!isNaN(token)) { + // 숫자인 경우 스택에 push + stack.push(Number(token)); + } else { + // 연산자인 경우 스택에서 두 숫자를 pop + const b = stack.pop(); + const a = stack.pop(); + + switch (token) { + case "+": + stack.push(a + b); + break; + case "-": + stack.push(a - b); + break; + case "*": + stack.push(a * b); + break; + case "/": + stack.push(parseInt(a / b)); + break; + } + } + } + + // 스택에 남아 있는 최종 결과 반환 + return stack.pop(); +}; diff --git a/wjsdncl/155_min_stack.js b/wjsdncl/155_min_stack.js new file mode 100644 index 0000000..5d42e87 --- /dev/null +++ b/wjsdncl/155_min_stack.js @@ -0,0 +1,33 @@ +/** + * 31m 11s 소요 + * + * 시간복잡도 : O(1) + * + * 지피티와 함께 풀었습니다 어렵네요 + */ +var MinStack = function () { + this.stack = []; + this.minStack = []; +}; + +MinStack.prototype.push = function (val) { + this.stack.push(val); + if (this.minStack.length === 0 || val <= this.minStack[this.minStack.length - 1]) { + this.minStack.push(val); + } +}; + +MinStack.prototype.pop = function () { + const poppedValue = this.stack.pop(); + if (poppedValue === this.minStack[this.minStack.length - 1]) { + this.minStack.pop(); + } +}; + +MinStack.prototype.top = function () { + return this.stack[this.stack.length - 1]; +}; + +MinStack.prototype.getMin = function () { + return this.minStack[this.minStack.length - 1]; +}; diff --git a/wjsdncl/1584_min_cost_to_connect_all_points.js b/wjsdncl/1584_min_cost_to_connect_all_points.js new file mode 100644 index 0000000..1f0aad6 --- /dev/null +++ b/wjsdncl/1584_min_cost_to_connect_all_points.js @@ -0,0 +1,50 @@ +/** + * 38m17s 소요 + * + * 시간 복잡도: O(N^2) + * + * 어떻게 풀어야 할지 감이 안와서 풀이와 GPT의 도움을 받았습니다. + * Prim's Algorithm 또는 Kruskal's Algorithm 중 하나를 사용하여 풀 수 있다고 합니다. + * + * 모든 점들을 연결하는 최소 비용을 구하는 문제입니다. + * + * 시작점을 0으로 설정하고, 시작점에서 가장 가까운 점을 선택하여 MST에 추가합니다. + * 이후 추가된 점에서 가장 가까운 점을 선택하여 MST에 추가합니다. + * 이를 반복하여 모든 점을 연결할 때까지 반복합니다. + * 최종적으로 MST에 추가된 비용의 합을 반환합니다. + */ +var minCostConnectPoints = function (points) { + const n = points.length; + const minCost = new Array(n).fill(Infinity); // 최소 비용 배열 + const visited = new Array(n).fill(false); // 방문 여부 + minCost[0] = 0; // 시작점의 비용은 0 + let totalCost = 0; + + // 모든 점을 연결할 때까지 반복 + for (let i = 0; i < n; i++) { + let currMin = Infinity; + let currPoint = -1; + + // 최소 비용으로 연결할 점 선택 + for (let j = 0; j < n; j++) { + if (!visited[j] && minCost[j] < currMin) { + currMin = minCost[j]; + currPoint = j; + } + } + + // 현재 점을 MST에 추가 + visited[currPoint] = true; + totalCost += currMin; + + // 다른 점들의 최소 비용 업데이트 + for (let j = 0; j < n; j++) { + if (!visited[j]) { + const dist = Math.abs(points[currPoint][0] - points[j][0]) + Math.abs(points[currPoint][1] - points[j][1]); + minCost[j] = Math.min(minCost[j], dist); + } + } + } + + return totalCost; +}; diff --git a/wjsdncl/15_3sum.js b/wjsdncl/15_3sum.js new file mode 100644 index 0000000..b1b5fba --- /dev/null +++ b/wjsdncl/15_3sum.js @@ -0,0 +1,54 @@ +/* +/ 42분 소요 +/ +/ 시간복잡도: O(n^2) +/ +/ 투 포인터 알고리즘을 몰라서 푸는데 시간이 오래 걸렸습니다. +/ 막 풀다가 for문 3개로 풀려고 했는데 시간 복잡도가 O(n^3)이 되어버려서 이건 좀... 싶었습니다. +/ 좀 찾아보니 투 포인터 알고리즘을 찾아서 풀 수 있었습니다. +/ 알고리즘을 잘 몰라서 공부 좀 해야 할 것 같습니다. +/ +/ 배열을 오름차순으로 정렬한 뒤, 배열을 순회하며 투 포인터 알고리즘을 사용하여 3개의 숫자의 합이 0이 되는 경우를 찾습니다. +/ 중복된 숫자는 건너뛰고, 결과 배열에 추가합니다. +*/ +var threeSum = function (nums) { + // nums 배열을 오름차순으로 정렬 (O(n log n)) + nums.sort((a, b) => a - b); + const result = []; + + // 배열 순회 (O(n)) + for (let i = 0; i < nums.length - 2; i++) { + // 중복된 숫자는 건너뛰기 + if (i > 0 && nums[i] === nums[i - 1]) continue; + + let left = i + 1; + let right = nums.length - 1; + + // 투 포인터 알고리즘 (O(n)) + while (left < right) { + const sum = nums[i] + nums[left] + nums[right]; + + // 0보다 작으면 start++ + if (sum < 0) { + left++; + } + // 0보다 크면 end-- + else if (sum > 0) { + right--; + } + // 0이면 결과 배열에 추가 + else { + result.push([nums[i], nums[left], nums[right]]); + + // 현재 요소와 다음/이전 요소가 같으면 건너뛰기 + while (left < right && nums[left] === nums[left + 1]) left++; + while (left < right && nums[right] === nums[right - 1]) right--; + + left++; + right--; + } + } + } + + return result; +}; diff --git a/wjsdncl/191_number_of_1_bits.js b/wjsdncl/191_number_of_1_bits.js new file mode 100644 index 0000000..156a3a6 --- /dev/null +++ b/wjsdncl/191_number_of_1_bits.js @@ -0,0 +1,21 @@ +/** + * 14m32s 소요 + * + * 시간복잡도: O(n) n = 1의 개수 + * + * 비트 연산자를 사용하는게 오랜만이라 기억이 안나서 찾아보았습니다. + * 찾아보면서 Brian Kernighan's 알고리즘을 알게 되었습니다. + * 이 알고리즘은 n과 n-1을 AND 연산하면 1의 개수가 1개 줄어든다는 것을 이용합니다. + * + * n이 0이 될 때까지 n과 n-1을 AND 연산하고, 1의 개수를 세어 반환합니다. + */ +var hammingWeight = function (n) { + let count = 0; + + while (n !== 0) { + n &= n - 1; + count++; + } + + return count; +}; diff --git a/wjsdncl/1_two_sum.js b/wjsdncl/1_two_sum.js new file mode 100644 index 0000000..b4473ed --- /dev/null +++ b/wjsdncl/1_two_sum.js @@ -0,0 +1,31 @@ +/** + * 20m31s 소요 + * + * 시간 복잡도: O(n) + * + * 중첩 for문을 사용해서 만들었는데 시간 복잡도가 O(n^2)이 되어버렸습니다. + * 시간복잡도를 O(n)으로 줄이기 위해 해시맵을 사용하여 풀었습니다. + * + * 해시맵을 사용하여 타겟에서 현재 숫자를 뺀 값을 키로 인덱스를 값으로 저장합니다. + * 현재 숫자와 뺀 값이 해시맵에 존재하면 해당 값을 반환합니다. + * 존재하지 않으면 현재 숫자와 인덱스를 해시맵에 저장합니다. + */ +var twoSum = function (nums, target) { + // 값을 저장할 해시맵 초기화 + const hashMap = {}; + + // 배열을 인덱스와 함께 순회 + for (let i = 0; i < nums.length; i++) { + // 현재 숫자와 더해서 타겟이 되는 값을 계산 + const complement = target - nums[i]; + + // 해시맵에 해당 값(complement)이 존재하는지 확인 + if (hashMap.hasOwnProperty(complement)) { + // 존재하면 현재 인덱스와 해시맵에 저장된 인덱스를 반환 + return [hashMap[complement], i]; + } + + // 존재하지 않으면 현재 숫자와 그 인덱스를 해시맵에 저장 + hashMap[nums[i]] = i; + } +}; diff --git a/wjsdncl/2013_detect_squares.js b/wjsdncl/2013_detect_squares.js new file mode 100644 index 0000000..cdf3471 --- /dev/null +++ b/wjsdncl/2013_detect_squares.js @@ -0,0 +1,51 @@ +/** + * 51m18s 소요 + * + * 시간복잡도: O(n) (n: 점의 개수) + * + * 주어진 점들로부터 정사각형을 만들 수 있는 경우의 수를 구하는 문제입니다. + * + * 어떻게 풀어야 할지 감이 전혀 안와서 풀이랑 GPT의 도움을 받았습니다. + * + * 점들의 빈도를 저장하기 위한 맵을 생성합니다. + * 새로운 점을 추가할 때마다 빈도를 증가시킵니다. + * 정사각형을 만들 수 있는 경우의 수를 구합니다. + * 대각선의 조건을 만족하는 점들을 찾습니다. + * 대각선 점을 기준으로 나머지 두 점의 빈도를 곱하여 더합니다. + * 결과를 반환합니다. + */ +var DetectSquares = function () { + // 점들의 빈도를 저장하기 위한 맵 + this.points = new Map(); +}; + +DetectSquares.prototype.add = function (point) { + const [x, y] = point; + const key = `${x},${y}`; + if (!this.points.has(key)) { + this.points.set(key, 0); + } + this.points.set(key, this.points.get(key) + 1); +}; + +DetectSquares.prototype.count = function (point) { + const [px, py] = point; + let count = 0; + + // 모든 점을 탐색하며 정사각형의 가능성을 확인 + for (let [key, freq] of this.points) { + const [x, y] = key.split(",").map(Number); + + // x나 y가 같으면 대각선이 될 수 없으므로 스킵 + if (x === px || y === py) continue; + + // 대각선의 조건: 두 점 간의 x, y 차이가 같아야 함 + if (Math.abs(x - px) === Math.abs(y - py)) { + // 대각선 점을 기준으로 나머지 두 점의 빈도를 곱함 + const point1 = `${x},${py}`; + const point2 = `${px},${y}`; + count += (this.points.get(point1) || 0) * (this.points.get(point2) || 0) * freq; + } + } + return count; +}; diff --git a/wjsdncl/202_happy_number.js b/wjsdncl/202_happy_number.js new file mode 100644 index 0000000..08bafe9 --- /dev/null +++ b/wjsdncl/202_happy_number.js @@ -0,0 +1,38 @@ +/** + * 29m21s + * + * 시간 복잡도: O(log n) + * + * 어떻게 풀어야 할지 감이 안 잡혀서 챗지피티에게 도움을 받았습니다 + * + * 이 문제도 토끼와 거북이 알고리즘을 사용하여 풀 수 있습니다. + * + * slow는 n으로 초기화하고 fast는 n의 다음 수로 초기화합니다. + * slow와 fast가 만나지 않을 때까지 반복합니다. + * 만약 fast가 1이면 true를 반환합니다. + * 그렇지 않으면 false를 반환합니다. + */ +var isHappy = function (n) { + // 각 자리의 제곱합을 계산하는 함수 + const getNext = (num) => { + let sum = 0; + while (num > 0) { + const digit = num % 10; + sum += digit * digit; + num = Math.floor(num / 10); + } + return sum; + }; + + let slow = n; + let fast = getNext(n); + + // 사이클이 감지될 때까지 반복 + while (fast !== 1 && slow !== fast) { + slow = getNext(slow); + fast = getNext(getNext(fast)); + } + + // fast가 1이면 true, 그렇지 않으면 false + return fast === 1; +}; diff --git a/wjsdncl/207_course_schedule.js b/wjsdncl/207_course_schedule.js new file mode 100644 index 0000000..3263bbb --- /dev/null +++ b/wjsdncl/207_course_schedule.js @@ -0,0 +1,40 @@ +/** + * 39m12s 소요 + * + * 시간 복잡도: O(n + m) (n: numCourses, m: prerequisites.length) + * + * n개의 과목이 있을 때 모든 과목을 수강할 수 있는지 확인하는 문제입니다. + * + * 이것도 어떻게 풀어야 할지 전혀 모르겠어서 풀이랑 ChatGPT에게 도움을 받았습니다. + * 사이클이 존재하는지 확인하는 문제라고 합니다. 사이클이 있으면 모든 과목을 수강할 수 없습니다. + */ +var canFinish = function (numCourses, prerequisites) { + // 그래프 초기화 + const graph = Array.from({ length: numCourses }, () => []); + for (let [a, b] of prerequisites) { + graph[b].push(a); + } + + // 방문 상태 배열 (0: 방문 안 함, 1: 방문 중, 2: 방문 완료) + const visited = new Array(numCourses).fill(0); + + // DFS를 사용하여 사이클 탐지 + const hasCycle = (node) => { + if (visited[node] === 1) return true; // 사이클 발견 + if (visited[node] === 2) return false; // 이미 방문 완료된 노드 + + visited[node] = 1; // 현재 노드 방문 중 + for (let neighbor of graph[node]) { + if (hasCycle(neighbor)) return true; + } + visited[node] = 2; // 방문 완료 + return false; + }; + + // 모든 노드에 대해 DFS 수행 + for (let i = 0; i < numCourses; i++) { + if (hasCycle(i)) return false; // 사이클 발견 시 false 반환 + } + + return true; // 모든 노드 탐색 후 사이클이 없으면 true 반환 +}; diff --git a/wjsdncl/208_implement_trie.js b/wjsdncl/208_implement_trie.js new file mode 100644 index 0000000..b17d3f2 --- /dev/null +++ b/wjsdncl/208_implement_trie.js @@ -0,0 +1,51 @@ +/** + * 24m12s 소요 + * + * 시간복잡도: O(m) (m: 문자열의 길이) + * + * Trie 구조를 처음 접해봐서 풀이와 ChatGPT의 도움을 받았습니다. + * + * Trie 구조를 구현하는 문제입니다. + * Trie 구조는 문자열을 저장하고 검색하는 데 사용되는 트리 자료구조입니다. + */ +var TrieNode = function () { + this.children = {}; + this.isEnd = false; +}; + +var Trie = function () { + this.root = new TrieNode(); +}; + +Trie.prototype.insert = function (word) { + let node = this.root; + for (let char of word) { + if (!node.children[char]) { + node.children[char] = new TrieNode(); + } + node = node.children[char]; + } + node.isEnd = true; +}; + +Trie.prototype.search = function (word) { + let node = this.root; + for (let char of word) { + if (!node.children[char]) { + return false; + } + node = node.children[char]; + } + return node.isEnd; +}; + +Trie.prototype.startsWith = function (prefix) { + let node = this.root; + for (let char of prefix) { + if (!node.children[char]) { + return false; + } + node = node.children[char]; + } + return true; +}; diff --git a/wjsdncl/20_valid_parentheses.js b/wjsdncl/20_valid_parentheses.js new file mode 100644 index 0000000..5188ce1 --- /dev/null +++ b/wjsdncl/20_valid_parentheses.js @@ -0,0 +1,39 @@ +/** + * 22m41s + * + * 시간 복잡도: O(n) + * + * 맵을 사용해서 괄호의 짝을 저장하고 스택을 사용해서 괄호의 짝이 맞는지 확인하는 방법으로 풀었습니다. + * + * 스택을 사용해서 여는 괄호를 저장하고 닫는 괄호를 만나면 스택에서 가장 최근의 여는 괄호를 꺼내서 짝이 맞는지 확인합니다. + * 짝이 맞지 않으면 false를 반환합니다. + * 모든 괄호를 확인한 후 스택이 비어 있으면 true를 반환합니다. + * 스택이 비어 있지 않으면 false를 반환합니다. + */ +var isValid = function (s) { + const stack = []; + // 괄호의 짝을 저장한 맵 + const map = { + ")": "(", + "}": "{", + "]": "[", + }; + + for (let char of s) { + // 닫는 괄호인 경우 + if (map[char]) { + // 스택에서 가장 최근의 여는 괄호를 꺼냄 + const topElement = stack.length ? stack.pop() : "#"; + // 짝이 맞지 않으면 false 반환 + if (topElement !== map[char]) { + return false; + } + } else { + // 여는 괄호는 스택에 추가 + stack.push(char); + } + } + + // 스택이 비어 있어야 모든 괄호가 짝을 이룬 것 + return stack.length === 0; +}; diff --git a/wjsdncl/213_house_robber_ii.js b/wjsdncl/213_house_robber_ii.js new file mode 100644 index 0000000..ac892b5 --- /dev/null +++ b/wjsdncl/213_house_robber_ii.js @@ -0,0 +1,29 @@ +/** + * 36m21s 소요 + * + * 시간복잡도: O(n) + * + * 집이 원형으로 배치되어 있고 각 집에는 돈이 들어있습니다. 인접한 두 집을 털면 경보가 울립니다. 경보를 울리지 않고 가장 많은 돈을 훔치는 문제입니다. + * + * 어떻게 풀어야 할지 감이 잡히지 않아서 풀이를 참고했습니다. + */ +var rob = function (nums) { + if (nums.length === 1) return nums[0]; // 집이 한 개인 경우 집을 털고 종료 + + const dp = Array(nums.length).fill(0); // 첫 번째 집을 턴 경우 + const dp2 = Array(nums.length).fill(0); // 첫 번째 집을 털지 않은 경우 + + dp[0] = nums[0]; // 첫 번째 집을 털었을 때의 금액 + dp[1] = Math.max(nums[0], nums[1]); // 첫 번째 혹은 두 번째 집을 털었을 때 최대 금액 + + dp2[0] = 0; // 첫 번째 집을 털지 않으므로 0 + dp2[1] = nums[1]; // 두 번째 집을 털었을 때의 금액 + + for (let i = 2; i < nums.length; i++) { + dp[i] = Math.max(dp[i - 1], dp[i - 2] + nums[i]); // 첫 번째 집을 털었을 때의 최대 금액 + dp2[i] = Math.max(dp2[i - 1], dp2[i - 2] + nums[i]); // 첫 번째 집을 털지 않았을 때의 최대 금액 + } + + // 첫 번째 집을 털었을 때와 첫 번째 집을 털지 않았을 때 중 최대 금액을 반환 + return Math.max(dp[nums.length - 2], dp2[nums.length - 1]); +}; diff --git a/wjsdncl/215_kth_largest_element_in_an_array.js b/wjsdncl/215_kth_largest_element_in_an_array.js new file mode 100644 index 0000000..a078ebe --- /dev/null +++ b/wjsdncl/215_kth_largest_element_in_an_array.js @@ -0,0 +1,27 @@ +/** + * 13m25s 소요 + * + * 시간복잡도: O(N log K) (N: nums의 길이, K: k) + * + * 배열에서 k번째로 큰 요소를 찾는 문제입니다. + * + * 최소 힙을 사용하여 풀 수 있습니다. + * 배열을 순회하며 힙에 요소를 삽입합니다. + * 힙 크기가 k를 초과하면 가장 작은 값을 제거합니다. + * 힙의 첫 번째 요소가 k번째로 큰 값입니다 + */ +var findKthLargest = function (nums, k) { + const minHeap = new MinPriorityQueue(); // 최소 힙 생성 + + for (let num of nums) { + minHeap.enqueue(num); // 힙에 삽입 + + // 힙 크기가 k를 초과하면 가장 작은 값 제거 + if (minHeap.size() > k) { + minHeap.dequeue(); + } + } + + // 힙의 첫 번째 요소가 k번째로 큰 값 + return minHeap.front().element; +}; diff --git a/wjsdncl/217_contains_duplicate.js b/wjsdncl/217_contains_duplicate.js new file mode 100644 index 0000000..a25dff2 --- /dev/null +++ b/wjsdncl/217_contains_duplicate.js @@ -0,0 +1,19 @@ +/* +/ 22분 소요 +/ +/ 시간 복잡도: O(n log n) +/ +/ 이중 for문 이용해서 풀었는데 Time Limit Exceeded가 나와서 다시 풀었습니다. +/ +/ 배열을 오름차순으로 정렬한 뒤, 배열을 순회하며 중복된 요소가 있는지 확인합니다. +/ 중복된 요소가 있다면 true를 반환하고, 없다면 false를 반환합니다. +*/ +var containsDuplicate = function (nums) { + nums.sort((a, b) => a - b); // nums 배열을 오름차순으로 정렬 (O(n log n)) + + // 정렬된 nums 배열을 순회 (O(n)) + for (let i = 0; i < nums.length; i++) { + if (nums[i] === nums[i + 1]) return true; // 현재 요소와 다음 요소가 같다면 true 반환 + } + return false; // 중복된 요소가 없다면 false 반환 +}; diff --git a/wjsdncl/21_merge_two_sorted_lists.js b/wjsdncl/21_merge_two_sorted_lists.js new file mode 100644 index 0000000..7bc5d45 --- /dev/null +++ b/wjsdncl/21_merge_two_sorted_lists.js @@ -0,0 +1,35 @@ +/** + * 28m11s 소요 + * + * 시간복잡도: O(n) + * + * 어떻게 풀어야할지 모르겠어서 찾아봐서 풀었습니다. + * 노드 어렵네요. + * + * 1. 두 리스트를 비교하면서 작은 값을 새로운 리스트에 추가합니다. + * 2. 작은 값을 추가한 리스트의 다음 값을 작은 값이 아닌 리스트로 변경합니다. + * 3. 1, 2를 반복합니다. + * 4. 작은 값이 없는 리스트를 추가합니다. + * 5. 새로운 리스트를 반환합니다. + */ +var mergeTwoLists = function (list1, list2) { + let head = new ListNode(); + let current = head; + + // 두 리스트를 비교하면서 작은 값을 새로운 리스트에 추가 (O(n)) + while (list1 && list2) { + if (list1.val < list2.val) { + current.next = list1; + list1 = list1.next; + } else { + current.next = list2; + list2 = list2.next; + } + + current = current.next; + } + + current.next = list1 || list2; + + return head.next; +}; diff --git a/wjsdncl/226_invert_binary_tree.js b/wjsdncl/226_invert_binary_tree.js new file mode 100644 index 0000000..7b208c7 --- /dev/null +++ b/wjsdncl/226_invert_binary_tree.js @@ -0,0 +1,17 @@ +/** + * 13m23s 소요 + * + * 시간복잡도: O(n) + * + * 이진 트리가 주어졌을 때 이진 트리를 좌우반전한 결과를 반환하는 문제입니다. + */ +var invertTree = function (root) { + if (!root) return null; + + // 왼쪽과 오른쪽을 재귀적으로 바꿔줍니다. + let temp = root.left; + root.left = invertTree(root.right); + root.right = invertTree(temp); + + return root; +}; diff --git a/wjsdncl/235_lowest_common_ancestor_of_a_binary_search_tree.js b/wjsdncl/235_lowest_common_ancestor_of_a_binary_search_tree.js new file mode 100644 index 0000000..c2aa1c5 --- /dev/null +++ b/wjsdncl/235_lowest_common_ancestor_of_a_binary_search_tree.js @@ -0,0 +1,32 @@ +/** + * 28m41s 소요 + * + * 시간복잡도: O(n) (n: 트리의 높이) + * + * 트리 문제라 어려울 줄 알았는데 생각보다 쉬웠습니다. + * + * 이진 탐색 트리에서 두 노드의 가장 가까운 공통 조상을 찾는 문제입니다. + * + * 현재 노드의 값이 두 노드보다 크면 왼쪽으로 이동하고, + * 현재 노드의 값이 두 노드보다 작으면 오른쪽으로 이동합니다. + * 현재 노드가 두 노드 사이에 있는 경우 현재 노드가 최소 공통 조상입니다. + * + * 최소 공통 조상을 찾지 못한 경우 null을 반환합니다. + */ +var lowestCommonAncestor = function (root, p, q) { + while (root) { + // 현재 노드 값이 두 노드보다 크면 왼쪽으로 이동 + if (root.val > p.val && root.val > q.val) { + root = root.left; + } + // 현재 노드 값이 두 노드보다 작으면 오른쪽으로 이동 + else if (root.val < p.val && root.val < q.val) { + root = root.right; + } + // 현재 노드가 LCA인 경우 + else { + return root; + } + } + return null; // 트리 순회가 끝난 경우 +}; diff --git a/wjsdncl/238_product_of_array_except_self.js b/wjsdncl/238_product_of_array_except_self.js new file mode 100644 index 0000000..1e928ea --- /dev/null +++ b/wjsdncl/238_product_of_array_except_self.js @@ -0,0 +1,32 @@ +/** + * 35m16s 소요 + * + * 시간복잡도: O(n) + * + * 나눗셈을 사용 안하고 어떻게 풀어야 할지 감이 안 잡혀서 풀이를 보고 이해했습니다. + * 이해하는 데 시간이 걸렸지만 이해하고 나니 쉬운 문제였습니다. + * + * 왼쪽에서부터 곱을 계산하여 answer에 저장합니다. + * 오른쪽에서부터 곱을 계산하여 answer에 곱해줍니다. + * answer를 반환합니다. + */ +var productExceptSelf = function (nums) { + const n = nums.length; + const answer = new Array(n); + + // 왼쪽에서부터 곱을 계산하여 answer에 저장 (O(n)) + let leftProduct = 1; + for (let i = 0; i < n; i++) { + answer[i] = leftProduct; + leftProduct *= nums[i]; + } + + // 오른쪽에서부터 곱을 계산하여 answer에 곱해줌 (O(n)) + let rightProduct = 1; + for (let i = n - 1; i >= 0; i--) { + answer[i] *= rightProduct; + rightProduct *= nums[i]; + } + + return answer; +}; diff --git a/wjsdncl/242_valid_anagram.js b/wjsdncl/242_valid_anagram.js new file mode 100644 index 0000000..1f0e633 --- /dev/null +++ b/wjsdncl/242_valid_anagram.js @@ -0,0 +1,19 @@ +/** + * 15m33s 소요 + * + * 시간 복잡도: O(n log n) + * + * split을 사용하면 배열로 변환 할 수 있어서 이걸 사용했습니다. + * 배열을 sort로 정렬하고 join으로 다시 문자열로 변환하여 비교했습니다. + * + * 두 문자열의 길이가 다르다면 false를 반환합니다. + * 두 문자열을 배열로 변환하고, 알파벳 순으로 정렬한 후 다시 문자열로 변환하여 비교합니다. + * 두 문자열이 동일하면 애너그램이므로 true를 반환하고 그렇지 않으면 false를 반환합니다. + */ +var isAnagram = function (s, t) { + // 두 문자열의 길이가 다르다면 false 반환 (O(1)) + if (s.length !== t.length) return false; + + // 문자열을 배열로 변환하고 알파벳 순으로 정렬한 후 다시 문자열로 변환하여 비교 (O(n log n)) + return s.split("").sort().join("") === t.split("").sort().join(""); +}; diff --git a/wjsdncl/252_meeting_schedule.js b/wjsdncl/252_meeting_schedule.js new file mode 100644 index 0000000..b3e3a9f --- /dev/null +++ b/wjsdncl/252_meeting_schedule.js @@ -0,0 +1,23 @@ +/* +/ 26분 소요 +/ +/ 시간 복잡도: O(n log n) +/ +/ 시작 시간을 기준으로 오름차순 정렬한 뒤, 배열을 순회하며 현재 회의의 시작 시간이 이전 회의의 종료 시간보다 빠른지 확인합니다. +/ 만약 빠르다면 false를 반환하고, 모든 회의에 대해 충돌이 없을 경우 true를 반환합니다. +*/ +class Solution { + canAttendMeetings(intervals) { + // 시작 시간을 기준으로 오름차순 정렬 (O(n log n)) + intervals.sort((a, b) => a.start - b.start); + + // 배열 순회 (O(n)) + for (let i = 1; i < intervals.length; i++) { + // 현재 회의의 시작 시간이 이전 회의의 종료 시간보다 빠른 경우 false 반환 + if (intervals[i].start < intervals[i - 1].end) return false; + } + + // 모든 회의에 대해 충돌이 없을 경우 true 반환 + return true; + } +} diff --git a/wjsdncl/268_missing_number.js b/wjsdncl/268_missing_number.js new file mode 100644 index 0000000..2de691c --- /dev/null +++ b/wjsdncl/268_missing_number.js @@ -0,0 +1,23 @@ +/* +/ 13분 소요 +/ +/ 시간복잡도: O(n log n) +/ +/ 배열을 오름차순으로 정렬한 뒤, 배열을 순회하며 누락된 숫자를 찾아 반환합니다. +/ 맨 앞의 숫자가 0이 아니라면 0을 반환합니다. +*/ +var missingNumber = function (nums) { + nums.sort((a, b) => a - b); // nums 배열을 오름차순으로 정렬 (O(n log n)) + + // 맨 앞의 숫자가 0이 아닌 경우 0 반환 + if (nums[0] !== 0) return 0; + + // 배열 순회하여 누락된 숫자 반환 (O(n)) + for (let i = 0; i < nums.length - 1; i++) { + if (nums[i + 1] - nums[i] !== 1) return nums[i] + 1; + } + + return 0; +}; + +console.log(missingNumber([3, 0, 1])); // 2 diff --git a/wjsdncl/338_counting_bits.js b/wjsdncl/338_counting_bits.js new file mode 100644 index 0000000..429d2e4 --- /dev/null +++ b/wjsdncl/338_counting_bits.js @@ -0,0 +1,22 @@ +/** + * 17m26s 소요 + * + * 시간복잡도 : O(n log n) + * + * O(n)으로 풀 수 있는거 같은데 잘 모르겠습니다. + * + * 1. 0부터 n까지의 수를 이진수로 변환한다. + * 2. 0의 개수를 세고 배열에 저장한다. + * 3. 배열을 반환한다. + */ +var countBits = function (n) { + const result = []; + + // 0부터 n까지 반복 (O(n)) + for (let i = 0; i <= n; i++) { + // 이진수로 변환 후 0의 개수를 세어 배열에 저장 (O(log n)) + result.push(i.toString(2).replace(/0/g, "").length); + } + + return result; +}; diff --git a/wjsdncl/371_sum_of_two_integers.js b/wjsdncl/371_sum_of_two_integers.js new file mode 100644 index 0000000..013962e --- /dev/null +++ b/wjsdncl/371_sum_of_two_integers.js @@ -0,0 +1,20 @@ +/** + * 17m41s 소요 + * + * 시간복잡도: O(1) + * + * 두 정수 a, b가 주어졌을 때 두 정수의 합을 +, - 연산자를 사용하지 않고 구하는 문제입니다. + * 어떻게 풀어야할지 모르겠어서 ChatGPT의 도움을 받았습니다. + * 비트 연산자를 사용하여 풀 수 있습니다. + */ +var getSum = function (a, b) { + while (b !== 0) { + // 자리 올림 없이 더한 결과를 저장 + let temp = a ^ b; + // 자리 올림 값을 계산 + b = (a & b) << 1; + // a에 자리 올림 없는 결과를 저장 + a = temp; + } + return a; +}; diff --git a/wjsdncl/39_combination_sum.js b/wjsdncl/39_combination_sum.js new file mode 100644 index 0000000..df8d073 --- /dev/null +++ b/wjsdncl/39_combination_sum.js @@ -0,0 +1,33 @@ +/** + * 29m41s 소요 + * + * 시간복잡도: O(2^n) + * + * 주어진 숫자 배열에서 합이 target이 되는 모든 조합을 구하는 문제입니다. + * + * 백트래킹을 사용하여 모든 조합을 구합니다. + * 현재 합이 target과 같으면 결과 배열에 추가합니다. + * 현재 합이 target보다 크면 더 이상 탐색하지 않습니다. + * 현재 합이 target보다 작으면 다음 인덱스부터 탐색합니다. + * 탐색이 끝나면 결과 배열을 반환합니다. + */ +var combinationSum = function (candidates, target) { + const result = []; + + const backtrack = (current, remain, start) => { + if (remain === 0) { + result.push([...current]); + return; + } + if (remain < 0) return; + + for (let i = start; i < candidates.length; i++) { + current.push(candidates[i]); + backtrack(current, remain - candidates[i], i); // 같은 위치에서 다시 시작 + current.pop(); // 탐색이 끝난 후 원래 상태로 복구 + } + }; + + backtrack([], target, 0); + return result; +}; diff --git a/wjsdncl/48_rotate_image.js b/wjsdncl/48_rotate_image.js new file mode 100644 index 0000000..dd74465 --- /dev/null +++ b/wjsdncl/48_rotate_image.js @@ -0,0 +1,28 @@ +/** + * 31m11s 소요 + * + * 시간 복잡도 O(n^2) + * + * 어려워서 찾아봤습니다. 풀이를 보니까 쉬운 문제인데 생각을 못했습니다. + * + * 1. 행과 열을 바꿉니다. (i, j) -> (j, i) + * 2. 열을 뒤집습니다. + * 3. 행렬을 반환합니다. + */ +var rotate = function (matrix) { + const n = matrix.length; + + // 행과 열을 바꿉니다. (i, j) -> (j, i) (O(n^2)) + for (let i = 0; i < n; i++) { + for (let j = i; j < n; j++) { + [matrix[i][j], matrix[j][i]] = [matrix[j][i], matrix[i][j]]; + } + } + + // 열을 뒤집습니다. (O(n^2)) + for (let i = 0; i < n; i++) { + matrix[i].reverse(); + } + + return matrix; +}; diff --git a/wjsdncl/543_diameter_of_binary_tree.js b/wjsdncl/543_diameter_of_binary_tree.js new file mode 100644 index 0000000..e659af5 --- /dev/null +++ b/wjsdncl/543_diameter_of_binary_tree.js @@ -0,0 +1,29 @@ +/** + * 26m53s 소요 + * + * 시간복잡도: O(n) + * + * 이진 트리가 주어졌을 때 가장 긴 경로의 길이를 구하는 문제입니다. + * + * 시도해보다가 잘 모르겠어서 풀이를 참고해서 풀었습니다. + */ +var diameterOfBinaryTree = function (root) { + let diameter = 0; + + const depth = (node) => { + if (!node) return 0; + + // 왼쪽과 오른쪽의 깊이를 재귀적으로 구합니다. + const left = depth(node.left); + const right = depth(node.right); + + // 가장 긴 경로를 찾습니다. + diameter = Math.max(diameter, left + right); + + return Math.max(left, right) + 1; + }; + + depth(root); + + return diameter; +}; diff --git a/wjsdncl/54_spiral_matrix.js b/wjsdncl/54_spiral_matrix.js new file mode 100644 index 0000000..0f8bfc3 --- /dev/null +++ b/wjsdncl/54_spiral_matrix.js @@ -0,0 +1,55 @@ +/** + * 38m 41s 소요 + * + * 시간복잡도: O(n * m) (n: 행의 길이, m: 열의 길이) + * + * 풀어보려고 했는데 어렵네요 풀이 참고해서 풀었습니다. + * + * 1. matrix의 행과 열의 길이를 구한다. + * 2. top이 bottom보다 작거나 같고 left가 right보다 작거나 같을 때까지 반복한다. + * 3. 왼쪽에서 오른쪽으로 이동하면서 result에 값을 추가한다. + * 4. 위에서 아래로 이동하면서 result에 값을 추가한다. + * 5. top이 bottom보다 작거나 같으면 오른쪽에서 왼쪽으로 이동하면서 result에 값을 추가한다. + * 6. left가 right보다 작거나 같으면 아래에서 위로 이동하면서 result에 값을 추가한다. + */ +var spiralOrder = function (matrix) { + const result = []; + if (matrix.length === 0) return result; + + let top = 0; + let bottom = matrix.length - 1; + let left = 0; + let right = matrix[0].length - 1; + + while (top <= bottom && left <= right) { + // 왼쪽에서 오른쪽으로 이동 + for (let i = left; i <= right; i++) { + result.push(matrix[top][i]); + } + top++; + + // 위에서 아래로 이동 + for (let i = top; i <= bottom; i++) { + result.push(matrix[i][right]); + } + right--; + + if (top <= bottom) { + // 오른쪽에서 왼쪽으로 이동 + for (let i = right; i >= left; i--) { + result.push(matrix[bottom][i]); + } + bottom--; + } + + if (left <= right) { + // 아래에서 위로 이동 + for (let i = bottom; i >= top; i--) { + result.push(matrix[i][left]); + } + left++; + } + } + + return result; +}; diff --git a/wjsdncl/56_merge_intervals.js b/wjsdncl/56_merge_intervals.js new file mode 100644 index 0000000..4f138f4 --- /dev/null +++ b/wjsdncl/56_merge_intervals.js @@ -0,0 +1,34 @@ +/** + * 21m41s 소요 + * + * 시간복잡도: O(n log n) + * + * 배열의 모든 겹치는 구간을 병합하고 겹치지 않는 구간은 그대로 반환하는 문제입니다. + * + * 시작점을 기준으로 구간을 정렬하고, 결과 배열에 첫 구간을 넣어둡니다. + * 그 다음 구간부터 시작점이 이전 구간의 끝점보다 작거나 같은 경우 두 구간을 병합하고, + * 그렇지 않은 경우 결과 배열에 추가합니다. + */ +var merge = function (intervals) { + if (intervals.length <= 1) return intervals; + + // 시작점 기준으로 구간 정렬 + intervals.sort((a, b) => a[0] - b[0]); + + const result = [intervals[0]]; // 초기 결과 배열 + + for (let i = 1; i < intervals.length; i++) { + const prev = result[result.length - 1]; // 결과 배열의 마지막 구간 + const curr = intervals[i]; // 현재 구간 + + if (prev[1] >= curr[0]) { + // 겹치는 경우 병합 + prev[1] = Math.max(prev[1], curr[1]); + } else { + // 겹치지 않는 경우 결과 배열에 추가 + result.push(curr); + } + } + + return result; +}; diff --git a/wjsdncl/572_subtree_of_another_tree.js b/wjsdncl/572_subtree_of_another_tree.js new file mode 100644 index 0000000..c55dbf3 --- /dev/null +++ b/wjsdncl/572_subtree_of_another_tree.js @@ -0,0 +1,24 @@ +/** + * 23m41s 소요 + * + * 시간복잡도: O(n) + * + * 두 이진 트리가 주어졌을 때, 두 번째 트리가 첫 번째 트리의 서브트리인지 확인하는 문제입니다. + * + * 빈 트리인 경우 false를 반환합니다. + * 두 트리가 동일한지 확인하는 함수로 두 트리가 동일하지 않은 경우 왼쪽과 오른쪽 자식을 재귀적으로 비교합니다. + * 두 트리가 동일한 경우 true를 반환하고, 그렇지 않은 경우 false를 반환합니다. + */ +var isSubtree = function (root, subRoot) { + if (!root) return false; // 빈 트리인 경우 false 반환 + + // 두 트리가 동일한지 확인하는 함수 + const isSameTree = (R, S) => { + if (!R && !S) return true; // 둘 다 null인 경우 두 트리는 동일 + if (!R || !S || R.val !== S.val) return false; // 둘 중 하나가 null이거나 노드의 값이 다르면 트리는 동일하지 않음 + return isSameTree(R.left, S.left) && isSameTree(R.right, S.right); // 현재 노드가 같다면 왼쪽과 오른쪽 자식을 재귀적으로 비교 + }; + + if (isSameTree(root, subRoot)) return true; // 두 트리가 동일한 경우 true 반환 + return isSubtree(root.left, subRoot) || isSubtree(root.right, subRoot); // 두 트리가 동일하지 않은 경우 왼쪽과 오른쪽 자식을 재귀적으로 비교 +}; diff --git a/wjsdncl/57_Insert_Interval.js b/wjsdncl/57_Insert_Interval.js new file mode 100644 index 0000000..0910d30 --- /dev/null +++ b/wjsdncl/57_Insert_Interval.js @@ -0,0 +1,39 @@ +/** + * 32m31s 소요 + * + * 시간복잡도 : O(n) (n: intervals의 길이) + * + * 구간들의 배열과 새로운 구간이 주어질 때, 구간들을 병합한 결과를 반환하는 문제입니다. + * + * 구간들을 순회하면서 새로운 구간과 겹치는 구간을 병합합니다. + * 새로운 구간이 구간들보다 클 때까지 구간들을 병합합니다. + * 새로운 구간이 구간들보다 작을 때까지 구간들을 병합합니다. + * 나머지 구간들을 결과에 추가합니다. + * 결과를 반환합니다. + */ +var insert = function (intervals, newInterval) { + const result = []; + let i = 0; + + // newInterval 이전의 겹치지 않는 구간 추가 + while (i < intervals.length && intervals[i][1] < newInterval[0]) { + result.push(intervals[i]); + i++; + } + + // newInterval과 겹치는 구간 병합 + while (i < intervals.length && intervals[i][0] <= newInterval[1]) { + newInterval[0] = Math.min(newInterval[0], intervals[i][0]); + newInterval[1] = Math.max(newInterval[1], intervals[i][1]); + i++; + } + result.push(newInterval); // 병합된 구간 추가 + + // newInterval 이후의 겹치지 않는 구간 추가 + while (i < intervals.length) { + result.push(intervals[i]); + i++; + } + + return result; +}; diff --git a/wjsdncl/70_climbing_stairs.js b/wjsdncl/70_climbing_stairs.js new file mode 100644 index 0000000..4753078 --- /dev/null +++ b/wjsdncl/70_climbing_stairs.js @@ -0,0 +1,26 @@ +/** + * 19m54s 소요 + * + * 시간복잡도: O(n) + * + * dp(동적 프로그래밍)를 사용해서 풀 수 있는 문제입니다. + * + * 1. dp 배열을 생성하고 0으로 초기화합니다. + * 2. dp[0]과 dp[1]을 1로 초기화합니다. + * 3. 2부터 n까지 반복하면서 dp[i] = dp[i - 1] + dp[i - 2]를 수행합니다. + * 4. dp[n]을 반환합니다. + */ +var climbStairs = function (n) { + // dp 배열을 생성하고 0으로 초기화 + const dp = Array(n + 1).fill(0); + + dp[0] = 1; + dp[1] = 1; + + // 2부터 n까지 반복 (O(n)) + for (let i = 2; i <= n; i++) { + dp[i] = dp[i - 1] + dp[i - 2]; + } + + return dp[n]; +}; diff --git a/wjsdncl/743_network_delay_time.js b/wjsdncl/743_network_delay_time.js new file mode 100644 index 0000000..fdb152a --- /dev/null +++ b/wjsdncl/743_network_delay_time.js @@ -0,0 +1,58 @@ +/** + * 37m26s 소요 + * + * 시간복잡도: O(n^2) + * + * 그래프 형태로 주어진 배열에서 k번 노드에서 모든 노드로 가는 최단 시간을 구하는 문제입니다. + * + * 어떻게 풀어야 할지 감이 잘 안와서 풀이랑 GPT의 도움을 받았습니다. + * 다익스트라 알고리즘을 사용하면 풀 수 있다고 합니다. + * + * 다익스트라 알고리즘은 최단 경로를 찾는 알고리즘 중 하나입니다. + * 시작 노드에서 다른 노드로 가는 최단 경로를 찾을 때 사용합니다. + * + * 그래프를 초기화합니다. + * 다익스트라 알고리즘을 사용하여 최단 경로를 찾습니다. + * 최단 경로 배열 중 최대값을 찾아 반환합니다. + */ +var networkDelayTime = function (times, n, k) { + const graph = new Map(); // 그래프 생성 + + // 그래프 초기화 + for (let i = 1; i <= n; i++) { + graph.set(i, []); + } + for (let [u, v, w] of times) { + graph.get(u).push([v, w]); + } + + // 다익스트라 알고리즘 + const dist = Array(n + 1).fill(Infinity); + dist[k] = 0; + + const pq = new MinPriorityQueue({ priority: (x) => x[1] }); // [노드, 거리] + pq.enqueue([k, 0]); + + // 우선순위 큐에서 노드를 꺼내면서 최단 거리 갱신 + while (!pq.isEmpty()) { + const [current, time] = pq.dequeue().element; + + // 현재 노드의 거리가 최단 거리보다 크면 무시 + if (time > dist[current]) continue; + + // 인접 노드를 순회하며 최단 거리 갱신 + for (let [neighbor, weight] of graph.get(current)) { + const newTime = time + weight; + + // 최단 거리 갱신 + if (newTime < dist[neighbor]) { + dist[neighbor] = newTime; + pq.enqueue([neighbor, newTime]); + } + } + } + + // 최종 거리 배열에서 최대값 확인 + const result = Math.max(...dist.slice(1)); // 1번 노드부터 n번 노드까지 + return result === Infinity ? -1 : result; +}; diff --git a/wjsdncl/746_min_cost_climbing_stairs.js b/wjsdncl/746_min_cost_climbing_stairs.js new file mode 100644 index 0000000..53fc409 --- /dev/null +++ b/wjsdncl/746_min_cost_climbing_stairs.js @@ -0,0 +1,22 @@ +/** + * 29m35s 소요 + * + * 시간복잡도: O(n) + * + * 감이 안잡혀서 찾아보니 동적 계획법(Dynamic Programming)을 사용하여 풀 수 있다는 것을 알게 되었습니다. + * DP는 복잡한 문제를 간단한 하위 문제로 나누어 푸는 방법입니다. + * + * 배열 끝에서부터 역순으로 최소 비용을 계산합니다. + * i번째 계단에 다음 두 계단 중 작은 비용을 더합니다. + * 0번째 또는 1번째 계단에서 출발할 수 있으므로 둘 중 작은 값을 반환합니다. + */ +var minCostClimbingStairs = function (cost) { + // 배열 끝에서부터 역순으로 최소 비용을 계산 (O(n)) + for (let i = cost.length - 3; i >= 0; i--) { + // i번째 계단에 다음 두 계단 중 작은 비용을 더함 + cost[i] += Math.min(cost[i + 1], cost[i + 2]); + } + + // 0번째 또는 1번째 계단에서 출발할 수 있으므로 둘 중 작은 값을 반환 + return Math.min(cost[0], cost[1]); +}; diff --git a/wjsdncl/7_reverse_integer.js b/wjsdncl/7_reverse_integer.js new file mode 100644 index 0000000..d80628a --- /dev/null +++ b/wjsdncl/7_reverse_integer.js @@ -0,0 +1,20 @@ +/** + * 14m51s 소요 + * + * 시간복잡도 : O(log n) (n: x의 길이) + * + * 32비트 정수 x를 뒤집어서 반환하는 문제입니다. + * + * 32비트 정수의 범위를 벗어나면 0을 반환합니다. + * 숫자를 문자열로 변환하여 뒤집은 뒤, 다시 숫자로 변환합니다. + * 32비트 정수 범위를 벗어나는지 확인합니다. + * 음수인 경우 부호를 복원합니다. + */ +var reverse = function (x) { + const isNegative = x < 0; // 음수 여부 확인 + const reversed = parseInt(Math.abs(x).toString().split("").reverse().join("")); // 절댓값 뒤집기 + + if (reversed > 2 ** 31 - 1) return 0; // 32비트 정수 범위 확인 + + return isNegative ? -reversed : reversed; // 부호 복원 +}; diff --git a/wjsdncl/973_k_closest_points_to_origin.js b/wjsdncl/973_k_closest_points_to_origin.js new file mode 100644 index 0000000..25d780d --- /dev/null +++ b/wjsdncl/973_k_closest_points_to_origin.js @@ -0,0 +1,11 @@ +/** + * 21m 16s 소요 + * + * 시간복잡도: O(n log n) + * + * 1. 배영의 요소를 x^2 + y^2를 기준으로 정렬합니다. + * 2. 가장 k개의 요소를 반환합니다. + */ +var kClosest = function (points, k) { + return points.sort((a, b) => a[0] ** 2 + a[1] ** 2 - (b[0] ** 2 + b[1] ** 2)).slice(0, k); +};