diff --git a/container-with-most-water/jinvicky.java b/container-with-most-water/jinvicky.java new file mode 100644 index 000000000..b9c91fd2a --- /dev/null +++ b/container-with-most-water/jinvicky.java @@ -0,0 +1,26 @@ +/** + * 구하려는 것은 최대 영역값이기 때문에 x,y축을 계산한 현재 영역과 기존 영역을 비교해 max값을 반환하면 됩니다. + * 포인터를 어떻게 좁힐까 생각할 수 있는데 쉽게 말해서 start와 end 중 더 작은 쪽을 앞 또는 뒤로 이동하면 됩니다. + * 영역은 두 막대가 모두 충족가능한 길이가 되어야 하므로 Math.min()으로 설정합니다. + */ +class Solution { + public int maxArea(int[] height) { + int start = 0; + int end = height.length - 1; + int area = 0; + + while (start < end) { + int y = Math.min(height[start], height[end]); // y축은 더 작은 값으로 설정 + int x = Math.abs(start - end); // end - start도 가능 + int calculatedArea = x * y; + area = Math.max(area, calculatedArea); + + // [중요] 포인터 이동 로직 + if (height[start] <= height[end]) + start++; + else + end--; + } + return area; + } +} diff --git a/design-add-and-search-words-data-structure/jinvicky.java b/design-add-and-search-words-data-structure/jinvicky.java new file mode 100644 index 000000000..c6d73aa77 --- /dev/null +++ b/design-add-and-search-words-data-structure/jinvicky.java @@ -0,0 +1,44 @@ +class WordDictionary { + + private static class Node { + Node[] next = new Node[26]; + boolean isEnd; + } + + private final Node root = new Node(); + + public WordDictionary() {} + + public void addWord(String word) { + Node cur = root; + for (int k = 0; k < word.length(); k++) { + char ch = word.charAt(k); + int i = ch - 'a'; + if (cur.next[i] == null) cur.next[i] = new Node(); + cur = cur.next[i]; + } + cur.isEnd = true; + } + + public boolean search(String word) { + return dfs(word, 0, root); + } + + private boolean dfs(String word, int idx, Node node) { + if (node == null) return false; + if (idx == word.length()) return node.isEnd; + + char ch = word.charAt(idx); + if (ch == '.') { + // 모든 가능 문자로 한 글자 매칭 + for (int c = 0; c < 26; c++) { + if (node.next[c] != null && dfs(word, idx + 1, node.next[c])) { + return true; + } + } + return false; + } else { + return dfs(word, idx + 1, node.next[ch - 'a']); + } + } +} diff --git a/house-robber-ii/jinvicky.java b/house-robber-ii/jinvicky.java new file mode 100644 index 000000000..89235f421 --- /dev/null +++ b/house-robber-ii/jinvicky.java @@ -0,0 +1,39 @@ +class Solution { + /** + * 기존 house-robber 1 문제에서 로직을 가져오되, 접근법을 달리하는 문제 + * 처음생각했던 접근법은 그냥 dp값에서 첫번째 또는 마지막 요소를 빼서 나온 최댓값 아닌가? 했지만 범위에 따라 dp가 달라지므로 오답 + */ + public int rob(int[] nums) { + if (nums.length == 1) return nums[0]; + else if (nums.length == 2) + return Math.max(nums[0], nums[1]); + else if (nums.length == 3) { + return Math.max(Math.max(nums[0], nums[1]), nums[2]); + } + + int n = nums.length; + // 첫째 요소만 포함한 dp (마지막 요소 포함 X) + int[] firstDp = new int[n - 1]; + firstDp[0] = nums[0]; + for (int i = 1; i < n - 1; i++) { + int prev2AndNowRob = (i - 2 < 0 ? 0 : firstDp[i - 2]) + nums[i]; + int prev1Rob = firstDp[i - 1]; + + firstDp[i] = Math.max(prev2AndNowRob, prev1Rob); + } + // System.out.println(firstDp[n-2]); // ok + + // 마지막 요소만 포함한 dp (첫번째 요소 포함 X) + int[] lastDp = new int[n]; + lastDp[1] = nums[1]; + for (int i = 2; i < n; i++) { + int prev2AndNowRob = (i - 2 < 1 ? 0 : lastDp[i - 2]) + nums[i]; + int prev1Rob = lastDp[i - 1]; + + lastDp[i] = Math.max(prev2AndNowRob, prev1Rob); + } + // System.out.println(lastDp[n-1]); // ok + + return Math.max(firstDp[n - 2], lastDp[n - 1]); + } +} diff --git a/longest-consecutive-sequence/jinvicky.java b/longest-consecutive-sequence/jinvicky.java index 01e9cf62a..fc191129b 100644 --- a/longest-consecutive-sequence/jinvicky.java +++ b/longest-consecutive-sequence/jinvicky.java @@ -1,10 +1,28 @@ import java.util.HashSet; import java.util.Set; -// 연속적인 숫자의 길이를 구하는 것이기 때문에 이전, 다음 수가 집합의 일부인지를 파악해야 한다. -// map, set 자료구조를 사용하면 조회 성능을 O(1)로 높일 수 있다. -// 어려웠던 점은 연속적인 숫자의 start가 되냐 여부 조건을 떠올리는 것이었다. while문이 약해서 length++하는 로직이 힘들었다. -// 문제의 조건은 배열 내에서의 연속적인 숫자의 길이이기 때문에 while을 사용해도 성능 이슈 걱정할 필요가 없었다. +/** + * 왜 set을 썼을까? 내가 원하는 "특정 조건"을 제시했을 때 그 숫자를 O(1)으로 조회할 수 있기 때문이다. + * set이 줄 것을 알기에 나는 조건을 설계하는 데만 집중한다. + * 1. 내가 포함된 연속된 시퀀스가 있는가? -> set.contains(n-1) + * 2. 내가 새로운 시퀀스의 start인가? -> !set.contains(n-1) + *

+ * 여기서 가장 긴 길이를 구한다 == Math.max(기존 최대길이, 현재 계산한 최대길이) -> 자동으로 Math.max()가 떠오른다. + * 현재 최대길이는 본인을 포함한 1부터 시작한다. + * [길이를 계산할 때 항상 해야 할까???] -> 아니다! + * 왜? 이미 내가 포함된 연속된 시퀀스는 maxLength를 비교하는 과정을 거쳤는데 굳이 또? + *

+ * 배움: 일단 계산을 떠올린다. -> 그리고 그 계산을 언제(if) 수행할 것인지 조건을 설정한다. (항상 해도 되는가? 중복되지는 않는가?) + *

+ * [성능에 대한 잘못된 생각] + * O(n)이라면 꼭 for문 1번으로 해결해야 한다는 잘못된 생각을 갖고 있었다. + * 첫 번째 루프가 O(n), 두 번째 루프가 O(n)이고, 두 개를 합치면 O(n + n)입니다. + * 시간 복잡도 계산에서 상수 계수는 무시되므로 결국 O(n)으로 표기한다. + *

+ * [후기] + * 처음에는 어 기존이랑... 지금이랑 별도 배열로 체크? 그런데 배열의 개수가 어디까지 늘어나지...? + * 항상 기억할 점은 최대 길이, 최소 길이와 같은 문제는 결과가 중요하지 어느 숫자로 이루어져있는지 알 필요가 없다. + */ class Solution { public int longestConsecutive(int[] nums) { Set set = new HashSet<>(); @@ -26,7 +44,6 @@ public int longestConsecutive(int[] nums) { maxLength = Math.max(length, maxLength); } } - return maxLength; } } diff --git a/longest-increasing-subsequence/jinvicky.java b/longest-increasing-subsequence/jinvicky.java new file mode 100644 index 000000000..9949450b3 --- /dev/null +++ b/longest-increasing-subsequence/jinvicky.java @@ -0,0 +1,63 @@ +import java.util.ArrayList; +import java.util.List; + +/** + * 가장 먼저 들었던 의문은 왜 dp가 2차원 배열이 아닐까? 였지만, + * dp를 2차원 자료구조로 선언하는 경우는 경로 저장이나 상태 전이가 2개 이상의 조건에 따라 나눠질 때인데 + * 이 문제는 1차원으로 해결이 가능한 문제입니다. 가장 중요한 건 “마지막 원소 위치 i”라는 하나의 상태입니다. + * + * LIS = "주어진 수열에서 순서를 지키면서 고를 수 있는 원소 중, 값이 점점 커지도록 선택했을 때 만들 수 있는 가장 긴 부분 수열." + * brute force로 이중 for문을 사용하는 것은 비효율적입니다. 그래서 for문 + BS를 곁들였습니다. + * + * Arrays.binarySearch()는 정렬된 배열에서만 동작하므로 이 문제의 테스트 케이스에 맞지 않습니다. + * + * 신규 메서드: Collections.binarySearch() + * List 안의 요소들에 대해서 이분 탐색을 해서 해당 index를 반환합니다. (없을 경우 -1) + * 단, list 안의 요소들은 정렬되어 있어야 합니다 (이분 탐색의 기본 조건처럼) + * 신규 메서드 혹은 직접적으로 binarySearch() 메서드를 구현해도 됩니다. 다만 원본 nums를 이분 탐색하는 것이 아니며 + * 직관적으로 개념을 이해하기 위해서 간편한 메서드 방식인 Collections.binarySearch()를 사용했습니다. + * + * + * [테스트 케이스 과정 출력] + * 0 + * [10] + * 0 + * [9] + * 0 + * [2] + * 1 + * [2, 5] + * 1 + * [2, 3] + * 2 + * [2, 3, 7] + * 3 + * [2, 3, 7, 101] + * 3 + * [2, 3, 7, 18] + */ +import java.util.*; + +class Solution { + public int lengthOfLIS(int[] nums) { + List tails = new ArrayList<>(); // 길이 k인 증가 수열의 "꼬리 최솟값"들 + + for (int x : nums) { + int idx = Collections.binarySearch(tails, x); + // 해당 x 숫자를 찾지 못하면 -1을 반환하면 이는 list에서 범위에 벗어나기 때문에 + // 음수면 삽입 위치로 변환해서 범위 예외를 처리해야 합니다. + if (idx < 0) idx = -(idx + 1); + + // 중요 연산은 더하기와 교체입니다. + // 가장 크다는 것: 주어진 인덱스와 꼬리 리스트의 길이가 같다. -> 새 subsequence가 생깁니다. + // 그렇지 않다는 것은 최댓값보다 더 작은 값이 있다는 것, 그 자리를 주어진 x로 교체한다. + // 결론: 가장 크면 add, 그 외에는 인덱스 자리를 x로 교체 + if (idx == tails.size()) { + tails.add(x); // 가장 크면 뒤에 추가 → 길이 +1 + } else { + tails.set(idx, x); // 아니면 그 자리의 꼬리를 더 작은 x로 교체 + } + } + return tails.size(); // 꼬리 리스트 길이 = LIS 길이 + } +} diff --git a/maximum-product-subarray/jinvicky.java b/maximum-product-subarray/jinvicky.java new file mode 100644 index 000000000..523554013 --- /dev/null +++ b/maximum-product-subarray/jinvicky.java @@ -0,0 +1,33 @@ +class Solution { + public int maxProduct(int[] nums) { + if (nums.length == 1) + return nums[0]; + if (nums.length == 2) { + return Math.max(nums[0], Math.max(nums[0] * nums[1], nums[1])); + } + + int len = nums.length; + int[] max = new int[len]; + int[] min = new int[len]; + int overall = 0; + + max[0] = min[0] = overall = nums[0]; + + for (int i = 1; i < len; i++) { + // 후보 3을 준비 + int justNum = nums[i]; + // 계속 더한 값 + int keep = justNum * max[i-1]; + // 이전 최소에 음수 곱해서 리버스 + int reverse = justNum * min[i-1]; + + // max와 min 배열을 업데이트 + max[i] = Math.max(justNum, Math.max(keep, reverse)); + min[i] = Math.min(justNum, Math.min(keep, reverse)); + + // overall을 업데이트, 누적 비교로 최대 전역 유지 + overall = Math.max(overall, max[i]); + } + return overall; + } +} diff --git a/missing-number/jinvicky.java b/missing-number/jinvicky.java new file mode 100644 index 000000000..6665b2516 --- /dev/null +++ b/missing-number/jinvicky.java @@ -0,0 +1,24 @@ +class Solution { + /** + * 처음에는 set 자료구조를 동원해서 꼭 빠진 숫자를 찾겠다고 다짐했으나, + * 생각해보니 단순히 범위가 0부터 nums.length까지의 연속된 시퀀스라면 + * 그냥 0부터 n까지 더했을 때의 원래 예상값에서 현재 nums의 합계를 빼면 되는 것이다. + * + * 최댓값, 최솟값을 구할때와 비슷하게 굳이 내용 안을 다 찾으려고 형식 자료구조에 얽매이지 않아도 된다. + */ + public int missingNumber(int[] nums) { + int expected = 0; // 0부터 n까지 더한 숫자의 합계 + int input = 0; //nums가 준 숫자들의 합계 + + for (int n : nums) { + input += n; + } + + for (int i = 0; i <= nums.length; i++) { + expected += i; + } +// System.out.println(expected + " and " + input); + + return expected - input; + } +} diff --git a/spiral-matrix/jinvicky.java b/spiral-matrix/jinvicky.java new file mode 100644 index 000000000..ec06bf5ec --- /dev/null +++ b/spiral-matrix/jinvicky.java @@ -0,0 +1,38 @@ +import java.util.*; + +/** + * dir[][]의 방향을 나선으로 맞추는 것이 가장 중요. 단순 dfs, bfs의 4방향이 아님. + */ +class Solution { + public List spiralOrder(int[][] matrix) { + List ans = new ArrayList<>(); + int m = matrix.length; + if (m == 0) return ans; + int n = matrix[0].length; + + boolean[][] visited = new boolean[m][n]; + + // 한 배열에 (row, col) 방향쌍을 보관: → ↓ ← ↑ + int[][] dir = {{0,1}, {1,0}, {0,-1}, {-1,0}}; + int d = 0; // 현재 방향 인덱스 + int i = 0, j = 0; // 현재 위치 + + for (int k = 0; k < m * n; k++) { + ans.add(matrix[i][j]); + visited[i][j] = true; + + int ni = i + dir[d][0]; + int nj = j + dir[d][1]; + + // 경계 밖이거나 이미 방문했다면 방향 전환 + if (ni < 0 || ni >= m || nj < 0 || nj >= n || visited[ni][nj]) { + d = (d + 1) % 4; // 0→1→2→3→0 + ni = i + dir[d][0]; + nj = j + dir[d][1]; + } + + i = ni; j = nj; + } + return ans; + } +} diff --git a/top-k-frequent-elements/jinvicky.java b/top-k-frequent-elements/jinvicky.java index 005411266..3ae172bd1 100644 --- a/top-k-frequent-elements/jinvicky.java +++ b/top-k-frequent-elements/jinvicky.java @@ -1,22 +1,21 @@ -import java.util.HashMap; -import java.util.Map; -import java.util.PriorityQueue; +import java.util.*; class Solution { public int[] topKFrequent(int[] nums, int k) { - // [풀이] - // 1. <숫자: 빈도수>를 저장하는 HashMap과 [빈도수, 숫자]를 저장하는 PriorityQueue를 선언한다. - // 2. HashMap에 숫자별로 빈도수를 함께 저장해서 해시테이블을 만든다. - // [우선순위 큐에 사용된 자료구조] - // 1. 별도 클래스를 선언 - // 2. 요구사항 자료형 배열을 선언한다. - // 처음에는 별도 클래스를 선언했다가 값이 2개이며 알고리즘 로직 자체가 어려워서 int[] 구조로 풀이했다. - // (주로 알고리즘이 어려우면 가독성이 나쁘더라도 자료구조를 단순화하는 습관이 있다) - // [어려웠던 점] - // 1. 우선순위 큐는 매번 요소가 추가될 때마다 내부 정렬을 수행하기 때문에 연산을 수행하면서 k개를 유지해야 한다. - // 또한 기존 [빈도수, 숫자]를 버려야만 올바른 답을 도출할 수 있었다. - // 2. [숫자, 빈도수]로 저장하는 것만 생각했더니 내부 정렬을 어떻게 하지 못해서 굉장히 고민했다. 정답은 반대였다. - + /** + * for문이 총 3번 필요하다. (1.빈도_초기화, 2.큐에 저장, 3.결과배열에 할당 + * 빈도_초기화 -> k:v로 쉽게 저장하는 자료구조 + * 큐에 저장 -> 최소 힙으로 정렬되는 우선순위 큐 선언 -> 큐 같은 자료구조에서 k:v를 하고 싶다면 new int[2]가 가장 쉬움 + * 결과배열에 할당 -> k개만큼 answer[]에 저장 + * + * 문제는 상위 k개를 유지해야 하고 그를 위해서는 k개를 넘었을 때 빈도수가 낮은 순으로 flush하는 로직이 필요하다는 것. + * 큐는 선입선출 -> 빈도수가 낮을 수록 위로 정렬되게 해야 flush했을 때 빈도수가 낮은 순서대로 사라진다. + * + * 결과배열에 할당할 때는 그냥 앞에서부터 하면 된다. + * 이미 k개를 만족했고, 빈도수가 낮 -> 높 순서대로 그대로 쌓으면 된다. + * + * 첫번째부터 꺼내서 결과배열에 할당할 거면 [빈도수, 숫자]로 큐에 저장하는 게 맞다 + */ int[] answer = new int[k]; Map map = new HashMap<>(); diff --git a/unique-paths/jinvicky.java b/unique-paths/jinvicky.java new file mode 100644 index 000000000..a7430fd9e --- /dev/null +++ b/unique-paths/jinvicky.java @@ -0,0 +1,24 @@ +class Solution { + public int uniquePaths(int m, int n) { + /** + * 여기서 이동가능한 경우는 right, down 두가지 경우이다. + * 모든 블록은 내 왼쪽 블록에서 나로 온 경우, 내 위 블록에서 나로 온 경우를 고려해서 [i-1][j] + [i][j-1]로 표현할 수 있다. + * 단 가로 첫번째 줄과 세로 첫번째 줄은 1로 초기화 해줘야 한다. (왜냐하면 각각 down, right이 없기 때문에 그 블록들은 1가지 경우로밖에 도달할 수 없기 때문이다.) + */ + int[][] dp = new int[m][n]; + for (int i = 0; i < m; i++) { + dp[i][0] = 1; + } + + for (int j = 0; j < n; j++) { + dp[0][j] = 1; + } + + for (int i = 1; i < m; i++) { + for (int j = 1; j < n; j++) { + dp[i][j] = dp[i-1][j] + dp[i][j-1]; + } + } + return dp[m - 1][n - 1]; + } +} diff --git a/valid-parentheses/jinvicky.java b/valid-parentheses/jinvicky.java new file mode 100644 index 000000000..8301b5354 --- /dev/null +++ b/valid-parentheses/jinvicky.java @@ -0,0 +1,36 @@ +import java.util.Stack; + +class Solution { + /** + * 주어진 문자열 s의 괄호가 제대로 닫혀있는지 여부를 판단하는 문제입니다. + * 나올 수 있는 괄호의 종류는 3개이며 짝맞추기에 유리한 자료구조인 스택을 사용합니다. + * {, [, ( 같은 왼쪽 괄호는 무조건 stack에 추가됩니다. + * }, ], ) 같은 오른쪽 괄호를 만나면 매칭되는 왼쪽 괄호를 만날 때까지 pop()을 통해 stack에서 괄호들을 꺼내야 합니다. + */ + public boolean isValid(String s) { + Stack stack = new Stack<>(); + + for (char c : s.toCharArray()) { + if (c == '(' || c == '[' || c == '{') { + stack.push(c); + } else { + /** + * [중요] 문자열이 오른쪽 괄호로만 구성되는 경우도 있으므로 for문이 반복되는데 스택이 비었다면 + * 그 즉시 false를 반환하고 break해야만 stack Exception을 막을 수 있습니다. + * 문제 케이스: "]" + */ + if (stack.isEmpty()) { + return false; + } + if (stack.peek() == '(' && c == ')') { + stack.pop(); + } else if (stack.peek() == '[' && c == ']') { + stack.pop(); + } else if (stack.peek() == '{' && c == '}') { + stack.pop(); + } + } + } + return stack.isEmpty(); // 스택이 비었다면 괄호는 짝이 맞는다는 의미입니다. + } +}