LeetCode/Grind169

15. 3Sum ☆☆☆★

hyunkookim 2025. 4. 23. 05:26

15. 3Sum

class Solution:
    def threeSum(self, nums: List[int]) -> List[List[int]]:
        # 숫자 3개 더해서 0
        # for 돌리면서 투포인터 사용
        nums.sort() # 우선 정렬 -> 그래야.. 투포인터 사용가능        
        
        res = []
        for a in range(len(nums)):
            l, r = a+1, len(nums)-1
            while l<r: # 같으면 나옴
                if nums[a]+ nums[l] + nums[r] == 0:
                    res.append([nums[a], nums[l], nums[r]])
                elif nums[a]+ nums[l] + nums[r] > 0:
                    r -=1
                else: # nums[a]+ nums[l] + nums[r] < 0:
                    l +=1
        return res

 

지금 코드는 3Sum 문제의 기본 구조는 잘 잡았는데,
중복 제거 로직이 빠져 있어서 '오답'이 나올 수 있어요.


❌ 문제점 요약

  1. 중복된 a 값에 대해서도 계속 검사하고 있음 → [-1, -1, 2] 같은 조합이 중복해서 나올 수 있음
  2. 중복된 l 또는 r 값도 고려하지 않음 → 예: [0, 0, 0]이 여러 번 나올 수 있음

✅ 수정된 코드 (정답 + 중복 제거):

class Solution:
    def threeSum(self, nums: List[int]) -> List[List[int]]:
        nums.sort()
        res = []

        for i in range(len(nums)):
            # 중복된 i 값은 skip
            if i > 0 and nums[i] == nums[i-1]:
                continue

            l, r = i+1, len(nums)-1
            
            """
            # while j<k and k<len(nums)-1: 하면 안됨.
            이미 k = len(nums) - 1로 초기화했기 때문에,
            k < len(nums) - 1은 **처음부터 False**가 됩니다.
            그 결과, while 루프가 한 번도 실행되지 않을 수 있습니다.
            """
            while l < r:
                total = nums[i] + nums[l] + nums[r]
                if total < 0:
                    l += 1
                elif total > 0:
                    r -= 1
                else:
                    res.append([nums[i], nums[l], nums[r]])
                    # 중복된 l, r 피하기 위해 skip
                    while l < r and nums[l] == nums[l+1]:
                        l += 1
                    while l < r and nums[r] == nums[r-1]:
                        r -= 1
                    # 다음 후보로 이동
                    l += 1
                    r -= 1

        return res

🔍 핵심 아이디어 요약

단계 설명
1. 정렬 투 포인터 사용 가능하게 만들기 위해
2. for 루프 첫 번째 숫자 nums[i] 고정
3. 투 포인터 i+1부터 끝까지 l, r로 탐색
4. 중복 제거 nums[i], nums[l], nums[r] 값이 이전과 같으면 skip

 

중복제거를 왜 total 이 0 일때만 해? 0보다 작을때랑 클때도 해야하는거 아님?

 

ChatGPT의 말:

좋은 질문이에요! 😊
겉보기엔 total < 0이나 total > 0일 때도 중복 제거가 필요해 보이지만,
실제로는 total == 0일 때만 중복 제거를 하면 충분해요.

왜 그런지 단계별로 설명할게요.


🔍 핵심 원리: 중복된 결과만 제거하면 됨

우리가 중복 제거를 하는 **진짜 이유는 "결과 조합이 중복되는 걸 막기 위해서"**예요.

즉,

  • [-1, 0, 1]
  • [-1, 0, 1]
    이런 식으로 같은 triplet이 결과에 여러 번 나오는 걸 막는 게 목표죠.

✅ total == 0일 때만 중복 제거를 해도 되는 이유

예시: [ -1, -1, 2 ]

nums = [-1, -1, -1, 2]
  • 첫 번째 -1에서 (l, r)이 (1, 3) → [-1, -1, 2] ⬅️ 유효
  • 다음 l=2도 -1이니까 중복 → while로 스킵!

이렇게 중복 제거는 유효한 조합을 찾았을 때만 의미가 있어요.


❌ 만약 total < 0이거나 total > 0일 때 중복을 제거하면?

  • 이 경우는 아직 정답이 아니에요.
  • 그러니 굳이 중복 제거해봤자 "정답 후보도 아니고",
    오히려 건너뛴 값이 다음 정답 후보가 될 수도 있어요.

예:

nums = [-2, -1, -1, 0, 1, 2]
  • nums[i] = -2
  • l = 1, r = 5, total = -1 → l += 1 해야 함
  • 여기서 중복 제거한다고 l = 3까지 넘기면 [-2, 0, 2] 같은 조합을 놓칠 수 있음

🔚 결론

조건 중복 제거 해야 함? 이유
total == 0 ✅ YES 결과 조합 중복 방지
total < 0 / > 0 ❌ NO 정답 아님 → 중복 제거 의미 없음, 오히려 정답을 건너뛸 수도 있음



class Solution:
    def threeSum(self, nums: List[int]) -> List[List[int]]:
        # 숫자 3개 더해서 0
        # for 돌리면서 투포인터 사용
        nums.sort() # 우선 정렬 -> 그래야.. 투포인터 사용가능        
        
        res = []
        for a in range(len(nums)):
            # 중복된 nums[a] 값 skip
            while a>0 and nums[a-1] == nums[a]:
                continue

            l, r = a+1, len(nums)-1
            while l<r: # 같으면 나옴
                subSum = nums[a]+ nums[l] + nums[r]
                if subSum == 0:
                    res.append([nums[a], nums[l], nums[r]])

                    # 같을때 결과 추가 후, 중복 제거 위해, index 옮김
                    while l<r and nums[l] == nums[l+1]:
                        l +=1
                    while l<r and nums[r-1] == nums[r]:
                        r -=1
                    # 다음 스텝을 위해 이동
                    l+=1
                    r-=1
                elif subSum > 0:
                    r -=1
                else: # subSum < 0:
                    l +=1
        return res

 

지금 코드에서 **시간 제한(TLE)**이 걸리는 가장 큰 이유
👉 while a > 0 and nums[a-1] == nums[a]: continue 이 무한 루프로 작동하기 때문이에요.


❌ 문제 원인: continue 안에서 a가 증가하지 않음

for a in range(len(nums)):
    while a > 0 and nums[a-1] == nums[a]:
        continue  # ❗️ a는 증가하지 않음 → 무한 루프 가능성

이렇게 쓰면 a는 그대로인데 while 조건만 계속 검사해서 빠져나오지 못해요.
CPU 무한 루프 발생 → 시간 초과(TLE)


✅ 고치려면 이렇게:

while 대신 if + continue 를 써야 해요:

for a in range(len(nums)):
    if a > 0 and nums[a] == nums[a - 1]:
        continue

→ 이러면 중복된 a 값은 스킵하고, 다음 루프로 넘어가요. (a는 for문이 자동으로 증가시켜 줌)

 

최종 코드

class Solution:
    def threeSum(self, nums: List[int]) -> List[List[int]]:
        # 숫자 3개 더해서 0
        # for 돌리면서 투포인터 사용
        nums.sort() # 우선 정렬 -> 그래야.. 투포인터 사용가능        
        
        res = []
        for a in range(len(nums)):
            # 중복된 nums[a] 값 skip
            if a>0 and nums[a-1] == nums[a]:
                continue

            l, r = a+1, len(nums)-1
            while l<r: # 같으면 나옴
                subSum = nums[a]+ nums[l] + nums[r]
                if subSum == 0:
                    res.append([nums[a], nums[l], nums[r]])

                    # 같을때 결과 추가 후, 중복 제거 위해, index 옮김
                    while l<r and nums[l] == nums[l+1]:
                        l +=1
                    while l<r and nums[r-1] == nums[r]:
                        r -=1
                    # 다음 스텝을 위해 이동
                    l+=1
                    r-=1
                elif subSum > 0:
                    r -=1
                else: # subSum < 0:
                    l +=1
        return res

 



3번째 코드

class Solution:
    def threeSum(self, nums: List[int]) -> List[List[int]]:
        # 3개 합이 0 되는 조합 추가
        # i 는 for index 로, j, k 는 투포인터로

        res = []
        for i in range(len(nums)):
            j, k = i+1, len(nums)-1

            while j<len(nums) and j<=k:
                total = nums[i]+nums[j]+nums[k]

                if total > 0:
                    k-=1
                elif total < 0: 
                    j+=1
                else: #total==0
                    res.append([nums[i], nums[j], nums[k]])
        return res

 

❌ 현재 코드의 문제점

  1. 정렬을 안 해투포인터가 의미 없음
    → 투포인터 기법은 배열이 정렬되어 있어야 방향성을 알 수 있습니다.
  2. 중복 결과 필터링 안함
    → [0,0,0] 같이 같은 조합이 여러 번 나올 수 있음
  3. j <= k 조건이 이상함
    j < k가 되어야지 j == k일 땐 의미 없는 조합이 됩니다.

❌ 코드 문제점 요약

  1. if i-1 > 0 and nums[i] == nums[i-1]:
    i > 0으로 써야지 제대로 중복 제거가 됩니다.
    i-1 > 0이면 i >= 2부터만 비교하게 되어 첫 중복 체크를 놓침.
  2. while nums[k-1] == nums[k]:
    → k-1이 배열 밖으로 나갈 수 있음, k > j 조건을 추가해야 안전
  3. while nums[j] == nums[j+1]:
    → j+1도 배열 밖 접근 가능성 있음, j < k 조건 필요
  4. j<len(nums) and j<k
    → j < k 하나만 쓰면 충분하고 더 직관적
  5. 중복 스킵한 후 j += 1, k -= 1 안 해줌
    같은 값 건너뛴 후에도 한 칸 이동은 꼭 필요
class Solution:
    def threeSum(self, nums: List[int]) -> List[List[int]]:
        # 3개 합이 0 되는 조합 추가
        # i 는 for index 로, j, k 는 투포인터로
        
        # 투 포인터 사용하려면, 정렬이 되어있어야!! <-- 아주 중요!!
        nums.sort()

        res = []
        for i in range(len(nums)):
            if i>0 and nums[i] == nums[i-1]: # 이전값이랑 같으면 그냥 넘어감, i==1 부터 검색해야하므로, i>0 임!!
                continue
            
            j, k = i+1, len(nums)-1

            while j<k: # j != k 되어야해서, j<=k 아니라, j<k 되어야 함
                total = nums[i]+nums[j]+nums[k]

                if total > 0:
                    k-=1
                elif total < 0: 
                    j+=1
                else: #total==0
                    res.append([nums[i], nums[j], nums[k]])

                    # 추가했으니, 또 다른 조합 찾아야.
                    while j<k and nums[k-1] == nums[k]: # k 는 왼쪽으로 가야하는데, 현재값이랑 계속 같으면 스킵
                        k-=1
                    while j<k and nums[j] == nums[j+1]: # j 는 오른쪽으로 가야하는데, 현재값이랑 계속 같으면 스킵
                        j+=1
                    # 스킵해도 마지막에 옮겨야, 새로운 값이 나옴
                    k-=1
                    j+=1
                    
        return res