Merge Sorted Array — 뒤에서부터 채우는 제자리 병합
전체 보기접기
- 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)` 정렬 유지하기
정렬된 두 배열을 nums1 안에서 합치기, 어떤 문제인가요?
LeetCode 88번 문제는 이렇게 주어집니다.
오름차순으로 정렬된 두 정수 배열
nums1,nums2와 정수m,n이 주어집니다.nums1의 처음m개 원소와nums2의n개 원소를 합쳐, 오름차순으로 정렬된 하나의 배열로 만드세요. 결과는 새로 반환하지 않고nums1안에 저장해야 합니다.
여기서 nums1의 길이는 m + n이며:
- 앞의
m개는 실제 값 - 뒤의
n개는0으로 채워진 빈 자리
입니다.
예시는 이렇습니다.
nums1 = [1,2,3,0,0,0], m = 3, nums2 = [2,5,6], n = 3→[1,2,2,3,5,6]nums1 = [1], m = 1, nums2 = [], n = 0→[1]nums1 = [0], m = 0, nums2 = [1], n = 1→[1]
제약은 다음과 같습니다.
nums1.length == m + nnums2.length == n0 <= m, n <= 2001 <= m + n <= 200-10^9 <= nums1[i], nums2[j] <= 10^9
문제의 follow-up은 O(m + n)으로 풀 수 있는가입니다.
핵심은 nums1 뒤쪽에 이미 빈 공간이 준비되어 있다는 점입니다. 이 글에서는 별도 배열을 쓰는 직관적인 풀이부터, 왜 앞에서부터 병합하면 안 되는지, 그리고 뒤에서부터 채우는 제자리 O(m + n) 풀이까지 정리하겠습니다. 시간·공간 복잡도 표기가 익숙하지 않다면 시간 복잡도와 공간 복잡도 완전 정복을 먼저 읽어 보시는 것을 권합니다.
문제를 다시 해석해 보기
첫 번째 예시를 보겠습니다.
nums1 = [1, 2, 3, 0, 0, 0], m = 3
nums2 = [2, 5, 6], n = 3
여기서 실제로 병합해야 하는 대상은:
nums1의 앞 3개: [1, 2, 3]
nums2 전체 : [2, 5, 6]
입니다. nums1의 뒤쪽 0, 0, 0은 값이 아니라 빈 슬롯입니다.
즉 문제는 사실상:
정렬된 두 배열 [1, 2, 3]과 [2, 5, 6]을 병합하되
결과를 nums1의 전체 공간 [_, _, _, _, _, _]에 써라
로 볼 수 있습니다.
Phase 1. 새 배열에 병합한 뒤 nums1에 복사하기
가장 직관적인 방법은 새 배열을 하나 만들고, 일반적인 merge를 수행한 다음 nums1에 복사하는 것입니다.
fun merge(nums1: IntArray, m: Int, nums2: IntArray, n: Int) {
val merged = IntArray(m + n)
var i = 0
var j = 0
var k = 0
while (i < m && j < n) {
if (nums1[i] <= nums2[j]) {
merged[k++] = nums1[i++]
} else {
merged[k++] = nums2[j++]
}
}
while (i < m) merged[k++] = nums1[i++]
while (j < n) merged[k++] = nums2[j++]
for (index in merged.indices) {
nums1[index] = merged[index]
}
}
첫 번째 예시에서는:
[1, 2, 3] 과 [2, 5, 6]을 병합
-> [1, 2, 2, 3, 5, 6]
을 만든 뒤, 이를 nums1에 복사합니다.
이 방식은 이해하기 쉽고 정답도 맞습니다. 시간 복잡도는 O(m + n)입니다. 다만 merged 배열 때문에 추가 공간이 O(m + n) 듭니다. 문제는 결과를 nums1에 직접 저장할 수 있는 구조를 주었으므로, 이 공간을 아낄 수 있습니다.
왜 앞에서부터 직접 병합하면 안 되나요?
nums1 안에 바로 써 보려고 앞에서부터 병합하면, 아직 읽지 않은 값을 덮어쓸 수 있습니다.
예를 들어:
nums1 = [1, 2, 3, 0, 0, 0]
nums2 = [2, 5, 6]
에서 앞에서부터 쓰기 시작하면:
1은 괜찮습니다- 다음 자리에
2를 넣는 순간 - 원래
nums1에 있던 뒤쪽 값들이 섞이기 시작합니다
더 작은 예시로 보면 더 분명합니다.
nums1 = [4, 5, 6, 0, 0, 0]
nums2 = [1, 2, 3]
정답의 맨 앞은 1이어야 합니다. 그런데 nums1[0] = 1을 쓰는 순간, 원래 nums1[0]에 있던 4를 잃어버립니다. 아직 그 값은 뒤쪽 어디에 들어가야 할지 처리도 못 했는데 말입니다.
즉 앞에서부터 쓰면:
- 읽기 대상과 쓰기 대상이 충돌하고
- 아직 처리하지 않은 원래 값을 덮어쓸 위험이 있습니다
이 문제를 피하려면 아직 아무 값도 없는 뒤쪽 빈 공간부터 채워야 합니다.
Phase 2. 뒤에서부터 채우기
가장 큰 값은 두 배열의 맨 끝 중 하나에 있습니다. 따라서 결과 배열의 맨 뒤부터 채우면 덮어쓰기 문제 없이 병합할 수 있습니다.
포인터는 이렇게 둡니다.
i = m - 1:nums1의 실제 마지막 원소j = n - 1:nums2의 마지막 원소write = m + n - 1:nums1의 마지막 칸
fun merge(nums1: IntArray, m: Int, nums2: IntArray, n: Int) {
var i = m - 1
var j = n - 1
var write = m + n - 1
while (j >= 0) {
if (i >= 0 && nums1[i] > nums2[j]) {
nums1[write] = nums1[i]
i--
} else {
nums1[write] = nums2[j]
j--
}
write--
}
}
첫 번째 예시를 따라가 봅시다.
nums1 = [1, 2, 3, 0, 0, 0]
nums2 = [2, 5, 6]
i = 2 (3), j = 2 (6), write = 5
3 vs 6 -> 6이 더 큼 -> nums1[5] = 6
i = 2 (3), j = 1 (5), write = 4
3 vs 5 -> 5가 더 큼 -> nums1[4] = 5
i = 2 (3), j = 0 (2), write = 3
3 vs 2 -> 3이 더 큼 -> nums1[3] = 3
i = 1 (2), j = 0 (2), write = 2
2 vs 2 -> nums2 쪽을 써도 됨 -> nums1[2] = 2
j < 0가 되면 종료
결과는:
[1, 2, 2, 3, 5, 6]
입니다.
시간 복잡도는 O(m + n)이고, 추가 공간은 포인터 몇 개뿐이므로 O(1)입니다.
왜 while (j >= 0)만 보면 되나요?
이 구현에서 핵심은 nums2의 값이 모두 들어가기만 하면 끝난다는 점입니다.
두 경우를 나눠 보면:
-
nums1값이 먼저 다 소진되는 경우 이때는 남은nums2값을 계속 복사해야 하므로 반복이 더 필요합니다. -
nums2값이 먼저 다 소진되는 경우 이때nums1의 남은 값들은 이미 제자리에 있습니다.
예를 들어:
nums1 = [1, 2, 3, 0, 0, 0]
nums2 = [4, 5, 6]
이면 뒤에서 6, 5, 4를 채운 뒤 종료하면 되고, 앞쪽 [1, 2, 3]은 손댈 필요가 없습니다.
그래서 반복 조건을:
while (j >= 0)
만 두면 충분합니다. nums1 쪽 잔여 처리를 따로 쓸 필요가 없습니다.
같은 값이면 어느 쪽을 먼저 써도 되나요?
됩니다. 이 문제는 최종적으로 정렬만 맞으면 되기 때문입니다.
예를 들어 nums1[i] == nums2[j]라면:
nums1쪽 값을 먼저 써도 되고nums2쪽 값을 먼저 써도 됩니다
현재 구현은:
if (i >= 0 && nums1[i] > nums2[j]) { ... } else { ... }
이므로 같을 때는 nums2[j]를 씁니다. 정렬 결과에는 문제가 없습니다.
엣지 케이스
자주 확인해 볼 만한 입력은 이렇습니다.
| 입력 | 결과 | 이유 |
|---|---|---|
nums1=[1], m=1, nums2=[], n=0 |
[1] |
두 번째 배열이 비어 있음 |
nums1=[0], m=0, nums2=[1], n=1 |
[1] |
첫 번째 배열의 실제 값이 없음 |
nums1=[2,0], m=1, nums2=[1], n=1 |
[1,2] |
더 작은 값이 nums2에 있음 |
nums1=[1,2,3,0,0,0], m=3, nums2=[2,5,6], n=3 |
[1,2,2,3,5,6] |
기본 예시 |
nums1=[4,5,6,0,0,0], m=3, nums2=[1,2,3], n=3 |
[1,2,3,4,5,6] |
앞에서부터 쓰면 위험한 대표 예시 |
특히 m = 0인 경우가 중요합니다. 이때는 nums1의 앞쪽 값은 전부 무시해야 하고, 사실상 nums2를 그대로 복사하는 문제가 됩니다.
두 풀이를 다시 비교
| 풀이 | 시간 | 공간 | 특징 |
|---|---|---|---|
| 새 배열에 병합 후 복사 | O(m + n) |
O(m + n) |
가장 직관적이지만 추가 공간이 필요 |
| 뒤에서부터 제자리 병합 | O(m + n) |
O(1) |
nums1의 빈 공간을 활용하는 최종 풀이 |
마무리
- 이 문제는 정렬된 두 배열의 병합 문제이지만, 결과를
nums1안에 저장해야 합니다 — 뒤쪽0들은 값이 아니라 빈 공간입니다 - 앞에서부터 직접 쓰면 아직 읽지 않은
nums1값을 덮어쓸 수 있습니다 — 그래서 정면으로 병합하면 안 됩니다 - 가장 큰 값은 항상 두 배열의 끝 중 하나에 있습니다 — 이 점 때문에 뒤에서부터 채우는 전략이 성립합니다
while (j >= 0)만으로 충분합니다 —nums2가 소진되면nums1의 남은 값은 이미 제자리에 있기 때문입니다- 최종 제자리 풀이는
O(m + n)시간,O(1)공간입니다 — 이 문제의 핵심은 빈 공간을 단순 저장소가 아니라 병합 버퍼로 보는 관점입니다
Find Numbers with Even Number of Digits — 자릿수 개수의 짝수 여부 세기
다음 편Duplicate Zeros — 고정 길이 배열을 오른쪽에서부터 제자리 갱신하기
다음으로 읽어볼 글
Squares of a Sorted Array — 절댓값 기준 투 포인터로 `O(n)` 정렬 유지하기
정렬된 배열의 각 원소를 제곱한 뒤 다시 정렬된 배열로 만드는 LeetCode 977번, 제곱 후 정렬 `O(n log n)` 풀이부터 양끝 절댓값을 비교하는 투 포인터 `O(n)` 풀이까지 정리합니다.
Duplicate Zeros — 고정 길이 배열을 오른쪽에서부터 제자리 갱신하기
고정 길이 배열에서 `0`을 복제하며 나머지를 오른쪽으로 미는 LeetCode 1089번, 별도 배열 `O(n)` 풀이부터 오른쪽에서부터 덮어쓰는 제자리 `O(n)`/`O(1)` 풀이까지 정리합니다.
Find Numbers with Even Number of Digits — 자릿수 개수의 짝수 여부 세기
배열에서 자릿수가 짝수인 수가 몇 개인지 구하는 LeetCode 1295번, 문자열 길이 풀이부터 나눗셈으로 자릿수 세기, 범위 비교 `O(n)` 풀이까지 정리합니다.