[LeetCode] 3Sum - 이분탐색, 투포인터
리뷰
해당 문제는 완전탐색 → 완전탐색 조합 → 이분탐색으로 이어지기까지 시간복잡도를 어떻게 단축시킬 수 있는지, 접근 방법에 대해 생각해본 좋은 문제였다.
1. 조합을 구하는 시간복잡도가 너무 크면 가지치기가 큰 의미가 없을 수 있다.
완전탐색으로 조합을 모두 구하는 경우는 O(3000 C 3)으로 TLE가 발생한다. 하지만, 다른 풀이가 생각나지 않았고 promising (유망함)으로 가지치기를 시도하였으나 TLE가 발생하였다. 이럴 경우, 시간 복잡도를 만족하는 다른 풀이를 생각해보자.
2. 시간복잡도를 줄여나가는 과정: 시간복잡도를 쪼개고 어느 부분의 시간을 단축시킬 수 있을지 고민하자.
시간복잡도를 줄여나가기 위해선 처음에 완전탐색부터 시작할 것이다. 완탐의 시간복잡도에서 정확히 어느 부분의 계산에서 시간복잡도를 줄일 수 있을지 파악하자.
예를 들어, 해당 문제는 완전탐색의 경우 O(N^3)의 시간이 소요된다. 이를 쪼개서 생각하면 O(N * N * N) (N: i구하는 시간, N: j구하는 시간, N: k구하는 시간) 이다. nums[i], nums[j]를 O(N^2)로 구해놨다면, nums[k]는 nums[i] + nums[j] + nums[k] == 0이라는 조건에 맞게, nums를 이분탐색하여 구할 수 있을 것이다. 즉, k구하는 시간을 log scale로 떨어트릴 수 있을 것이다.
3. 조건을 확실히 짚고 넘어가자.
조건1: i != j, i != k, j != k
서로 다른 위치만 가능하고, for문 탐색할 때 i, j, k의 범위를 유의해야할 것이다.
조건2: 중복은 포함하지 않는다.
ex) (-1, 0, 1), (0, 1, -1)
이를 정확히 짚고 구현해줘야 한다.
이분탐색 풀이
[제한]
- 숫자개수 n: 3 ~ 3,000
- O(n^2)까지 가능
[유형]
Goal: nums[i] + nums[j] + nums[k] == 0인 모든 (nums[i], nums[j], nums[k])를 구하라.
* i != j, i != k, j != k → 서로 다른 위치여야 함.
* 중복은 포함하지 않는다. → (-1, 0, 1) , (0, 1, -1)
유형1. 완전탐색: O(N^3) → TLE
O(i 구하는 시간 * j 구하는 시간 * k 구하는 시간)
유형2. 완탐으로 조합구하기: O(3000 C 3) → O(45억) → TLE
유형3. 완탐에서 일부를 log scale로 떨어트릴 순 없을까?
완전탐색: O(N * N * N) (N: i구하는 시간, N: j구하는 시간, N: k구하는 시간)
nums[i] 와 nums[j]를 O(N^2)로 구했다면, nums[i] + nums[j] + nums[k] == 0을 만족하는 nums[k]값이 nums에 존재하는지 확인하면 될 것. 정렬 후, 이분탐색
=> 정렬 + 이분탐색: O(N * N * logN) (N: i구하는 시간, N: j구하는 시간, N: k구하는 시간)
[설계]
1. nums 정렬
2. for문으로 i, j 탐색하며 k는 이분탐색
중복을 포함하지 않아야 하므로 다음과 같은 조건식 추가.
// 이미 이전 완탐에서 (i, x, x)를 구한경우 (정렬된 상태이므로)
private boolean alreadyFindOne(int i, int[] nums) {
return i!=0 && nums[i-1]==nums[i];
}
// 이미 이전 완탐에서 (i, j, x)를 구한경우. (정렬된 상태이므로)
private boolean alreadyFindTwo(int i, int j, int[] nums) {
return j!=i+1 && nums[j-1]==nums[j];
}
[코드]
class Solution {
public List<List<Integer>> threeSum(int[] nums) {
return pro(nums);
}
//O(N^2 logN)
private List<List<Integer>> pro(int[] nums) {
List<List<Integer>> answer = new ArrayList<>();
// 1. 정렬: O(N logN)
sortByAscending(nums);
// 2. for문 탐색하며 이분탐색: O(N * NlogN)
for(int i=0; i<nums.length; i++) {
if(alreadyFindOne(i, nums)) {
continue;
}
for(int j=i+1; j<nums.length-1; j++) {
if(alreadyFindTwo(i, j, nums)) {
continue;
}
int sum = nums[i] + nums[j];
int k = 0 - sum;
if(binarySearch(k, j+1, nums)) {
List<Integer> temp = List.of(nums[i], nums[j], k);
answer.add(temp);
};
}
}
return answer;
}
// 이미 이전 완탐에서 (i, x, x)를 구한경우 (정렬된 상태이므로)
private boolean alreadyFindOne(int i, int[] nums) {
return i!=0 && nums[i-1]==nums[i];
}
// 이미 이전 완탐에서 (i, j, x)를 구한경우. (정렬된 상태이므로)
private boolean alreadyFindTwo(int i, int j, int[] nums) {
return j!=i+1 && nums[j-1]==nums[j];
}
// find가 있으면 true, 없으면 false
private boolean binarySearch(int find, int left, int[] nums) {
int right = nums.length-1;
while(left <= right) {
int mid = (left + right) / 2;
if(nums[mid] == find) {
return true;
} else if(nums[mid] < find) {
left = mid + 1;
} else {
right = mid - 1;
}
}
return false;
}
private void sortByAscending(int[] nums) {
Arrays.sort(nums);
}
}