KMP
KMP
KMP는 문자열에서 패턴을 효율적으로 찾는 알고리즘이다.
한 줄로 요약하면 다음과 같다.
실패했을 때도 이미 맞춘 접두사 정보를 재사용해서
비교를 다시 하지 않는 문자열 탐색 알고리즘
즉 KMP의 핵심은:
다시 처음부터 비교하지 않는다
는 점이다.
1. 언제 쓰는가
아래 상황이면 KMP를 떠올릴 수 있다.
- 문자열에서 패턴 찾기
- 부분 문자열 등장 여부
- 패턴 등장 횟수
- 접두사 / 접미사 정보가 중요함
- 같은 문자열 비교를 반복하면 느림
대표 문제:
- 문자열 내 패턴 출현 위치 찾기
- 반복 패턴 분석
- 광고, 문자열 제곱, 주기 문제 일부
2. 왜 일반 탐색은 비효율적인가
패턴을 찾다가 중간에 틀리면, 단순 탐색은 시작점을 한 칸 옮기고 다시 비교한다.
즉 이미 맞춘 문자들도 또 비교하게 된다.
예:
- text =
ababababca - pattern =
abababca
이런 경우 앞부분이 많이 겹치므로, 단순 탐색은 중복 비교가 많아진다.
3. 핵심 아이디어
KMP는 패턴 내부의 접두사 / 접미사 정보를 이용해 점프한다.
즉 현재까지 맞춘 문자 중 일부는, 다음 비교에서도 그대로 활용할 수 있다.
flowchart TD
A["문자 비교"] --> B{"불일치 발생"}
B -->|No| A
B -->|Yes| C["pi 테이블 사용"]
C --> D["패턴 인덱스 점프"]
D --> A
즉 KMP는 불일치가 나도 text 인덱스를 크게 되돌리지 않고, pattern 쪽 포인터만 효율적으로 이동시킨다.
4. pi 배열이란 무엇인가
pi[i]는 패턴의 0..i 구간에서,
접두사이면서 접미사인 것의 최대 길이다.
말이 어려우면 이렇게 보면 된다.
앞부분과 뒷부분이 얼마나 겹치나?
예:
pattern = ababa
pi = 0 0 1 2 3
왜냐하면:
a-> 겹침 0ab-> 겹침 0aba->a겹침 -> 1abab->ab겹침 -> 2ababa->aba겹침 -> 3
이 감각이 가장 중요하다.
5. pi 배열을 왜 쓰는가
패턴을 맞추다가 j 위치에서 실패했다고 하자.
그때 패턴의 앞부분 중 일부는 이미 text와 맞았다는 사실을 알고 있다.
따라서 완전히 처음으로 돌아가지 않고,
j = pi[j - 1]
로 점프할 수 있다.
즉 이미 맞춘 접두사 정보를 재사용하는 것이다.
6. pi 배열 만들기
int[] getPi(String p) {
int n = p.length();
int[] pi = new int[n];
int j = 0;
for (int i = 1; i < n; i++) {
while (j > 0 && p.charAt(i) != p.charAt(j)) {
j = pi[j - 1];
}
if (p.charAt(i) == p.charAt(j)) {
pi[i] = ++j;
}
}
return pi;
}
해석
i: 새로 확인하는 위치j: 현재까지 맞춘 접두사 길이- mismatch가 나면
j = pi[j - 1]로 점프 - match가 나면 접두사 길이를 1 늘린다
7. pi 배열 손으로 만들어 보기
패턴:
ababaca
인덱스별로 보면:
a-> 0ab-> 0aba->a겹침 -> 1abab->ab겹침 -> 2ababa->aba겹침 -> 3ababac-> 겹침 없음 -> 0으로 떨어짐ababaca->a겹침 -> 1
즉:
pi = [0, 0, 1, 2, 3, 0, 1]
인덱스: 0 1 2 3 4 5 6
문자: a b a b a c a
pi: 0 0 1 2 3 0 1
│ ↗ │
└──────── 접두사 a와 접미사 a가 겹침 ────────┘
"abab" → 접두사 "ab" = 접미사 "ab" → pi[3] = 2
"ababa" → 접두사 "aba" = 접미사 "aba" → pi[4] = 3
8. KMP 탐색
List<Integer> kmp(String text, String pattern) {
int[] pi = getPi(pattern);
List<Integer> result = new ArrayList<>();
int j = 0;
for (int i = 0; i < text.length(); i++) {
while (j > 0 && text.charAt(i) != pattern.charAt(j)) {
j = pi[j - 1];
}
if (text.charAt(i) == pattern.charAt(j)) {
if (j == pattern.length() - 1) {
result.add(i - pattern.length() + 2); // 1-based index
j = pi[j];
} else {
j++;
}
}
}
return result;
}
이 코드를 읽을 때 핵심은 i와 j의 역할을 분리해서 보는 것이다.
i: text를 한 번만 왼쪽에서 오른쪽으로 훑는 포인터j: pattern에서 현재 몇 글자까지 맞았는지 나타내는 포인터
즉 KMP는 text를 다시 읽는 것이 아니라, pattern 쪽 정렬만 다시 맞춘다고 생각하면 이해가 쉬워진다.
KMP 매칭 과정 예시
text: a b a b a c d a b a b a c a
pattern: a b a b a c a
↑ ↑ ↑ ↑ ↑ ✓ j=5까지 매치, j=6에서 mismatch (d ≠ a)
→ j = pi[5-1] = pi[4] = 3 (aba 접두사가 겹침)
→ pattern을 2칸 밀어서 다시 비교:
text: a b a b a c d a b a b a c a
a b a b a c a
↑ 여기(j=3)부터 이어서 비교
이미 매치된 "aba"를 다시 비교하지 않는다
이것이 KMP가 O(N + M)인 이유다. text의 i는 절대 뒤로 가지 않는다.
9. 탐색을 어떻게 이해하면 좋은가
i는 text를 한 번만 왼쪽에서 오른쪽으로 본다.
j는 pattern에서 현재까지 맞춘 길이를 뜻한다.
- match면
j++ - mismatch면
j = pi[j - 1] - 완전 매칭이면 정답 기록 후
j = pi[j]
즉 text 인덱스를 되돌리지 않고, pattern 쪽만 효율적으로 재배치한다.
이 점 때문에 KMP는 단순 탐색보다 훨씬 빠르다. 앞에서 힘들게 맞춘 정보를 버리지 않기 때문이다.
mismatch가 났을 때 j가 어떻게 줄어드나
예를 들어 패턴이 ababaca이고,
현재 j = 5까지 맞춘 상태에서 다음 문자가 틀렸다고 하자.
KMP는 다시 처음으로 가지 않고
pi를 따라 “지금까지 맞춘 접두사 중 다음 후보”로 바로 이동한다.
flowchart LR
A["j = 5"] --> B["pi[4] = 3"]
B --> C{"여전히 불일치?"}
C -->|Yes| D["pi[2] = 1"]
D --> E{"여전히 불일치?"}
E -->|Yes| F["0"]
핵심은 text를 다시 읽는 것이 아니라, 패턴 내부의 겹침만 따라 내려간다는 점이다.
그래서 i는 뒤로 가지 않고도
다음 유효한 정렬 위치를 곧바로 찾을 수 있다.
10. 매칭 완료 후 왜 j = pi[j]인가
패턴 하나를 찾았다고 끝이 아닐 수 있다. 겹치는 다음 패턴도 찾아야 할 수 있다.
예:
- text =
aaaaa - pattern =
aaa
정답은 시작 위치가 1, 2, 3이다.
이런 겹침 매칭을 놓치지 않으려면,
매칭 완료 후에도 접두사 정보를 이용해 j를 옮겨야 한다.
즉 j = pi[j]는 단순한 코드 한 줄이 아니라,
“다음 후보 시작점을 접두사 정보로 바로 맞춘다”는 뜻이다.
11. 시간 복잡도
- pi 배열 생성:
O(M) - 탐색:
O(N)
즉 전체:
O(N + M)
이다.
KMP가 강력한 이유는 중복 비교를 제거해 선형 시간에 동작한다는 점이다.
12. 자주 하는 실수
1) mismatch 시 j = pi[j - 1]를 안 함
KMP의 핵심을 놓치는 것이다.
2) 패턴 매칭 완료 후 j = pi[j]를 빼먹음
겹치는 패턴을 놓칠 수 있다.
3) 인덱스 0-based / 1-based 혼동
문제 출력 형식에 따라 시작 위치 계산을 주의해야 한다.
4) pi 배열 의미를 외우기만 하고 이해하지 못함
접두사 / 접미사 겹침이라는 감각이 있어야 응용이 된다.
13. 시험장용 최소 암기 버전
KMP:
문자열 패턴 탐색 O(N + M)
핵심:
pi 배열
불일치 시 j = pi[j - 1]
pi 의미:
접두사이면서 접미사인 최대 길이
14. 최종 요약
KMP는 다음 문장으로 정리할 수 있다.
패턴의 접두사 / 접미사 정보를 이용해
중복 비교를 피하는 선형 문자열 탐색 알고리즘
문제를 보면 먼저 이 질문을 하면 된다.
문자열 비교 실패 후,
이미 맞춘 정보 중 재사용할 수 있는 부분이 있는가?
이 감각이 KMP의 핵심이다.
Comments
Sign in with GitHub to join the discussion.