|
| 1 | +--- |
| 2 | +layout: post |
| 3 | +title: (Leetcode) 15 - 3Sum |
| 4 | +categories: [스터디-알고리즘] |
| 5 | +tags: [자바, java, 리트코드, Leetcode, 알고리즘, algorithm, array, 배열] |
| 6 | +date: 2024-05-07 23:59:59 +0900 |
| 7 | +toc: true |
| 8 | +--- |
| 9 | + |
| 10 | +[New Year Gift - Curated List of Top 75 LeetCode Questions to Save Your Time](https://www.teamblind.com/post/New-Year-Gift---Curated-List-of-Top-75-LeetCode-Questions-to-Save-Your-Time-OaM1orEU) |
| 11 | + |
| 12 | +위 링크에 있는 추천 문제들을 시간이 있을때마다 풀어보려고 한다. |
| 13 | + |
| 14 | +--- |
| 15 | + |
| 16 | +[https://leetcode.com/problems/3sum/description/](https://leetcode.com/problems/3sum/description/) |
| 17 | + |
| 18 | +medium 문제이다. 하지만 정답률이 매우 낮은 문제다. |
| 19 | + |
| 20 | +단순히 for 문으로 3가지 조합을 찾을 경우에는 O(n^3) 으로 최대 3000^3 까지 가야하므로 이 방식은 피해야 할 것이라는 가정하에 진행하였다. |
| 21 | + |
| 22 | +그러면 어떤 경우에 문제가 요구하는 조건 (3가지 수를 더했을 때 0이 되는 경우) 을 만족할 수 있을까 생각해보면 다음과 같았다. |
| 23 | + |
| 24 | +- [0, 0, 0] |
| 25 | +- 0 이 포함된 경우 [0, a, -a] |
| 26 | +- a = -(b + c) 인 경우 [a, b, c] |
| 27 | + |
| 28 | +그리고 이를 코드로 구현해보았다. |
| 29 | + |
| 30 | +## 내가 작성한 풀이 |
| 31 | + |
| 32 | +```java |
| 33 | +class Solution { |
| 34 | + public List<List<Integer>> threeSum(int[] nums) { |
| 35 | + List<List<Integer>> result = new ArrayList<>(); |
| 36 | + Map<Integer, Integer> map = new HashMap<>(); |
| 37 | + for (int num : nums) { |
| 38 | + map.put(num, map.getOrDefault(num, 0) + 1); |
| 39 | + } |
| 40 | + |
| 41 | + // case 1 : 0이 3개 있을 경우 |
| 42 | + int count = map.getOrDefault(0, 0); |
| 43 | + if (count > 2) { |
| 44 | + result.add(List.of(0, 0, 0)); |
| 45 | + } |
| 46 | + |
| 47 | + // case 2 : 0이 있을 경우 |
| 48 | + if (count > 0) { |
| 49 | + for (int num : map.keySet()) { |
| 50 | + System.out.println("num : " + num); |
| 51 | + // 중복을 방지하기 위해 0 이나 음수는 체크하지 않음 |
| 52 | + if (num > 0 && map.containsKey(num) && map.containsKey(-num)) { |
| 53 | + result.add(List.of(-num, 0, num)); |
| 54 | + } |
| 55 | + } |
| 56 | + } |
| 57 | + |
| 58 | + // case 3 : a = -(b + c) 인 경우 (a가 1일 경우는 불가능함. case1에 포함됨) |
| 59 | + for (int num : map.keySet()) { |
| 60 | + if (num > 1) { |
| 61 | + for (int i = num - 1; i > num / 2 - (num % 2 == 0 ? 1 : 0); i--) { |
| 62 | + int a = -i; |
| 63 | + int b = -num + i; // -(num - i) |
| 64 | + if (map.containsKey(a) && map.containsKey(b)) { |
| 65 | + if (a < b) { |
| 66 | + result.add(List.of(a, b, num)); |
| 67 | + } else if (a > b) { |
| 68 | + result.add(List.of(b, a, num)); |
| 69 | + } else if (map.get(a) > 1) { |
| 70 | + result.add(List.of(a, a, num)); |
| 71 | + } |
| 72 | + } |
| 73 | + } |
| 74 | + } else if (num < -1) { |
| 75 | + int abs = Math.abs(num); |
| 76 | + for (int i = abs - 1; i > abs / 2 - (num % 2 == 0 ? 1 : 0); i--) { |
| 77 | + int a = i; |
| 78 | + int b = abs - i; |
| 79 | + if (map.containsKey(a) && map.containsKey(b)) { |
| 80 | + if (a < b) { |
| 81 | + result.add(List.of(num, a, b)); |
| 82 | + } else if (a > b) { |
| 83 | + result.add(List.of(num, b, a)); |
| 84 | + } else if (map.get(a) > 1) { |
| 85 | + result.add(List.of(a, a, num)); |
| 86 | + } |
| 87 | + } |
| 88 | + } |
| 89 | + } |
| 90 | + } |
| 91 | + |
| 92 | + return result; |
| 93 | + } |
| 94 | +} |
| 95 | +``` |
| 96 | + |
| 97 | +통과는 하긴 하는데 아쉽게도 이 방법은 정말 아슬아슬하게 통과한다. |
| 98 | + |
| 99 | + |
| 100 | + |
| 101 | +## 개선해보기 |
| 102 | + |
| 103 | +case3 의 속도가 문제다. case3 에 대해서 아래와 같이 개선해보았다. |
| 104 | + |
| 105 | +```java |
| 106 | +// case 3 : a = -(b + c) 인 경우 (a가 1일 경우는 불가능함. case1에 포함됨) |
| 107 | +Set<String> keySet = new HashSet<>(); |
| 108 | +for (int a : map.keySet()) { |
| 109 | + for (int b : map.keySet()) { |
| 110 | + if (a == 0 || b == 0) { |
| 111 | + continue; |
| 112 | + } |
| 113 | + |
| 114 | + if(a == b) { |
| 115 | + if(map.get(a) == 1) { |
| 116 | + continue; |
| 117 | + } |
| 118 | + } |
| 119 | + |
| 120 | + int twoSum = a + b; |
| 121 | + if(twoSum == 0) { |
| 122 | + continue; |
| 123 | + } |
| 124 | + |
| 125 | + if (a == -twoSum || b == -twoSum) { |
| 126 | + if (map.get(-twoSum) == 1) { |
| 127 | + continue; |
| 128 | + } |
| 129 | + } |
| 130 | + |
| 131 | + if(map.containsKey(-twoSum)) { |
| 132 | + List<Integer> list = new ArrayList<>(List.of(a,b, -twoSum)); |
| 133 | + Collections.sort(list); |
| 134 | + |
| 135 | + String key = String.format("%d-%d-%d", list.get(0), list.get(1), list.get(2)); |
| 136 | + if(!keySet.contains(key)){ |
| 137 | + result.add(list); |
| 138 | + keySet.add(key); |
| 139 | + } |
| 140 | + } |
| 141 | + } |
| 142 | +} |
| 143 | +``` |
| 144 | + |
| 145 | + |
| 146 | + |
| 147 | +이전보다는 나아진 것을 볼 수 있다. |
| 148 | + |
| 149 | +### 또 개선해보기 |
| 150 | + |
| 151 | +아무래도 String 으로 key를 만드는 부분이 거슬린다. |
| 152 | + |
| 153 | +해당 부분을 제거하기 위해 두개의 set을 추가해보았다. |
| 154 | + |
| 155 | +```java |
| 156 | +Set<Integer> checkDone = new HashSet<>(); |
| 157 | +List<Integer> keySetList = new ArrayList<>(map.keySet().stream().toList()); |
| 158 | +Collections.sort(keySetList); |
| 159 | +for (int a : keySetList) { |
| 160 | + int aCount = map.get(a); |
| 161 | + Set<Integer> checkDone2 = new HashSet<>(); |
| 162 | + for (int b : keySetList) { |
| 163 | + if (a == 0 || b == 0) { |
| 164 | + continue; |
| 165 | + } |
| 166 | + |
| 167 | + int twoSum = -(a + b); |
| 168 | + if (twoSum == 0) { |
| 169 | + continue; |
| 170 | + } |
| 171 | + |
| 172 | + int twoSumCount = map.getOrDefault(twoSum, 0); |
| 173 | + if (twoSumCount == 0) { |
| 174 | + continue; |
| 175 | + } |
| 176 | + |
| 177 | + if (a == b && aCount == 1) { |
| 178 | + continue; |
| 179 | + } |
| 180 | + |
| 181 | + if (a == twoSum || b == twoSum) { |
| 182 | + if (twoSumCount == 1) { |
| 183 | + continue; |
| 184 | + } |
| 185 | + } |
| 186 | + |
| 187 | + if (checkDone.contains(b) || checkDone.contains(twoSum)) { |
| 188 | + continue; |
| 189 | + } |
| 190 | + |
| 191 | + if (checkDone2.contains(b) || checkDone2.contains(twoSum)) { |
| 192 | + continue; |
| 193 | + } |
| 194 | + |
| 195 | + if (b > twoSum) { |
| 196 | + checkDone2.add(twoSum); |
| 197 | + result.add(List.of(a, twoSum, b)); |
| 198 | + } else { |
| 199 | + checkDone2.add(b); |
| 200 | + result.add(List.of(a, b, twoSum)); |
| 201 | + } |
| 202 | + } |
| 203 | + checkDone.add(a); |
| 204 | +} |
| 205 | +``` |
| 206 | + |
| 207 | + |
| 208 | + |
| 209 | +또 다시 이전보다는 나아진 것을 볼 수 있다. |
| 210 | + |
| 211 | +### TC, SC |
| 212 | + |
| 213 | +최종적으로 시간 복잡도는 O(n^2), 공간 복잡도는 O(n^2) 이다. |
| 214 | + |
| 215 | +## 모범 답안 |
| 216 | + |
| 217 | +```java |
| 218 | +class Solution { |
| 219 | + public List<List<Integer>> threeSum(int[] nums) { |
| 220 | + List<List<Integer>> result = new ArrayList<>(); |
| 221 | + Arrays.sort(nums); |
| 222 | + for (int i = 0; i < nums.length - 2 && nums[i] <= 0; i++) { |
| 223 | + if (i != 0 && nums[i] == nums[i - 1]) continue; |
| 224 | + twoSum(-nums[i], nums, i + 1, result); |
| 225 | + } |
| 226 | + return result; |
| 227 | + } |
| 228 | + |
| 229 | + private void twoSum(int target, int[] nums, int startIndex, List<List<Integer>> result) { |
| 230 | + int i = startIndex; |
| 231 | + int j = nums.length - 1; |
| 232 | + while (i < j) { |
| 233 | + if (nums[i] + nums[j] < target) { |
| 234 | + i++; |
| 235 | + continue; |
| 236 | + } |
| 237 | + if (nums[i] + nums[j] > target) { |
| 238 | + j--; |
| 239 | + continue; |
| 240 | + } |
| 241 | + result.add(Arrays.asList(-target, nums[i], nums[j])); |
| 242 | + i++; |
| 243 | + j--; |
| 244 | + while (j > i && nums[j] == nums[j + 1]) |
| 245 | + j--; |
| 246 | + } |
| 247 | + } |
| 248 | +} |
| 249 | +``` |
| 250 | + |
| 251 | +- treesum 을 twosum 으로 나누어 생각한다. |
| 252 | +- nums 를 이미 정렬하였기 때문에 `nums[i] == nums[i - 1]` 조건을 만족하면 스킵(continue)하여 중복 입력을 방지한다. |
| 253 | +- `i < nums.length - 2 && nums[i] <= 0` |
| 254 | + |
| 255 | +### 동작 과정 |
| 256 | + |
| 257 | +example input : [-4, -2, -2, -2, 0, 1, 2, 2, 2, 3, 3, 4, 4, 6, 6] |
| 258 | + |
| 259 | +- i = 0 |
| 260 | + |
| 261 | + - twoSum(-nums[0], nums, 1, result) -> twoSum(4, nums, 1, result) |
| 262 | + - i = startIndex = 1, j = nums.length - 1 |
| 263 | + - i < j (중간에서 만나기 때문에 반목문을 다 돌지 않아도 된다.) |
| 264 | + - nums[i] + nums[j] < target : i 값이 커져야 target에 가까워짐 (i++) |
| 265 | + - nums[i] + nums[j] > target : j 값이 커져야 target에 가까워짐 (j--) |
| 266 | + - nums[i] + nums[j] == target : result에 등록 |
| 267 | + - 등록 후에는 i++, j-- 진행 하여 다른 값이 없는지 추가로 확인 (이 때 j가 중복된 값이 나오지 않도록 while로 반복) |
| 268 | + - 여기서 j 가 아닌 i 를 옮겨도 무방하다. |
| 269 | + |
| 270 | +- ... |
| 271 | + |
| 272 | +#### j가 아니라 i를 옮기는 코드 |
| 273 | + |
| 274 | +``` |
| 275 | +while (j > i && nums[i] == nums[i - 1]) |
| 276 | + i++; |
| 277 | +``` |
| 278 | + |
| 279 | +### TC, SC |
| 280 | + |
| 281 | +모범답안도 동일하게 시간 복잡도는 O(n^2), 공간 복잡도는 O(n^2) 이다. 하지만 실제 동작은 20-30ms 로 끝나기 떄문에 약 33배 차이가 난다. |
| 282 | + |
| 283 | + |
0 commit comments