문제


정수 배열 nums가 주어졌을 때, 다음 조건을 만족하는 모든 세 수의 조합 [nums[i], nums[j], nums[k]]을 반환하라.

* i != j, i != k, j != k (서로 다른 인덱스를 가져야 함)

* nums[i] + nums[j] + nums[k] == 0 (세 수의 합이 0이 되어야 함)

* 단, 중복된 조합은 포함하지 않아야 한다.

https://leetcode.com/problems/3sum/description/

 

예시

Example 1:

Input: nums = [-1,0,1,2,-1,-4]
Output: [[-1,-1,2],[-1,0,1]]

Explanation: 
nums[0] + nums[1] + nums[2] = (-1) + 0 + 1 = 0.
nums[1] + nums[2] + nums[4] = 0 + 1 + (-1) = 0.
nums[0] + nums[3] + nums[4] = (-1) + 2 + (-1) = 0.
The distinct triplets are [-1,0,1] and [-1,-1,2].
Notice that the order of the output and the order of the triplets does not matter.

Example 2:

Input: nums = [0,1,1]
Output: []

Explanation: The only possible triplet does not sum up to 0.

Example 3:

Input: nums = [0,0,0]
Output: [[0,0,0]]

Explanation: The only possible triplet sums up to 0.

 

제약

* 3 <= nums.length <= 3000
* -10^5 <= nums[i] < = 10^5  

 

1) Brute Force 풀이

import java.util.*;
public class ThreeNumberSum01TimeOut {
    public static void main(String[] args) {

        int[] nums = {-1, 0, 1, 2, -1, -5};
        List<List<Integer>> result = threeNumberSum(nums);
        System.out.println(result);

    }

    // 1 - Brute Force => O(N^3)
    public static List<List<Integer>>threeNumberSum(int[] nums) {

        Arrays.sort(nums); // {-5, -1, -1, 0, 1, 2}; => O(NlogN)

        List<List<Integer>> result = new LinkedList<>();

        for (int i = 0; i < nums.length - 2; i++) {
            
            // 중복 값 예외처리
            if (i > 0 && nums[i] == nums[i - 1]) {
                continue;
            }
            
            for (int j = i + 1 ; j < nums.length - 1; j++) {
                
                // 중복 값 예외처리
                if (j > i + 1 && nums[j] == nums[j - 1]) {
                    continue;
                }

                for (int k = j + 1; k < nums.length; k++) {
                    
                    // 중복 값 예외처리
                    if (k > j + 1 && nums[k] == nums[k - 1]) {
                        continue;
                    }

                    if (nums[i] + nums[j] + nums[k] == 0) {
                        result.add(Arrays.asList(nums[i], nums[j], nums[k]));
                    }
                }
            }
        }

        return result;
    }
}

 

가장 단순한 방법으로 '완전탐색(Brute Force)'을 적용해볼 수 있다. 

단, 문제에 '중복된 조합은 포함되어야 하지 않아야 한다'는 조건이 있으므로  중복 값 예외처리 하는 로직이 필요하다.

// 중복 값 예외처리
if (i > 0 && nums[i] == nums[i - 1]) {
    continue;
}

 

배열이 정렬된 상태에서는 '연속된 값의 중복 여부'를 판단하여 데이터 중복 저장을 방지할 수 있다.

그림에서 볼 수 있듯이 이중 혹은 삼중 Loop를 순회할 때, 연속된 인덱스에 동일한 값이 나타나면 조합이 중복해서 발생하게 된다.

연속된 값을 비교하여 중복된 값이 등장했을 때, continue 키워드를 사용하여(= 다음번 인덱스의 원소로 skip) 간단히 중복 조합 저장을 방지할 수 있다.

 

하지만 삼중 Loop의 시간복잡도는 O(N^3)이고 문제 제약 조건에서 최대 배열의 길이가 3000이므로 해당 풀이는 timeout이 발생한다.

 

2) 투 포인터를 활용한 풀이

import java.util.Arrays;
import java.util.LinkedList;
import java.util.List;

public class ThreeNumberSum02 {
    public static void main(String[] args) {

        int[] nums = {-1, 0, 1, 2, -1, -5};
        List<List<Integer>> result = threeNumberSum(nums);
        System.out.println(result);

    }

    // 1 - Two Pointer => O(N^2)
    public static List<List<Integer>>threeNumberSum(int[] nums) {

        int left, right, sum;
        Arrays.sort(nums); // {-5, -1, -1, 0, 1, 2}; => O(NlogN)
        List<List<Integer>> result = new LinkedList<>();

        for (int i = 0; i < nums.length - 2; i++) {
            // 중복 값 skip
            if (i > 0 && nums[i] == nums[i - 1]) {
                continue;
            }

            // 이후부터 two pointer로 탐색
            left = i + 1;
            right = nums.length - 1;
            while (left < right) {
                sum = nums[i] + nums[left] + nums[right];
                if (sum < 0) {
                    left++;
                }
                else if (sum > 0) {
                    right--;
                }
                else { // sum == 0

                    result.add(Arrays.asList(nums[i], nums[left], nums[right]));
                    // *중복값 건너뛰기*
                    while (left < right && nums[left] == nums[left + 1]) {
                        left++;
                    }
                    while (left < right && nums[right] == nums[right - 1]) {
                        right--;
                    }

                    // 정답이 나왔으므로 투 포인터 모두 한 칸씩 이동
                    // 합이 0인 상황이므로, 양쪽 모두 이동 필요!
                    left++;
                    right--;
                }
            }

        }

        return result;
    }
}

 

투 포인터를 활용한 풀이는 다음과 같다.

 

0. 배열을 오름차순으로 정렬

 

1. 연속적으로 중복된 원소를 제외하고(-> 중복된 조합 저장 방지), 각 원소를 순회하며 다음에 나오는 값들에 대해서 '투 포인터'를 적용

  • i : 기준이 되는 원소
  • left : 투 포인터 중 왼쪽 포인터
  • right : 투 포인터 중 오른쪽 포인터

2. 세 값의 합(nums[i] + nums[left] + nums[right])을 '0'과 비교하여 투 포인터 조정

  • sum < 0 이면, left 한칸 이동 (=> sum 값을 키움)
  • sum > 0 이면, right 한칸 이동 (=> sum 값을 줄임)
  • sum == 0 이면, 
    • 1) result 리스트에 값 저장
    • 2) 투 포인터 중 left 부분에서 연속된 중복 값이 있는지 체크 -> 중복 조합 저장 방지
    • 3) 투 포인터 중 right 부분에서 연속된 중복 값이 있는지 체크 -> 중복 조합 저장 방지
    • 4) 정답이 나왔으므로 양쪽 투 포인터 모두 한칸 이동
      • WHY) 왜 두 포인터 모두 한 칸씩 이동해야 하는가? 한 쪽만 이동해서 확인해야 하는 경우도 있지 않을까?
      • ANSWER) 두 값이 고정된 상태에서 하나만 이동했을 때 또 다시 정답이 나오는 경우는 '중복된 데이터'인 경우 밖에 없음. 하지만 그러한 경우는 문제 조건에 의해 정답이 될 수가 없다.

 

 

이렇게 투 포인터를 활용하면 

 

  • 오름차순 정렬 -> O(NlogN)
  • 배열의 각 원소를 FOR LOOP로 순회 -> O(N)
    • 투 포인터가 While 문 안에서 left부터 right 까지 순회 -> O(N)

 

최종적으로 O(N^2) 시간 안에 동작할 수 있게 된다.

 

'PS > Leetcode' 카테고리의 다른 글

24. Swap Nodes in Pairs  (0) 2025.03.29

+ Recent posts