Skip to content

Commit aea6468

Browse files
add post '(Leetcode) 15 - 3Sum'
1 parent 2fe1729 commit aea6468

File tree

5 files changed

+283
-0
lines changed

5 files changed

+283
-0
lines changed

_posts/2024-05-07-leetcode-15.md

+283
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,283 @@
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+
![first-approach](/assets/images/2024-05-07-leetcode-15/first-approach.png)
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+
![improve-first-approach](/assets/images/2024-05-07-leetcode-15/improve-first-approach.png)
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+
![improve-again-first-approach](/assets/images/2024-05-07-leetcode-15/improve-again-first-approach.png)
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+
![best answer](/assets/images/2024-05-07-leetcode-15/best-answer.png)
Loading
Loading
Loading
Loading

0 commit comments

Comments
 (0)