diff --git a/combination-sum/ICE0208.java b/combination-sum/ICE0208.java new file mode 100644 index 0000000000..b552de72ba --- /dev/null +++ b/combination-sum/ICE0208.java @@ -0,0 +1,64 @@ +import java.util.ArrayList; +import java.util.List; + +class Solution { + /* + * [백트래킹 풀이] + * N: candidates의 길이 + * M: candidates에서 가장 작은 값 + * + * 시간 복잡도: O(N^(target / M)) + * - 재귀 한 단계에서 최대 N개의 후보를 선택할 수 있다. + * - 가장 작은 숫자를 반복해서 선택하면 재귀 깊이는 최대 target / M이다. + * - 실제로는 startIndex와 가지치기로 인해 이보다 적은 경우만 탐색한다. + * + * 공간 복잡도: O(target / M) + * - 재귀 호출 스택과 현재 조합 리스트가 최대 재귀 깊이만큼 사용된다. + * - 반환할 결과 리스트는 제외한다. + */ + + private int[] candidates; + private List> combinations; + + public List> combinationSum(int[] candidates, int target) { + this.candidates = candidates; + this.combinations = new ArrayList<>(); + + findCombinations(0, target, new ArrayList<>()); + + return combinations; + } + + private void findCombinations( + int startIndex, + int remainingTarget, + List currentCombination + ) { + // 목표 합을 정확히 완성한 경우 + if (remainingTarget == 0) { + combinations.add(new ArrayList<>(currentCombination)); + return; + } + + for (int i = startIndex; i < candidates.length; i++) { + int candidate = candidates[i]; + + // 현재 숫자를 선택하면 목표 합을 초과하는 경우 + if (candidate > remainingTarget) { + continue; + } + + currentCombination.add(candidate); + + // 같은 숫자를 여러 번 사용할 수 있으므로 i부터 다시 탐색한다. + findCombinations( + i, + remainingTarget - candidate, + currentCombination + ); + + // 다음 조합을 탐색하기 위해 현재 선택을 되돌린다. + currentCombination.remove(currentCombination.size() - 1); + } + } +} diff --git a/decode-ways/ICE0208.java b/decode-ways/ICE0208.java new file mode 100644 index 0000000000..63ca23aea0 --- /dev/null +++ b/decode-ways/ICE0208.java @@ -0,0 +1,51 @@ +class Solution { + private String s; + private Integer[] memo; + + /** + * 현재 위치에서 숫자 한 자리 또는 두 자리를 해석하며 가능한 경우의 수를 구한다. + * 각 index의 결과를 memo에 저장하여 동일한 위치를 중복 계산하지 않는다. + * + * 시간 복잡도: O(n) - 각 index를 한 번씩만 계산한다. + * 공간 복잡도: O(n) - memo 배열과 최대 n 깊이의 재귀 호출 스택 + */ + public int numDecodings(String s) { + this.s = s; + this.memo = new Integer[s.length()]; + + return dfs(0); + } + + private int dfs(int index) { + // 문자열 끝까지 유효하게 해석한 경우 + if (index == s.length()) { + return 1; + } + + // 0으로 시작하는 코드는 해석할 수 없다. + if (s.charAt(index) == '0') { + return 0; + } + + if (memo[index] != null) { + return memo[index]; + } + + // 현재 숫자 한 자리만 해석하는 경우 + int count = dfs(index + 1); + + // 현재 숫자와 다음 숫자를 함께 해석하는 경우 + if (index + 1 < s.length()) { + int twoDigitNumber = + (s.charAt(index) - '0') * 10 + + (s.charAt(index + 1) - '0'); + + if (twoDigitNumber <= 26) { + count += dfs(index + 2); + } + } + + memo[index] = count; + return count; + } +} diff --git a/maximum-subarray/ICE0208.java b/maximum-subarray/ICE0208.java new file mode 100644 index 0000000000..b6a460da68 --- /dev/null +++ b/maximum-subarray/ICE0208.java @@ -0,0 +1,54 @@ +class Solution { + /** + * 배열을 절반으로 나누고 최대 부분 배열이 + * 왼쪽, 오른쪽, 가운데를 걸치는 경우를 각각 구한다. + * + * 시간 복잡도: O(n log n) + * 공간 복잡도: O(log n) + */ + public int maxSubArray(int[] nums) { + return findMaxSum(nums, 0, nums.length - 1); + } + + private int findMaxSum(int[] nums, int left, int right) { + if (left == right) { + return nums[left]; + } + + int mid = left + (right - left) / 2; + + int leftMaxSum = findMaxSum(nums, left, mid); + int rightMaxSum = findMaxSum(nums, mid + 1, right); + int crossingMaxSum = findCrossingMaxSum(nums, left, mid, right); + + return Math.max( + Math.max(leftMaxSum, rightMaxSum), + crossingMaxSum + ); + } + + private int findCrossingMaxSum( + int[] nums, + int left, + int mid, + int right + ) { + int sum = 0; + int leftMaxSum = Integer.MIN_VALUE; + + for (int i = mid; i >= left; i--) { + sum += nums[i]; + leftMaxSum = Math.max(leftMaxSum, sum); + } + + sum = 0; + int rightMaxSum = Integer.MIN_VALUE; + + for (int i = mid + 1; i <= right; i++) { + sum += nums[i]; + rightMaxSum = Math.max(rightMaxSum, sum); + } + + return leftMaxSum + rightMaxSum; + } +} diff --git a/number-of-1-bits/ICE0208.java b/number-of-1-bits/ICE0208.java new file mode 100644 index 0000000000..cfe6d172d7 --- /dev/null +++ b/number-of-1-bits/ICE0208.java @@ -0,0 +1,62 @@ +class Solution2 { + /* + * 시간 복잡도: O(k) + * - k는 n의 이진 표현에서 1인 비트의 개수이다. + * - n & (n - 1)을 할 때마다 가장 오른쪽 1 비트가 하나씩 제거되므로, + * 1의 개수만큼 반복한다. + * - 문제 조건상 n은 최대 31비트 정수이므로 실질적으로는 O(1)로 볼 수 있다. + * + * 공간 복잡도: O(1) + * - 추가 변수만 사용한다. + */ + public int hammingWeight(int n) { + int answer = 0; + + while (n != 0) { + /* + * n & (n - 1)은 n의 가장 오른쪽 1 비트를 제거한다. + * + * 예를 들어 n = 1100일 때, + * n - 1 = 1011이 되고, + * + * 1100 + * & 1011 + * ------ + * 1000 + * + * 처럼 가장 오른쪽 1 비트가 제거된다. + */ + n = n & (n - 1); + answer++; + } + + return answer; + } +} + +class Solution1 { + /* + * 시간 복잡도: O(b) + * - b는 n의 이진 표현 길이이다. + * - 오른쪽 비트부터 하나씩 확인하므로 비트 수만큼 반복한다. + * - 문제 조건상 n은 최대 31비트 정수이므로 실질적으로는 O(1)로 볼 수 있다. + * + * 공간 복잡도: O(1) + * - 추가 변수만 사용한다. + */ + public int hammingWeight(int n) { + int answer = 0; + + while (n != 0) { + // 가장 오른쪽 비트가 1이면 카운트한다. + if ((n & 1) == 1) { + answer++; + } + + // 다음 비트를 확인하기 위해 오른쪽으로 한 칸 이동한다. + n >>>= 1; + } + + return answer; + } +} diff --git a/valid-palindrome/ICE0208.java b/valid-palindrome/ICE0208.java new file mode 100644 index 0000000000..30196bf36c --- /dev/null +++ b/valid-palindrome/ICE0208.java @@ -0,0 +1,43 @@ +class Solution { + + public boolean isPalindrome(String s) { + /* + * 문제에서는 대소문자를 구분하지 않고, + * 알파벳과 숫자만 남긴 뒤 팰린드롬인지 확인한다. + * + * Character.isLetterOrDigit(ch)는 + * 현재 문자가 알파벳 또는 숫자인지 확인하는 함수다. + * 공백, 쉼표, 콜론 같은 문자는 비교 대상이 아니므로 건너뛴다. + * + * 입력은 printable ASCII 문자로 제한되어 있으므로 + * charAt()으로 한 문자씩 확인해도 된다. + * + * 시간 복잡도: O(n) + * 공간 복잡도: O(1) + */ + int left = 0; + int right = s.length() - 1; + + while (left < right) { + while (left < right && !Character.isLetterOrDigit(s.charAt(left))) { + left++; + } + + while (left < right && !Character.isLetterOrDigit(s.charAt(right))) { + right--; + } + + char leftChar = Character.toLowerCase(s.charAt(left)); + char rightChar = Character.toLowerCase(s.charAt(right)); + + if (leftChar != rightChar) { + return false; + } + + left++; + right--; + } + + return true; + } +}