Move Zeroes — 쓰기 포인터 하나로 `O(n)`/`O(1)` 제자리 재배치
전체 보기접기
- 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 — 정렬 키와 문자 개수 키로 애너그램 묶기
0을 뒤로 밀기, 어떤 문제인가요?
LeetCode 283번 문제는 이렇게 주어집니다.
정수 배열
nums가 주어졌을 때, 모든0을 배열의 끝으로 이동하세요. 0이 아닌 원소의 상대적 순서는 유지해야 하며, 배열을 제자리(in-place) 에서 수정해야 합니다.
예시는 이렇습니다.
nums = [0, 1, 0, 3, 12]→[1, 3, 12, 0, 0]nums = [0]→[0]
제약은 다음과 같습니다.
1 <= nums.length <= 10^4-2^31 <= nums[i] <= 2^31 - 1
"0을 뒤로 민다"는 말을 뒤집으면 **"0이 아닌 원소를 앞으로 모은다"**는 뜻입니다. 이 글에서는 별도 배열을 쓰는 방식부터, 쓰기 포인터 하나로 제자리 스왑하는 방식까지 비교합니다. 시간·공간 복잡도 표기가 익숙하지 않다면 시간 복잡도와 공간 복잡도 완전 정복을 먼저 읽어 보시는 것을 권합니다.
Phase 1. 별도 배열에 모아서 복사
가장 직관적인 접근은 0이 아닌 원소만 새 배열에 순서대로 모은 뒤, 나머지를 0으로 채우는 것입니다.
fun moveZeroes(nums: IntArray) {
val result = IntArray(nums.size)
var idx = 0
for (num in nums) {
if (num != 0) result[idx++] = num
}
for (i in nums.indices) {
nums[i] = result[i]
}
}
nums = [0, 1, 0, 3, 12]를 따라가 봅시다.
- 0이 아닌 원소 수집:
result = [1, 3, 12, 0, 0] - 원본에 복사:
nums = [1, 3, 12, 0, 0]
정답은 맞지만, result 배열에 O(n) 추가 공간이 필요합니다. 문제가 요구하는 제자리(in-place) 조건을 만족하지 못합니다.
Phase 2. 쓰기 포인터 스왑 — 제자리 한 번 훑기
별도 배열 없이 하려면, 같은 배열 위에서 "다음에 0이 아닌 값을 쓸 위치"를 가리키는 write 포인터를 하나 둡니다. 0이 아닌 원소를 만날 때마다 write 위치와 스왑하면, 0이 자연스럽게 뒤쪽으로 밀립니다.
fun moveZeroes(nums: IntArray) {
var write = 0
for (read in nums.indices) {
if (nums[read] != 0) {
val temp = nums[write]
nums[write] = nums[read]
nums[read] = temp
write++
}
}
}
nums = [0, 1, 0, 3, 12]를 따라가 봅시다.
초기: [0, 1, 0, 3, 12]
W R
read=0: nums[0]=0 → 건너뜀
read=1: nums[1]=1 ≠ 0 → nums[0]↔nums[1]
[1, 0, 0, 3, 12] write=1
W R
read=2: nums[2]=0 → 건너뜀
read=3: nums[3]=3 ≠ 0 → nums[1]↔nums[3]
[1, 3, 0, 0, 12] write=2
W R
read=4: nums[4]=12 ≠ 0 → nums[2]↔nums[4]
[1, 3, 12, 0, 0] write=3
결과: [1, 3, 12, 0, 0] ✓
시간 O(n), 공간 O(1)입니다.
왜 0이 아닌 원소의 순서가 유지되나요?
write 포인터는 0이 아닌 원소를 만날 때만 전진합니다. 배열을 왼쪽부터 읽으면서, 0이 아닌 원소를 만나는 순서 그대로 write 위치에 기록하므로 상대적 순서가 보존됩니다.
write와 read가 같은 위치일 때는?
배열 앞부분에 0이 없으면 write와 read가 함께 움직입니다. 예를 들어 [1, 2, 0, 3]에서:
read = 0:nums[0] = 1 ≠ 0→nums[0]↔nums[0](자기 자신과 스왑),write = 1read = 1:nums[1] = 2 ≠ 0→nums[1]↔nums[1],write = 2
자기 자신과의 스왑은 결과에 영향을 주지 않습니다. 불필요한 쓰기를 줄이고 싶다면 if (read != write) 조건을 추가할 수 있습니다.
fun moveZeroes(nums: IntArray) {
var write = 0
for (read in nums.indices) {
if (nums[read] != 0) {
if (read != write) {
nums[write] = nums[read]
nums[read] = 0
}
write++
}
}
}
이 버전은 read == write일 때 아무 쓰기도 하지 않으므로, 0이 적은 배열에서 불필요한 메모리 쓰기를 줄여 줍니다.
참고: 이 쓰기 포인터 패턴은 String Compression의 읽기/쓰기 포인터와 같은 원리입니다.
read는 원본을 훑고,write는 결과를 기록하며, 결과가 원본보다 짧거나 같을 때 같은 배열 위에서 안전하게 동작합니다.
두 풀이를 다시 비교
| 풀이 | 시간 | 공간 | 특징 |
|---|---|---|---|
| 별도 배열 복사 | O(n) |
O(n) |
직관적이지만 제자리 조건 위반 |
| 쓰기 포인터 스왑 | O(n) |
O(1) |
한 번 훑기로 0을 뒤로 밀면서 순서 유지 |
마무리
- "0을 뒤로 민다"는 "0이 아닌 원소를 앞으로 모은다"로 바꿔 생각하면 쓰기 포인터가 자연스럽게 나온다 — 비교 대상이
0이라는 것만 다를 뿐, "조건을 만족하는 원소를 앞으로 압축"하는 일반 패턴입니다 write는 "다음에 값을 쓸 위치"를 추적하고,read는 원본을 순서대로 훑는다 — 이 역할 분리가 상대적 순서 보존을 보장합니다read != write일 때만 쓰기를 수행하면 불필요한 메모리 접근을 줄일 수 있다 — 0이 적은 입력에서 효과가 큽니다
Increasing Triplet Subsequence — `O(n)` 시간, `O(1)` 공간으로 푸는 그리디 패턴
다음 편String Compression — 읽기/쓰기 포인터로 `O(n)`/`O(1)` 제자리 압축
다음으로 읽어볼 글
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번, 완전 탐색부터 존재 배열, 음수 제자리 마킹 풀이까지 정리합니다.