diff --git a/3sum/haxr369.java b/3sum/haxr369.java new file mode 100644 index 000000000..59c582212 --- /dev/null +++ b/3sum/haxr369.java @@ -0,0 +1,64 @@ +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +/** + * 세 수의 인덱스가 모두 다르고 세 수 합이 0인 경우의 수를 구하기 + * 세 숫자가 들어간 배열은 중복되게 하지 않은다. + * + * 1. 모든 경우의 수를 구한다. => O(N^3) + * 3000*3000*3000 == 9*10^9 => 9억건... + * 2. left, mid, right라고 할 때, mid를 고정하고 left, right만 움직이는 two pointer를 응용하기 + */ +class Solution { + /** + * Runtime: 545 ms (Beats 14.44%) + * Memory: 60.78 MB (Beats 5.38%) + * Space Complexity: O(N) + * - 정렬을 위한 공간 => O(N) + * - 세 수를 저장하기 위한 리스트 => O(3) + * > O(N) + O(3) => O(N) + * Time Complexity: O(NlogN) + * - 정렬을 위한 시간 => O(NlogN) + * - mid의 순회 => O(N) + * - two pointer 조회 => O(N) + * > O(NlogN) + O(N)*O(N) ~= O(N^2) + */ + public List> threeSum(int[] nums) { + int L = nums.length; + Arrays.sort(nums); + + Set> st = new HashSet<>(); + + // mid를 늘려가면서 투 포인터 진행 + for (int mid = 1; mid < L - 1; mid++) { + int left = 0; + int right = L - 1; + while (left < mid && mid < right) { + int sm = nums[left] + nums[mid] + nums[right]; + if (sm == 0) { + /** + * left를 더하는 이유: + * 현재 찾은 경우의 수 외에 다른 경우가 존재할 수 있음 + * ex) -7,1,6 / -6,1,5 + * => mid는 고정이지만, left는 늘리고 right를 줄여서 0을 만들 수 있는 다른 경우가 있음 + */ + List li = new ArrayList<>(); + li.add(nums[left]); + li.add(nums[mid]); + li.add(nums[right]); + st.add(li); // 중복값이 있어도 무시할 수 있음. + left++; + } else if (sm < 0) { // 부족하면 left 늘리기 + left++; + } else { // 과하면 right 줄이기 + right--; + } + } + } + + return new ArrayList<>(st); + } +} diff --git a/climbing-stairs/haxr369.java b/climbing-stairs/haxr369.java new file mode 100644 index 000000000..d4c09a4d1 --- /dev/null +++ b/climbing-stairs/haxr369.java @@ -0,0 +1,38 @@ +class Solution { + /** + * 계산 n개를 오르기 위해 1 또는 2개씩 오르는 모든 경우의 수를 구하기 + * + * DP문제 풀이 과정 + * 1. 문제 단순화 + * - 숫자 N을 만들기 위해 1 또는 2를 더하는 순서를 만드는 경우의 수 + * 2. 문제에서 원하는 것 + * - N을 만들기 위한 모든 경우의 수 + * => DP[N] = N을 만들기 위한 모든 경우의 수 + * 2. 규칙 찾기 + * - N은 N-1에서 1 더하거나, N-2에서 2를 더하면 도달할 수 있다. + * - 따라서 N에 도달하기 위한 경우의 수는 N-1을 도달하기 위한 경우의 수 + N-2에 도달하기 위한 경우의 수 + * - DP[0] = 1 => 움직이지 않는다는 경우의 수 + * - DP[1] = 1 => 1칸 움직이는 경우의 수 + * - DP[N] = DP[N-2] + DP[N-1] + */ + public int climbStairs(int n) { + /** + * Runtime: 0 ms (Beats 100.00%) + * Memory: 42.00 MB (Beats 15.00%) + * Space Complexity: O(N) + * - N 크기의 배열 1개 사용으로 O(N) + * > O(N) + * Time Complexity: O(N) + * - N회 덧셈으로 => O(N) + * > O(N) + */ + int[] dp = new int[n + 1]; + dp[0] = 1; + dp[1] = 1; + + for (int i = 2; i < n + 1; i++) { + dp[i] = dp[i - 1] + dp[i - 2]; + } + return dp[n]; + } +} diff --git a/product-of-array-except-self/haxr369.java b/product-of-array-except-self/haxr369.java new file mode 100644 index 000000000..5c777506e --- /dev/null +++ b/product-of-array-except-self/haxr369.java @@ -0,0 +1,197 @@ +import java.util.HashMap; +import java.util.Map; + +/** + * + * 마지막 풀이가 최종풀이고 점차 복잡도가 개선됩니다. + * + * 1번째 풀이: 새그먼트 트리를 응용한 범위 곱 게산 구현 + * 2번째 풀이: 기본적인 prefix, suffix product 구현 + * 3번째 풀이: 최종 풀이로 prefix, suffix product의 공간복잡도를 O(1)로 최적화 + * + */ +class Solution { + + /** + * 풀이요약: 범위를 반씩 나누며 곱을 캐싱하고, 제외할 인덱스만 골라 탐색하는 분할 정복 기반 배타 곱 계산 + * + * 풀이결과: + * Runtime: 872 ms (Beats 3.62%) + * Memory: 137.29 MB (Beats 5.61%) + * Space Complexity: O(NlogN) + * - 길이 N인 배열을 절반씩 자른 범위 안에 수들을 곱한 값을 저장 O(NlogN) + * - 길이 N인 출력 배열 생성 O(N) + * > O(N) + O(NlogN) > O(NlogN) + * + * Time Complexity: O(NlogN) + * - 길이 N인 배열을 절반씩 잘라가면서 안에 수들을 곱하기 O(NlogN) + * - 0~N 범위를 절반씩 잘라가면서 i가 포함되지 않은 범위 수를 곱하기 O(logN) + * - N개의 숫자에 대해서 찾기 => O(NlogN) + * > O(NlogN) + O(NlogN) > O(NlogN) + */ + public int[] productExceptSelf1(int[] nums) { + Map mp = new HashMap<>(); + fndRngMul(mp, nums, 0, nums.length); + int[] ans = new int[nums.length]; + for (int i = 0; i < nums.length; i++) { + int val = binaryFnd(mp, i, 0, nums.length); + ans[i] = val; + } + + return ans; + } + + /*** + * 특정 idx를 제외한 구간 곱을 재귀적으로 찾는다. + * + * - 범위가 단일 원소면 곱에 포함될 값이 없으므로 1 반환 + * - 범위를 반으로 자르고, idx가 속하지 않은 부분의 곱을 즉시 사용 + * - idx가 속한 쪽은 계속 재귀 탐색 + */ + private int binaryFnd(Map mp, int idx, int str, int end) { + int val = 1; + + if (end - str == 1) { + return val; + } + + int mid = (str + end) / 2; + String leftKey = str + "_" + mid; + String rightKey = mid + "_" + end; + // System.out.println("----idx->"+idx+" mid->"+mid); + // idx는 좌측에 위치하기 때문에 우측 값을 곱함. + if (idx < mid) { + // 하지만 다은 idx가 포함된 범위를 탐색 + int leftVal = binaryFnd(mp, idx, str, mid); + // System.out.println(". ----leftVal->"+leftVal+" base->"+mp.get(rightKey)+" + // val-> "+val); + val = mp.get(rightKey) * leftVal; + } else { // idx는 우측에 위치하기 때문에 좌측 값을 곱함. + int rightVal = binaryFnd(mp, idx, mid, end); + // System.out.println(". ----rightVal->"+rightVal+" base->"+mp.get(leftKey)+" + // val-> "+val); + val = mp.get(leftKey) * rightVal; + } + return val; + } + + /** + * 배열의 범위 [str, end) 내 값들의 곱을 구해 캐싱한다. + * + * - 원소가 하나만 있는 구간이면 nums[str] 저장 + * - 구간을 반으로 나누어 왼쪽/오른쪽 곱을 재귀적으로 계산 + * - 전체 구간이 아니라면 left * right 를 캐싱 + * - 전체 구간(0~N)은 제외 곱 계산 시 무시해야 하므로 '1' 저장 + */ + private int fndRngMul(Map mp, int[] nums, int str, int end) { + String k = str + "_" + end; + + if (end - str == 1) { + // System.out.println("put k->"+k+" v->"+v); + mp.put(k, nums[str]); + return nums[str]; + } + int mid = (str + end) / 2; + int leftRngMul = fndRngMul(mp, nums, str, mid); + int rightRngMul = fndRngMul(mp, nums, mid, end); + + int v = 1; + // 전체 곱은 구하지 않음. + if (str == 0 && end == nums.length) { + v = 1; + } else { + v = leftRngMul * rightRngMul; + } + mp.put(k, v); + // System.out.println("put2 k->"+k+" v->"+v); + return v; + } + + /** + * 풀이요약: 좌우로 누적곱하는 배역을 만들고, i를 제외한 좌우 범위 누적곱을 곱한다. + * prefix-product와 suffix-product를 구하기 + * + * 풀이결과: + * Runtime: 3 ms (Beats 21.34%) + * Memory: 64.97 MB (Beats 19.65%) + * Space Complexity: O(N) + * - 길이가 N인 배열을 3개를 만들기 + * > O(N) + O(N) + O(N) > O(N) + * Time Complexity: O(N) + * - 길이 N인 배열 2번 순회하기 > O(N) + * - 0~N을 순회하면서 누적곱 곱하기 > O(N) + * > O(N) + O(N) > O(N) + */ + public int[] productExceptSelf2(int[] nums) { + int L = nums.length; + int[] leftAccMul = new int[L]; + int[] rightAccMul = new int[L]; + int[] ans = new int[L]; + + // 0부터 누적곱하기 + leftAccMul[0] = nums[0]; + for (int i = 1; i < L; i++) { + leftAccMul[i] = leftAccMul[i - 1] * nums[i]; + } + + // L-1부터 누적곱하기 + rightAccMul[L - 1] = nums[L - 1]; + for (int i = L - 2; i >= 0; i--) { + rightAccMul[i] = rightAccMul[i + 1] * nums[i]; + } + + // i를 제외한 누적곱을 곱하기 + for (int i = 0; i < L; i++) { + int val = 1; + if (i == 0) { // 오른쪽 누적곱만 곱하기 + val *= rightAccMul[i + 1]; // i+1 ~ L-1까지 곱한 값 + } else if (i == L - 1) { // 왼쪽 누적곱만 곱하기 + val *= leftAccMul[i - 1]; // 0~L-2까지 곱한 값 + } else { + val *= leftAccMul[i - 1] * rightAccMul[i + 1]; + } + ans[i] = val; + } + return ans; + } + + /** + * 풀이요약: 좌우로 누적곱하는 배역을 만들고, i를 제외한 좌우 범위 누적곱을 곱한다. + * prefix-product 배열을 출력 배열로 사용하기 + * suffix-product 배열 대신 오른쪽 누적곱을 한 변수로만 관리한다. + * + * 풀이결과: + * Runtime: 2 ms (Beats 89.36%) + * Memory: 72.28 MB (Beats 5.61%) + * Space Complexity: O(1) + * - 길이가 N인 배열을 1개를 만들지만, return에 쓰이므로 카운팅 안됨. + * - suffix-product 게산용 변수 1개 + * > O(1) > O(1) + * Time Complexity: O(N) + * - 길이 N인 배열 2번 순회하기 > O(N) + * > O(N) > O(N) + * + */ + public int[] productExceptSelf3(int[] nums) { + int L = nums.length; + int[] leftAccMul = new int[L]; + + // 0부터 누적곱하기 + leftAccMul[0] = 1; + for (int i = 1; i < L; i++) { + leftAccMul[i] = leftAccMul[i - 1] * nums[i - 1]; + // System.out.println("i->"+i+" val->"+leftAccMul[i]); + } + // System.out.println("--------------"); + // L-1부터 누적곱하기 + int rightAccMul = nums[L - 1]; + for (int i = L - 2; i >= 0; i--) { + // L-1번째 숫자는 suffix-product에서 곱할게 없다.. + // 0번째 숫자는 1~L-1 범위 누적곱만 곱해야한다. + leftAccMul[i] *= rightAccMul; + rightAccMul *= nums[i]; + // System.out.println("i->"+i+" val->"+leftAccMul[i]); + } + return leftAccMul; + } +} diff --git a/valid-anagram/haxr369.java b/valid-anagram/haxr369.java new file mode 100644 index 000000000..14bfc913d --- /dev/null +++ b/valid-anagram/haxr369.java @@ -0,0 +1,40 @@ +class Solution { + /** + * s의 문자 개수와 r의 문자 개수가 동일한지 체크 + * 배열1,2에 각각 문자 개수를 저장하기 + * a-z까지 26개 + */ + public boolean isAnagram(String s, String t) { + /** + * Runtime: 2 ms (Beats 98.15%) + * Memory: 44.74 MB (Beats 38.46%) + * Space Complexity: O(1) + * - 26 크기의 배열 2개 사용으로 2*O(26) + * > O(1) + * Time Complexity: O(N) + * - 문자열 s와 t의 문자를 하나하나 탐색 => 2*O(N) + * - 두 문자 사용건수 저장 배열 탐색 => O(26) + * > O(N) + */ + int[] scount = new int[26]; + int[] tcount = new int[26]; + + char A = 'a'; + for (char c : s.toCharArray()) { + scount[c - A] += 1; + } + + for (char c : t.toCharArray()) { + tcount[c - A] += 1; + } + + for (int i = 0; i < 26; i++) { + if (scount[i] != tcount[i]) { + return false; + } + } + + return true; + + } +} diff --git a/validate-binary-search-tree/haxr369.java b/validate-binary-search-tree/haxr369.java new file mode 100644 index 000000000..7dd405a4d --- /dev/null +++ b/validate-binary-search-tree/haxr369.java @@ -0,0 +1,86 @@ +/** + * Definition for a binary tree node. + * public class TreeNode { + * int val; + * TreeNode left; + * TreeNode right; + * TreeNode() {} + * TreeNode(int val) { this.val = val; } + * TreeNode(int val, TreeNode left, TreeNode right) { + * this.val = val; + * this.left = left; + * this.right = right; + * } + * } + */ +/*** + * dfs로 내려가면서 node와 자녀 노드의 값을 비교하기 + * 다만, 자녀노드는 주어진 범위 내에 있어야한다. + * sml < childNpde < lrg + * + */ +class Solution { + public boolean isValidBST(TreeNode root) { + return dfs(root, null, null); + } + + /** + * 1. node의 자녀 노드는 sml <= val <= lrg를 항상 만족해야한다. + * 2. sml, lrg가 null이면 상관 없음 + * 3. node의 val과도 맞게 유지해야한다. + * + * Runtime: 0 ms (Beats 100.00%) + * Memory: 45.1 MB (Beats 13.09%) + * Space Complexity: O(N) + * - 총 노드의 개수를 N이라고 할 때, dfs로 모든 노드 수만큼 스택을 쌓기 때문에 + * > O(N) + * Time Complexity: O(N) + * - dfs로 모든 노드를 1회 탐색 O(N) + * > O(N) + */ + private boolean dfs(TreeNode node, Integer sml, Integer lrg) { + TreeNode lN = node.left; + TreeNode rN = node.right; + boolean rslt = true; + // System.out.println("node.val->"+node.val+" sml->"+sml+" lrg->"+lrg+"====IN"); + if (lN != null) { + // 1. 범위 체크 + if ((sml != null && sml >= lN.val) + || (lrg != null && lrg <= lN.val)) { + return false; + } + + // 2. node와 left 자녀 값 비교 + if (node.val <= lN.val) { + return false; + } + // System.out.println(". lN->"+lN.val); + // 3. 범위 지정. 앞으로 모든 노드는 node.val 보다 작아야한다. + rslt = dfs(lN, sml, node.val); + if (!rslt) { + return rslt; + } + } + + if (rN != null) { + // 1. 범위 체크 + if ((sml != null && sml >= rN.val) + || (lrg != null && lrg <= rN.val)) { + return false; + } + // 2. node와 left 자녀 값 비교 + if (node.val >= rN.val) { + return false; + } + // System.out.println(". rN->"+rN.val); + // 3. 범위 지정. 앞으로 모든 노드는 node.val 보다 커야한다. + rslt = dfs(rN, node.val, lrg); + if (!rslt) { + return rslt; + } + } + // System.out.println("node.val->"+node.val+" sml->"+sml+" + // lrg->"+lrg+"====OUT"); + return rslt; + } +}