3Sum
정수 목록 nums가 주어집니다. nums의 서로 다른 세 위치에서 가져온 값으로 이루어진 모든 세 쌍 [a, b, c] 중 a + b + c = 0을 만족하는 것을 찾으세요. 각 세 쌍을 비내림차순(a ≤ b ≤ c)으로 작성하고, 여러 위치 조합에서 같은 세 쌍이 만들어지더라도 서로 다른 세 쌍은 한 번씩만 나열하세요. 첫 번째 값, 그다음 두 번째 값 순으로 정렬된 세 쌍을 반환하세요.
함수
- numsinteger-array
- 최소 세 개의 요소를 가진 정수 목록
- 반환값integer-2d-array
- 합이 0이 되는 모든 고유한 세 항목 조합을 각각 비내림차순으로 나열하고, 목록은 정렬합니다
제약 조건
3 ≤ nums.length ≤ 3000-105 ≤ nums[i] ≤ 105- 세 수의 조합 중 적어도 하나의 합이 0입니다.
- 두 트리플은 동일한 세 값을 가지고 있을 때 같습니다.
예제
- 입력
- nums = [-2, 0, 1, 1, -1, 2]
- 출력
- [[-2, 0, 2], [-2, 1, 1], [-1, 0, 1]]
- 설명
- -2 + 0 + 2, -2 + 1 + 1, -1 + 0 + 1은 모두 0이 됩니다.
[-2, 1, 1]은 1이 두 위치에 있으므로 값 1을 두 번 사용할 수 있지만,[-1, 0, 1]은 1 중 하나를 사용해 만들 수 있으며 한 번만 나타납니다.
- 입력
- nums = [0, 0, 0, 0]
- 출력
- [[0, 0, 0]]
- 설명
- 네 개의 0 중 어느 세 개를 선택해도 합은 0입니다. 위치를 선택하는 방법은 네 가지지만, 모두 같은 세 개의 값이므로 답에는
[0, 0, 0]이 한 번만 포함됩니다.
제출 시 숨은 테스트 +15개
후속 질문
같은 패턴으로 4Sum을 해결할 수 있습니다. 두 값을 고정하고 나머지 값에 두 포인터를 적용하세요. O(n³) 시간 복잡도로 작성하면서 모든 단계에서 중복 규칙을 올바르게 처리할 수 있나요?
힌트
하나씩 열어 보세요. 열 때마다 조금 더 알려 줍니다.
먼저 목록을 정렬하세요. 정렬된 목록은 두 가지 면에서 도움이 됩니다. 각 세 숫자 조합이 순서대로 나오고, 같은 값끼리 서로 붙어 있으므로 반복되는 값은 항상 반복되는 값 바로 옆에 놓입니다.
삼중항의 가장 작은 값인
nums[i]를 고정합니다. 나머지 두 값의 합은-nums[i]여야 하며, 이 값들은i의 오른쪽에 있는 정렬된 값들에서 가져옵니다. 이는 정렬된 목록에서 두 수의 합을 구하는 문제입니다.해당 쌍의 경우, 포인터 하나는
i바로 뒤에서 시작하고 다른 하나는 마지막 인덱스에서 시작하세요. 세 값의 합이 0보다 작으면 왼쪽 포인터를 오른쪽으로 이동하고, 0보다 크면 오른쪽 포인터를 왼쪽으로 이동하세요. 일치하는 값을 찾으면 두 포인터를 모두 이동하고, 왼쪽 포인터의 값과 같은 값이 반복되는 동안 왼쪽 포인터를 건너뛰세요. 값이 바로 앞의 값과 같은i는 건너뛰세요.
풀이
두 가지 이유로 3Sum은 보기보다 어렵습니다. 모든 세 쌍을 확인하는 데 O(n³)이 들고, 값이 반복되더라도 답에는 각 세 쌍이 한 번씩만 포함되어야 합니다. 정렬하면 두 문제를 모두 해결할 수 있습니다. 같은 값이 나란히 놓이므로 이웃한 값끼리 비교해 중복을 건너뛸 수 있고, 가장 작은 값을 하나 고정하면 나머지 두 값은 정렬된 목록에서 두 포인터를 한 번 훑어 해결하는 두 수의 합 문제로 바뀝니다.
모든 트리플을 시도해 보세요
정답이지만 가장 큰 테스트에서는 끝나지 않음
핵심 아이디어
먼저 목록을 정렬하세요. 그러면 임의의 세 위치 i < j < k에 있는 값은 이미 순서대로 배치되어 있으므로 nums[i] ≤ nums[j] ≤ nums[k], 트리플릿을 찾는 즉시 올바르게 작성됩니다. 중첩된 세 개의 반복문은 모든 위치 조합을 방문하므로 어떤 트리플릿도 놓치지 않습니다.
다음은 중복입니다. 첫 번째 예제를 정렬하면 [-2, -1, 0, 1, 1, 2]이고, [-1, 0, 1]의 1은 인덱스 3이나 인덱스 4에서 가져올 수 있습니다. 따라서 각 반복문은 같은 반복문에서 이전에 시도한 값과 같은 값을 가진 위치를 건너뜁니다. 그러면 각 반복문은 서로 다른 값을 각각 한 번씩 시도하고, 서로 다른 각 트리플릿은 정렬된 상태로 한 번씩만 나옵니다. 건너뛰기는 같은 반복문 안의 이전 위치와만 비교하므로 [-2, 1, 1]에서는 여전히 두 개의 1을 모두 사용합니다.
문제는 비용입니다. 트리플릿은 약 n³/6개이므로, 숫자 3000개라면 합계를 4.5 × 10^9번 계산해야 하며, 이는 어떤 시간 제한도 훨씬 초과합니다.
알고리즘
nums를 정렬합니다.- 각 위치를 대상으로
i를 반복하고,nums[i]가nums[i-1]과 같으면i를 건너뜁니다. - 그 안에서
i+1부터j를 반복하고,j > i+1이고nums[j]가nums[j-1]과 같으면j를 건너뜁니다. - 그 안에서 같은 건너뛰기 규칙을 적용해
j+1부터k를 반복하고, 세 수의 합이 0이면[nums[i], nums[j], nums[k]]를 기록합니다. - 찾은 순서대로 세 수 조합을 반환합니다. 이미 정렬되어 있습니다.
def threeSum(nums):
nums = sorted(nums)
n = len(nums)
triplets = []
for i in range(n - 2):
if i > 0 and nums[i] == nums[i - 1]:
continue # same first value as the round before
for j in range(i + 1, n - 1):
if j > i + 1 and nums[j] == nums[j - 1]:
continue # same second value as the round before
for k in range(j + 1, n):
if k > j + 1 and nums[k] == nums[k - 1]:
continue # same third value as the round before
if nums[i] + nums[j] + nums[k] == 0:
triplets.append([nums[i], nums[j], nums[k]])
return triplets값 하나를 고정하고 해시 집합으로 쌍 찾기
핵심 아이디어
첫 번째 값 nums[i]를 고정하면, 합이 -nums[i]가 되는 뒤쪽 값 두 개가 필요합니다. 이것이 Two Sum입니다. j를 i의 오른쪽으로 이동시키고, 지나온 값들을 집합에 저장합니다. 각 j에서 필요한 값은 need = -nums[i] - nums[j]입니다. need가 집합에 있으면 [nums[i], need, nums[j]]의 합은 0입니다. 집합 조회는 평균적으로 O(1)이 걸리므로, 하나의 i에 O(n), 전체 탐색에는 O(n²)이 걸립니다.
정렬은 여전히 중복 처리를 도와줍니다. 값이 바로 앞의 값과 같은 i는 건너뜁니다. 일치하는 값을 찾은 뒤에는 j를 nums[j]의 모든 복사본을 지나도록 이동합니다. 첫 번째 값과 세 번째 값이 고정되면 가운데 값도 고정되므로, 같은 값이 또 나오면 동일한 세 수 조합만 반복됩니다. need는 정렬된 목록에서 더 앞선 위치의 값이므로 need ≤ nums[j]이고, 세 수 조합은 순서대로 정렬됩니다. 또한 nums[i] > 0이 되는 즉시 탐색을 중단할 수 있습니다. 그 뒤의 두 값은 이 값보다 크거나 같으므로 합이 0이 될 수 없습니다.
한 가지 세부 사항이 있습니다. j가 오른쪽으로 이동하면 nums[j]는 커지고 need는 작아지므로, 하나의 i에 대한 세 수 조합은 가운데 값이 감소하는 순서로 나옵니다. [-2, -1, 0, 1, 1, 2]에서 i = 0일 때 두 번째 1에서 [-2, 1, 1]을 찾고, 그다음 2에서 [-2, 0, 2]를 찾습니다. 각 그룹을 답에 추가하기 전에 뒤집으세요. C 및 R 버전은 해시 집합 대신 값으로 인덱싱된 배열에 확인한 값을 표시합니다. 모든 값이 ±10^5 범위에 있으므로 이렇게 할 수 있습니다.
알고리즘
nums를 정렬합니다.- 각
i에 대해nums[i] > 0이면 중단하고,nums[i]가nums[i-1]와 같으면i를 건너뜁니다. - 빈 집합을 시작합니다.
i+1부터 각j에 대해need = -nums[i] - nums[j]를 계산합니다.need가 집합에 있으면[nums[i], need, nums[j]]를 기록하고nums[j]의 같은 값들이 이어지는 부분을 지나도록j를 이동합니다. nums[j]를 집합에 추가하고 다음j로 넘어갑니다.- 이
i에서 찾은 세 수 조합을 뒤집어 답에 추가합니다.
def threeSum(nums):
nums = sorted(nums)
n = len(nums)
triplets = []
for i in range(n - 2):
if nums[i] > 0:
break # the two values after it are at least as large
if i > 0 and nums[i] == nums[i - 1]:
continue # this first value was already handled
group = []
seen = set() # values between position i and position j
j = i + 1
while j < n:
need = -nums[i] - nums[j]
if need in seen:
group.append([nums[i], need, nums[j]])
while j + 1 < n and nums[j + 1] == nums[j]:
j += 1
seen.add(nums[j])
j += 1
# need shrinks as nums[j] grows, so this group came out backwards
group.reverse()
triplets.extend(group)
return triplets정렬하고 두 개의 포인터 사용하기
핵심 아이디어
정렬된 순서로 집합을 대체할 수 있습니다. nums[i]를 고정하고, lo를 i+1에, hi를 마지막 인덱스에 둔 다음 nums[i] + nums[lo] + nums[hi]를 살펴봅니다. 합이 0보다 작으면 더 큰 값이 필요하므로 lo를 오른쪽으로 이동합니다. 0보다 크면 더 작은 값이 필요하므로 hi를 왼쪽으로 이동합니다. 정확히 0이면 세 값의 조합을 기록하고 두 포인터를 모두 이동합니다.
어떤 세 값의 조합도 놓치지 않습니다. 합이 0보다 작을 때는 남은 값 중 가장 큰 값인 nums[hi]와 더해도 nums[lo]가 부족하므로, 현재 범위에 남아 있는 어떤 값과도 짝을 이룰 수 없습니다. 따라서 이 값을 버려도 아무것도 잃지 않습니다. 0보다 큰 경우는 그 반대입니다. 남은 값 중 가장 작은 값과 더해도 nums[hi]가 너무 큽니다. 매 단계마다 값 하나를 영구적으로 제외하므로, i 하나당 최대 n번의 단계가 필요하고 전체 탐색은 O(n²)이며, 정렬과 결과 출력 외에는 메모리가 필요하지 않습니다.
정렬된 [-2, -1, 0, 1, 1, 2]를 살펴보겠습니다. i = 0일 때(값은 -2), lo는 -1에서 시작하고 hi는 2에서 시작합니다. 합은 -1이므로 lo를 0으로 이동합니다. 이제 -2 + 0 + 2 = 0이므로 [-2, 0, 2]를 기록하고, 두 포인터는 1 두 개가 있는 위치에 도달합니다. 이 값들로 [-2, 1, 1]을 얻습니다. i = 1일 때(값은 -1), 0과 2의 합은 1이므로 hi를 두 번째 1로 이동하고, -1 + 0 + 1 = 0이 되어 [-1, 0, 1]을 기록합니다. i = 2의 값 0으로는 아무것도 찾지 못하고, i = 3에서는 값이 양수이므로 탐색을 중단합니다.
중복 값은 두 가지 규칙으로 처리해야 합니다. 값이 바로 앞의 값과 같은 i는 건너뜁니다. 일치하는 값을 찾은 후에는 lo를 사용한 값의 중복 항목들을 지나 이동합니다. hi에는 별도의 규칙이 필요하지 않습니다. lo가 더 큰 값에 놓이면, 이전 nums[hi]의 중복 값은 이제 합을 0보다 크게 만들고 저절로 제외됩니다. i가 서로 다른 값들을 오름차순으로 방문하고 lo는 오른쪽으로만 이동하므로, 세 값의 조합도 정렬된 순서로 나옵니다.
알고리즘
nums를 정렬합니다.- 각
i에 대해nums[i] > 0이면 중단하고,nums[i]가nums[i-1]과 같으면i를 건너뜁니다. lo = i+1과hi = n-1을 설정합니다.lo < hi인 동안nums[i],nums[lo],nums[hi]를 더합니다.- 합이 0보다 작으면
lo를 오른쪽으로 이동합니다. 0보다 크면hi를 왼쪽으로 이동합니다. - 합이 0이면 세 수의 조합을 기록하고, 두 포인터를 모두 이동한 다음
lo를 사용한 값의 중복 항목을 지나도록 이동합니다. - 세 수의 조합을 반환합니다. 이미 정렬되어 있습니다.
def threeSum(nums):
nums = sorted(nums)
n = len(nums)
triplets = []
for i in range(n - 2):
if nums[i] > 0:
break # the two values after it are at least as large
if i > 0 and nums[i] == nums[i - 1]:
continue # this first value was already handled
lo, hi = i + 1, n - 1
while lo < hi:
total = nums[i] + nums[lo] + nums[hi]
if total < 0:
lo += 1
elif total > 0:
hi -= 1
else:
triplets.append([nums[i], nums[lo], nums[hi]])
lo += 1
hi -= 1
while lo < hi and nums[lo] == nums[lo - 1]:
lo += 1
return triplets
함정과 경계 사례
오답의 대부분은 중복된 값에서 비롯되므로, 중복된 값이 있는 입력으로 테스트하세요.
nums[i]가nums[i+1]과 같을 때i를 건너뛰면 각 값의 마지막 복사본이 첫 번째 요소로 남고, 그 앞에 있는 복사본들은 사라집니다.[-1, -1, 2]에서는[-1, -1, 2]를 잃게 됩니다. 이전 위치인nums[i-1]과 비교하세요.nums[i] > 0대신nums[i] ≥ 0일 때 중단하면[0, 0, 0]을 놓칩니다.- 중복을 건너뛰는 대신 마지막에 제거하는 경우입니다. 0이 3000개 있으면 투 포인터 루프는 정리 작업을 하기 전에
[0, 0, 0]의 복사본을 수백만 개 기록하며, 여러 언어에서는 리스트 집합이 리스트를 동일성으로 비교하므로 복사본이 그대로 남습니다. - 같은 위치를 두 번 사용하는 경우입니다. 집합을 처음부터 전체 목록으로 채우는 해시 집합 방식은 하나뿐인 1을 두 번 사용해
[-2, 1, 3]을[-2, 1, 1]로 만듭니다. 이미 지나온 위치의 값만 조회하세요. - 세 수의 조합을 순서가 뒤바뀐 채 반환하는 경우입니다. 비교는 정확히 일치해야 하므로 해시 집합 방식에서는 각 그룹을 뒤집어야 하며, 세 수의 조합을 집합에 모으는 해법에서는 마지막에 정렬해야 합니다.
자주 묻는 질문4
3Sum의 시간 복잡도는 얼마인가요?
정렬 및 투 포인터 솔루션은 O(n²) 시간에 실행됩니다. 정렬에는 O(n log n)이 걸리고, 첫 번째 값을 선택하는 n가지 경우 각각에 대해 O(n) 순회를 한 번 수행합니다. 정렬과 출력에 필요한 공간을 제외하면 추가 공간은 O(1)입니다. 모든 세 쌍을 확인하면 대신 O(n³)이 걸립니다.
3Sum은 중복된 세 쌍을 어떻게 방지하나요?
목록을 정렬하므로 같은 값끼리 나란히 놓입니다. 그런 다음 바로 앞의 값과 같은 첫 번째 값은 건너뛰고, 일치할 때마다 왼쪽 포인터를 사용한 값의 중복 항목 뒤로 이동합니다. 각 세 값의 조합은 해당 값의 첫 번째 항목부터 한 번씩만 찾으므로, 결과를 저장할 집합은 필요하지 않습니다.
3Sum에는 투 포인터와 해시 집합 중 무엇을 사용해야 하나요?
둘 다 O(n²) 시간이 걸립니다. 투 포인터 방식은 추가 메모리가 필요 없고, 정렬된 순서 덕분에 트리플릿이 이미 순서대로 나옵니다. 해시 집합은 O(n) 메모리가 필요하며, 위치가 서로 다르고 결과가 정렬된 상태를 유지하도록 주의해야 합니다. Two Sum처럼 원래 인덱스를 반환하는 경우와 같이 정렬할 수 없을 때 해시 집합 아이디어가 중요합니다.
3Sum을 O(n²)보다 더 빠르게 풀 수 있을까요?
큰 차이는 아닙니다. 가장 잘 알려진 알고리즘도 n²보다 몇 개의 로그 인자만큼 빠를 뿐이며, 계산 기하학의 많은 난이도 결과는 어떤 알고리즘도 2보다 작은 n의 거듭제곱에 도달하지 못한다고 가정합니다. 그런 더 빠른 알고리즘은 연구 결과이므로, 면접에서 기대하는 답은 O(n²)입니다.
비슷한 문제
같은 아이디어를 쓰는 문제입니다. 두세 개를 풀면 패턴이 몸에 익습니다.
Python
def threeSum(nums):
# 여기에 코드를 작성하세요케이스 1
케이스 2
입력
nums = [-2, 0, 1, 1, -1, 2]
기대값
[[-2, 0, 2], [-2, 1, 1], [-1, 0, 1]]