정렬 후 인접 비교
정렬을 한 뒤 인접한 두 원소만 비교해서, 모든 쌍을 검사하지 않고도 특정 관계를 찾아내는 알고리즘 패턴입니다.
왜 인접만 봐도 되는가?
정렬의 핵심 성질:
"관계가 있는 두 원소"가 정렬 후 반드시 이웃이 된다.
→ 그 관계를 찾기 위해 모든 쌍을 비교(O(n²))할 필요 없이
인접한 쌍만 비교(O(n))하면 충분.
전체 비용은 정렬(O(n log n)) + 인접 비교(O(n)) = O(n log n).
활용 가능한 관계들
| 찾고 싶은 것 | 정렬 기준 | 인접 비교 조건 |
|---|---|---|
| 중복 원소 | 값 오름차순 | arr[i] == arr[i+1] |
| 가장 가까운 두 수 | 값 오름차순 | min(arr[i+1] - arr[i]) |
| 문자열 접두사 | 사전식 | arr[i+1].startsWith(arr[i]) |
| 겹치는 구간 | 시작점 오름차순 | prev.end > next.start |
| 연속된 정수 | 값 오름차순 | arr[i+1] - arr[i] == 1 |
패턴 1: 중복 검사
int[] arr = {3, 1, 2, 3, 4};
Arrays.sort(arr);
// [1, 2, 3, 3, 4]
for (int i = 1; i < arr.length; i++) {
if (arr[i] == arr[i-1]) {
return true; // 중복 발견
}
}
return false;
[ 단계별 추적 ]
[1, 2, 3, 3, 4]
↑ ↑ i=1: 1 != 2
↑ ↑ i=2: 2 != 3
↑ ↑ i=3: 3 == 3 → 중복!
해시셋(O(n))보다 약간 느리지만 공간 O(1) 인-플레이스.
패턴 2: 가장 가까운 두 수
int[] arr = {30, 10, 50, 11, 100};
Arrays.sort(arr);
// [10, 11, 30, 50, 100]
int minDiff = Integer.MAX_VALUE;
for (int i = 1; i < arr.length; i++) {
minDiff = Math.min(minDiff, arr[i] - arr[i-1]);
}
// minDiff = 1 ← (10, 11)
[ 단계별 추적 ]
[10, 11, 30, 50, 100]
↑ ↑ diff = 1 ← 최소
↑ ↑ diff = 19
↑ ↑ diff = 20
↑ ↑ diff = 50
→ 모든 쌍 O(n²) 비교 안 해도 정답
왜 인접만 봐도 충분한가? 정렬된 배열에서 어떤 두 수의 차이는 그 사이에 있는 인접 차이들의 합 이상. 따라서 최소 차이는 반드시 인접한 쌍에서 나옴.
패턴 3: 문자열 접두사 검사 (전화번호 목록)
String[] phoneBook = {"119", "97674223", "1195524421"};
Arrays.sort(phoneBook);
// ["119", "1195524421", "97674223"]
for (int i = 0; i < phoneBook.length - 1; i++) {
if (phoneBook[i+1].startsWith(phoneBook[i])) {
return false;
}
}
return true;
[ 단계별 추적 ]
정렬 후: ["119", "1195524421", "97674223"]
↑ ↑
└───────┴─ "1195524421".startsWith("119") → true → return false
왜 인접만 봐도 충분한가? 사전식 정렬에서, 어떤 문자열 A가 B의 접두사라면 A 바로 뒤에 B가 위치한다. 그 사이에 끼어드는 문자열 C가 있을 수 없음(C도 A를 접두사로 가져야 하므로).
직관:
A = "119"
B = "1195524421"
만약 정렬에서 A와 B 사이에 C가 끼어든다면?
A < C < B 이려면 C도 "119..."로 시작해야 함.
그렇지 않으면 C는 A보다 작거나(불가능) B보다 큼(불가능).
→ A와 B는 정렬 후 항상 인접
대안으로 HashMap 풀이도 가능:
- 모든 번호를 HashMap에 저장
- 각 번호의 모든 접두사를 만들어가며 다른 번호인지 확인
- 시간 O(N·L²), 공간 O(N)
- 정렬 풀이는 시간 O(N·L log N), 공간 O(1)
패턴 4: 구간 겹침 검사
int[][] intervals = {{1, 3}, {2, 6}, {8, 10}, {15, 18}};
// 시작점 기준 정렬
Arrays.sort(intervals, (a, b) -> a[0] - b[0]);
// 이미 정렬됨: [[1,3], [2,6], [8,10], [15,18]]
for (int i = 1; i < intervals.length; i++) {
if (intervals[i-1][1] > intervals[i][0]) {
// 겹침 발견 (이전 끝 > 다음 시작)
}
}
[ 단계별 추적 ]
[1, 3] [2, 6] [8, 10] [15, 18]
끝=3 시작=2 ← 3 > 2 → 겹침!
끝=6 시작=8 ← 6 < 8
끝=10 시작=15 ← 10 < 15
→ (1,3)과 (2,6) 겹침
이 패턴의 확장으로 회의실 배정 / 작업 스케줄링 / 구간 병합 같은 그리디 문제가 풀린다.
시간복잡도 비교
| 접근법 | 시간 | 공간 |
|---|---|---|
| 모든 쌍 비교 (naive) | O(n²) | O(1) |
| 해시 기반 | O(n) | O(n) |
| 정렬 후 인접 비교 | O(n log n) | O(1) |
해시가 가장 빠르지만, 인-플레이스가 필요하거나 입력 자체가 정렬된 상태로 들어올 때 정렬 후 인접 비교가 빛난다.
핵심 정리
이 패턴은 "관계 있는 원소가 정렬 후 이웃이 된다" 는 정렬의 성질을 활용해서, O(n²) 비교를 O(n log n)으로 줄이는 기법입니다. 중복 검사, 가장 가까운 두 수, 문자열 접두사, 구간 겹침 등 다양한 문제에 적용되며, 해시보다 느리지만 공간 O(1) 이고 코드가 간결한 게 장점입니다. 적용 가능 여부를 판단하려면 "정렬했을 때 답이 인접한 두 원소 사이에서 발견될 수 있는가?"를 먼저 검증해야 합니다.