Max Number of K-Sum Pairs — 정렬 `O(n log n)`에서 해시 `O(n)`까지 최대 페어 수 세기
전체 보기접기
- 01Merge Strings Alternately — 단일 루프로 `O(n + m)` 교차 병합
- 02Increasing Triplet Subsequence — `O(n)` 시간, `O(1)` 공간으로 푸는 그리디 패턴
- 03Move Zeroes — 쓰기 포인터 하나로 `O(n)`/`O(1)` 제자리 재배치
- 04String Compression — 읽기/쓰기 포인터로 `O(n)`/`O(1)` 제자리 압축
- 05Product of Array Except Self — 나눗셈 없이 `O(n)`/`O(1)`로 나머지 곱 구하기
- 06Reverse Vowels of a String — 투 포인터 `O(n)` 스왑으로 모음만 뒤집기
- 07Can Place Flowers — `O(n)` 그리디 스캔으로 인접 금지 배치 판정
- 08시간 복잡도와 공간 복잡도 완전 정복 — Big-O 표기법부터 실전 분석까지
- 09Greatest Common Divisor of Strings — `GCD`로 `O(n + m)` 문자열 최대 공약 패턴
- 10Container With Most Water — 투 포인터 `O(n)`로 최대 넓이 한 번에 찾기
- 11Is Subsequence — 투 포인터 `O(|s| + |t|)`로 부분 수열 판정
- 12Richest Customer Wealth — 2차원 배열에서 행 합의 최댓값 찾기
- 13Running Sum of 1d Array — 누적합의 가장 기본 형태
- 14Roman to Integer — 감산 규칙을 한 번의 스캔으로 처리하기
- 15Palindrome Number — 문자열 없이 절반만 뒤집는 숫자 팰린드롬 판정
- 16Max Number of K-Sum Pairs — 정렬 `O(n log n)`에서 해시 `O(n)`까지 최대 페어 수 세기읽는 중
- 17Ransom Note — 문자 개수로 문자열 구성 가능 여부 판정하기
- 18Middle of the Linked List — slow/fast 포인터로 중간 노드 찾기
- 19Max Consecutive Ones — 한 번의 스캔으로 가장 긴 1 구간 찾기
- 20Find Numbers with Even Number of Digits — 자릿수 개수의 짝수 여부 세기
- 21Merge Sorted Array — 뒤에서부터 채우는 제자리 병합
- 22Duplicate Zeros — 고정 길이 배열을 오른쪽에서부터 제자리 갱신하기
- 23Squares of a Sorted Array — 절댓값 기준 투 포인터로 `O(n)` 정렬 유지하기
- 24Valid Mountain Array — 올라갔다가 내려오는 배열 판별하기
- 25Check If N and Its Double Exist — 어떤 값과 그 두 배가 함께 있는지 확인하기
- 26Remove Duplicates from Sorted Array — 정렬 배열을 고유 값 구간으로 압축하기
- 27Remove Element — 앞쪽 유효 구간으로 압축하는 제자리 제거
- 28Replace Elements with Greatest Element on Right Side — 오른쪽 최댓값으로 바꾸기
- 29Sort Array By Parity — 짝수를 앞쪽으로 모으기
- 30Height Checker — 기대 순서와 다른 위치 세기
- 31Third Maximum Number — 세 변수로 상위 3개를 추적해 찾기
- 32Reorder Data in Log Files — 문자 로그만 정렬하고 숫자 로그는 순서 유지하기
- 33Valid Palindrome — 투 포인터로 영숫자만 비교하는 팰린드롬 판정
- 34Find All Numbers Disappeared in an Array — 음수 마킹으로 `O(n)`/`O(1)` 누락 숫자 찾기
- 35Most Common Word — 금지어를 제외하고 가장 자주 나온 단어 찾기
- 36Group Anagrams — 정렬 키와 문자 개수 키로 애너그램 묶기
합이 k인 쌍을 최대 몇 번 지울 수 있을까, 어떤 문제인가요?
LeetCode 1679번 문제는 이렇게 주어집니다.
정수 배열
nums와 정수k가 주어집니다. 한 번의 연산에서 합이k인 두 수를 골라 배열에서 제거할 수 있습니다. 수행할 수 있는 최대 연산 횟수를 반환하세요.
예시는 이렇습니다.
nums = [1, 2, 3, 4],k = 5→2nums = [3, 1, 3, 4, 3],k = 6→1
제약은 다음과 같습니다.
1 <= nums.length <= 10^51 <= nums[i] <= 10^91 <= k <= 10^9
핵심은 각 수가 만들 수 있는 짝이 오직 k - x 하나뿐이라는 점입니다. 이 글에서는 O(n²) 브루트포스부터, 정렬 + 투 포인터 O(n log n), 그리고 해시 카운팅 O(n)까지 단계적으로 정리하겠습니다. 시간·공간 복잡도 표기가 익숙하지 않다면 시간 복잡도와 공간 복잡도 완전 정복을 먼저 읽어 보시는 것을 권합니다.
Phase 1. 브루트포스 — 아직 쓰지 않은 짝을 뒤에서 찾기
가장 직관적인 접근은 각 원소마다 뒤쪽에서 아직 쓰지 않은 짝을 찾는 것입니다.
코드는 이렇게 됩니다.
fun maxOperations(nums: IntArray, k: Int): Int {
val used = BooleanArray(nums.size)
var answer = 0
for (i in nums.indices) {
if (used[i]) continue
for (j in i + 1 until nums.size) {
if (used[j]) continue
if (nums[i] + nums[j] == k) {
used[i] = true
used[j] = true
answer++
break
}
}
}
return answer
}
nums = [1, 2, 3, 4], k = 5를 따라가 봅시다.
i = 0일 때1의 보완값은4입니다. 뒤를 훑다가j = 3에서 찾습니다1과4를 사용 처리하고answer = 1i = 1일 때2의 보완값은3입니다.j = 2에서 찾습니다2와3을 사용 처리하고answer = 2
정답은 맞지만, 시간 복잡도가 O(n²)입니다. 최악의 경우 거의 모든 (i, j) 쌍을 확인하게 되기 때문입니다. n = 10^5이면 현실적으로 너무 느립니다. 공간도 used 배열 때문에 O(n)이 듭니다.
Phase 2. 정렬 + 투 포인터 — 합이 작으면 왼쪽, 크면 오른쪽 이동
배열을 정렬하면 현재 합이 너무 작은지, 너무 큰지를 바로 이용할 수 있습니다.
left는 가장 작은 값right는 가장 큰 값sum = nums[left] + nums[right]sum == k면 한 쌍 완성sum < k면left++sum > k면right--
코드는 이렇게 됩니다.
fun maxOperations(nums: IntArray, k: Int): Int {
nums.sort()
var left = 0
var right = nums.lastIndex
var answer = 0
while (left < right) {
val sum = nums[left] + nums[right]
when {
sum == k -> {
answer++
left++
right--
}
sum < k -> left++
else -> right--
}
}
return answer
}
nums = [3, 1, 3, 4, 3], k = 6을 정렬하면 [1, 3, 3, 3, 4]입니다.
left=0 (1), right=4 (4)
sum = 5 < 6
→ 더 큰 합이 필요하므로 left++
left=1 (3), right=4 (4)
sum = 7 > 6
→ 더 작은 합이 필요하므로 right--
left=1 (3), right=3 (3)
sum = 6
→ 한 쌍 완성, answer++
→ left++, right--
이제 left == right가 되어 종료됩니다. 답은 1입니다.
왜 sum < k면 left를 올려도 되나요?
정렬된 상태에서 nums[left] + nums[right] < k라면, 현재 left를 그대로 둔 채 right보다 왼쪽의 수와 짝지어도 합은 더 작거나 같습니다.
nums[left] + nums[any index <= right] <= nums[left] + nums[right] < k
즉 현재 nums[left]는 이 구간 안에서 누구와도 k를 만들 수 없습니다. 그래서 left를 버려도 됩니다.
반대로 sum > k라면:
nums[any index >= left] + nums[right] >= nums[left] + nums[right] > k
가 되므로, 현재 nums[right]도 이 구간 안에서 누구와도 k를 만들 수 없습니다. 그래서 right--가 맞습니다.
이 논리 덕분에 매 단계마다 한쪽 끝 원소를 안전하게 버릴 수 있습니다.
참고: Container With Most Water도
투 포인터를 쓰지만, 포인터를 움직이는 근거는 다릅니다. 그 문제는 "더 낮은 높이가 넓이 상한"이어서 낮은 쪽을 움직이고, 이 문제는 "정렬된 합이 너무 작거나 크다"는 비교 결과로 한쪽을 버립니다.
시간 복잡도는 정렬 때문에 O(n log n)입니다. 해시 테이블이 필요 없다는 장점이 있지만, 더 줄일 수는 있습니다.
Phase 3. 해시 카운팅 — 지금 필요한 보완값이 남아 있는지만 보기
각 수 x가 필요한 값은 오직 k - x 하나뿐입니다. 이 관찰을 그대로 쓰면 정렬도 필요 없습니다.
- 지금까지 봤지만 아직 짝을 못 찾은 수의 개수를
counts에 저장합니다 - 새 수
x를 볼 때need = k - x를 계산합니다 counts[need] > 0이면 바로 한 쌍을 만들고need를 하나 소비합니다- 없으면
x를counts에 저장합니다
코드는 이렇게 됩니다.
fun maxOperations(nums: IntArray, k: Int): Int {
val counts = HashMap<Int, Int>()
var answer = 0
for (x in nums) {
val need = k - x
val available = counts[need] ?: 0
if (available > 0) {
if (available == 1) {
counts.remove(need)
} else {
counts[need] = available - 1
}
answer++
} else {
counts[x] = (counts[x] ?: 0) + 1
}
}
return answer
}
같은 예시 nums = [3, 1, 3, 4, 3], k = 6을 따라가 봅시다.
x = 3,need = 3→ 아직 없음 →counts[3] = 1x = 1,need = 5→ 없음 →counts[1] = 1x = 3,need = 3→ 있음 →3과3을 묶고answer = 1x = 4,need = 2→ 없음 →counts[4] = 1x = 3,need = 3→ 없음 →counts[3] = 1
최종 답은 1입니다.
왜 보완값이 보이면 바로 묶어도 되나요?
이 문제에서 x가 만들 수 있는 짝은 오직 k - x입니다. 이미 앞에서 등장한 k - x가 아직 남아 있다면:
- 지금
x와 묶거나 - 나중의 다른
x와 묶거나
결국 소비되는 것은 x 하나와 k - x 하나입니다. 미뤄 둔다고 더 좋은 선택지가 생기지 않습니다. 그래서 보완값이 보이면 바로 묶어도 됩니다.
이 관점으로 보면 답은 결국 값별 개수의 문제입니다.
x != k - x이면x와k - x는 최대min(count[x], count[k - x])쌍x == k - x이면 같은 수 두 개가 필요하므로count[x] / 2쌍
해시 카운팅은 이 계산을 배열을 한 번 훑으면서 수행하는 방식입니다.
시간 복잡도는 평균적으로 O(n), 공간 복잡도는 O(n)입니다.
엣지 케이스
놓치기 쉬운 입력은 이렇습니다.
| 입력 | k |
결과 | 이유 |
|---|---|---|---|
[1, 2, 3, 4] |
5 |
2 |
(1, 4), (2, 3) 두 쌍 |
[3, 1, 3, 4, 3] |
6 |
1 |
3이 세 개라 3 + 3은 한 번만 가능 |
[2, 2, 2, 2] |
4 |
2 |
같은 값끼리 두 개씩 묶음 |
[1, 1, 1, 1] |
3 |
0 |
보완값 2가 아예 없음 |
[5] |
10 |
0 |
원소가 하나면 쌍을 만들 수 없음 |
특히 x == k - x인 경우를 꼭 따로 의식해야 합니다. 예를 들어 k = 4, x = 2라면 2 하나로는 부족하고 항상 두 개가 한 쌍입니다.
세 풀이를 다시 비교
브루트포스는 직관적이고, 정렬 + 투 포인터는 비교 기반으로 줄인 풀이입니다. 최종적으로는 해시 카운팅이 가장 짧고 빠릅니다.
| 풀이 | 시간 | 공간 | 특징 |
|---|---|---|---|
used + 이중 반복 |
O(n²) |
O(n) |
정답은 맞지만 큰 입력에서 너무 느림 |
| 정렬 + 투 포인터 | O(n log n) |
정렬 구현에 따라 다름 | 합이 작으면 왼쪽, 크면 오른쪽을 버리는 구조 |
| 해시 카운팅 | 평균 O(n) |
O(n) |
필요한 보완값 개수만 관리하면 됨 |
마무리
- 이 문제의 핵심은 "한 수가 만들 수 있는 짝은 오직
k - x하나뿐"이라는 점 — 그래서 문제를 보완값 관리 문제로 바꿔 볼 수 있습니다 - 정렬 + 투 포인터는 현재 합이 너무 작거나 크면 한쪽 끝 원소를 바로 버릴 수 있다는 점에서 성립합니다 — 정렬된 순서가 포인터 이동의 근거가 됩니다
- 해시 카운팅은 아직 짝을 못 찾은 수만 남겨 두는 방식입니다 — 보완값이 보이면 바로 소비해도 최대 개수를 놓치지 않습니다
x == k - x인 경우는 항상 같은 수 두 개가 필요합니다 — 그래서 가능한 쌍 수는 결국count[x] / 2입니다
다음으로 읽어볼 글
Group Anagrams — 정렬 키와 문자 개수 키로 애너그램 묶기
애너그램 문자열을 같은 그룹으로 묶는 LeetCode 49번, 정렬된 문자열을 키로 쓰는 풀이부터 문자 개수 배열을 해시 키로 바꾸는 `O(n * k)` 풀이까지 정리합니다.
Most Common Word — 금지어를 제외하고 가장 자주 나온 단어 찾기
문단에서 금지어를 제외하고 가장 많이 등장한 단어를 찾는 LeetCode 819번, 문장 부호 제거와 소문자 정규화, 해시 카운팅 풀이를 정리합니다.
Find All Numbers Disappeared in an Array — 음수 마킹으로 `O(n)`/`O(1)` 누락 숫자 찾기
1부터 n까지의 숫자 중 배열에 나타나지 않은 값을 찾는 LeetCode 448번, 완전 탐색부터 존재 배열, 음수 제자리 마킹 풀이까지 정리합니다.