LeetCode 167 Two Sum II 풀이: 정렬 배열 양끝 투 포인터
작성·코드 검증: solve together · 작성 언어: Python
0. 문제 링크와 출처
- 문제: 167. Two Sum II - Input Array Is Sorted
- 출처: LeetCode
- 학습: 투 포인터 학습 가이드
- 자체 판단 난이도: Level 2
플랫폼의 Medium 등급과는 별개로, 이 글에서는 Level 2로 판단했다. 배열이 정렬되어 있다는 조건에서 양끝 포인터를 움직이는 규칙을 스스로 찾아야 하므로 단순 반복문 문제인 Level 1보다는 어렵다. 다만 포인터 두 개와 합 비교만 사용하고, 재귀·복잡한 상태 관리나 특수한 자료구조가 필요하지 않아 Level 3까지의 난이도는 아니다. 1-based 인덱스 반환과 포인터를 안전하게 버릴 수 있는 이유를 설명해야 하는 점을 포함해 Level 2로 평가했다.
1. 문제 요약
오름차순으로 정렬된 1-based 정수 배열 numbers와 target이 주어진다. 서로 다른 두
원소의 합이 target이 되는 인덱스 [index1, index2]를 반환한다.
중요한 조건은 다음과 같다.
- 배열 길이는
2이상3 * 10^4이하다. - 배열은 비내림차순으로 정렬되어 있다.
- 정답은 정확히 하나 존재한다.
- 같은 원소를 두 번 사용할 수 없다.
- 반환 인덱스는 0-based가 아니라 1-based다.
- 추가 공간은 상수 공간만 사용할 수 있다.
numbers = [2,7,11,15], target = 9이면 2 + 7 = 9이므로 [1,2]를
반환한다.
2. 문제 해결에 사용되는 알고리즘이나 자료구조 설명
구간 판정과 포인터 이동
left는 배열의 첫 인덱스, right는 마지막 인덱스에서 시작한다. 두 값의 합을
current_sum이라고 할 때 정렬 조건에 따라 다음과 같이 이동한다.
current_sum < target: 합을 키워야 하므로 더 큰 값을 가리키도록left += 1current_sum > target: 합을 줄여야 하므로 더 작은 값을 가리키도록right -= 1current_sum == target: 정답을 찾았으므로 1-based 인덱스를 반환
합의 대소가 버릴 포인터를 결정하는 이유
현재 numbers[left] + numbers[right]가 target보다 작다고 하자. left를 그대로
두고 right를 더 왼쪽으로 옮기면 두 번째 값이 더 작아지므로 합은 지금보다 더 작아진다.
현재 left로는 어떤 더 오른쪽 원소와도 정답을 만들 수 없다. left를 증가시켜
더 큰 값을 시도해도 정답을 놓치지 않는다.
반대로 합이 target보다 크면 right를 그대로 두고 left를 오른쪽으로 이동할수록 합이
더 커지므로 정답이 될 수 없다. 이때는 right를 감소시켜야 한다.
이처럼 한 번 제외한 포인터 위치는 정답 후보가 될 수 없다는 근거가 있으므로, 모든 쌍을 확인하지 않고 한 번의 선형 이동으로 탐색할 수 있다.
상태 전이와 인덱스 처리
Python 리스트는 0-based 인덱스를 사용하지만 문제의 반환 형식은 1-based다.
numbers[left]와 numbers[right]를 찾은 뒤 반환할 때 각각 1을 더해야 한다.
포인터가 만날 때까지 반복한다. 문제 조건상 정답이 반드시 존재하므로 정상 입력에서는 두 포인터가 만나기 전에 반환하지만, 반복문 뒤의 반환값을 두어 함수 반환 형식을 완성한다.
오답 접근과 반례
모든 두 원소 조합을 확인하는 완전탐색은 정답을 찾을 수 있지만 O(n^2) 시간이 필요하다.
배열 길이가 3 * 10^4까지 가능하므로 모든 쌍을 검사하는 방식은 비효율적이다.
numbers = [1,2,3,...,30000]
target = 59999
정렬되어 있는데도 해시 맵을 사용해 보완값을 저장하는 방식은 평균 O(n) 시간에 풀 수
있지만 O(n) 추가 공간을 사용한다. 이 문제는 상수 공간 조건이 있으므로 투 포인터가
조건에 더 잘 맞는다.
반환 인덱스에 1을 더하지 않으면 값은 맞아도 출력 형식이 틀린다.
시간·공간 복잡도
각 반복에서 left 또는 right 중 하나가 한 칸씩 이동하고, 두 포인터는 배열을 최대 한 번
통과한다. 시간 복잡도는 O(n)이고, 포인터 몇 개만 사용하므로 공간 복잡도는 O(1)다.
3. 문제 해결 코드 (Python)
양끝 값을 더해 target과 비교하고, 합이 작은지 큰지에 따라 포인터를 이동한다. 정답을
반환할 때 문제의 1-based 인덱스 형식에 맞춰 +1을 적용한다.
class Solution:
def twoSum(self, numbers: list[int], target: int) -> list[int]:
left = 0
right = len(numbers) - 1
while left < right:
current_sum = numbers[left] + numbers[right]
if current_sum == target:
return [left + 1, right + 1]
if current_sum < target:
left += 1
else:
right -= 1
return []
정답이 없는 경우를 대비해 반복문 뒤에 빈 리스트를 반환하지만, 문제 조건에서는 항상 정답이 하나 존재한다. 같은 값이 여러 번 있어도 서로 다른 인덱스의 원소라면 정상적인 후보로 처리된다.
복사해 실행할 테스트
아래 assert를 풀이 코드와 같은 Python 파일에 붙여 실행하면 1-based 인덱스와 음수 경계를 확인할 수 있다.
solver = Solution()
assert solver.twoSum([2, 7, 11, 15], 9) == [1, 2]
assert solver.twoSum([2, 3, 4], 6) == [1, 3]
assert solver.twoSum([-1, 0], -1) == [1, 2]
4. 공식 예제와 추가 검증
작성한 twoSum 함수를 Python으로 실행해 공식 예제와 중복 값·음수·양끝 경계를 확인했다.
공식 예제
| 입력 | 기대 출력 | 확인 결과 |
|---|---|---|
numbers=[2,7,11,15], target=9 |
[1,2] |
통과 |
numbers=[2,3,4], target=6 |
[1,3] |
통과 |
numbers=[-1,0], target=-1 |
[1,2] |
통과 |
추가 검증 1: 같은 값 두 개
입력: numbers = [1,1,3,4], target = 2
기대 출력: [1,2]
확인 결과: 통과
검증 목적: 같은 값이 서로 다른 인덱스에 있을 때 정답으로 처리하는지 확인
추가 검증 2: 음수와 0
입력: numbers = [-5,-2,0,3,8], target = 1
기대 출력: [2,4]
확인 결과: 통과
검증 목적: 음수와 0이 포함된 정렬 배열에서 포인터 이동을 확인
추가 검증 3: 양끝 원소가 정답
입력: numbers = [-10,-3,0,4,12], target = 2
기대 출력: [1,5]
확인 결과: 통과
검증 목적: 초기 양끝 포인터가 바로 정답인 경우 확인
5. 다른 풀이 방식 검색 및 비교
- Two Sum II 공식 문제는 정렬된 1-based 배열에서 target을 만드는 두 인덱스를 요구한다.
- NeetCode의 Two Sum II 설명은 해시 맵 방식과 정렬 조건을 활용한 양끝 투 포인터를 비교한다.
해시 맵에 각 값을 저장하고 target - number를 조회하면 정렬 여부와 관계없이 평균 O(n)
시간에 풀 수 있다. 하지만 모든 값을 저장하므로 O(n) 공간이 필요하다.
정렬된 배열에서는 합의 크기에 따라 포인터를 안전하게 버릴 수 있으므로 추가 공간 없이
O(n) 시간에 해결하는 투 포인터 방식이 더 적합하다. 이 문제에서는 이미 정렬된 입력과
상수 공간 조건이 주어졌기 때문에 투 포인터를 선택했다.
마무리
Two Sum II는 정렬된 배열에서 양끝 두 값의 합을 비교해 탐색 범위를 줄이는 투 포인터 문제다.
합이 작으면 왼쪽을 오른쪽으로, 합이 크면 오른쪽을 왼쪽으로 이동하면 정답 후보를 안전하게
제외할 수 있다. 1-based 인덱스 반환만 주의하면 O(n) 시간과 O(1) 공간으로 해결할 수
있다.