[Algorithm] KMP알고리즘(Knuth Morris Pratt) 핵심 정리로 (C++ 코드 예제 2개 포함)
KMP알고리즘(Knuth Morris Pratt) 개념?
KMP(Knuth Morris Pratt) 알고리즘은 문자열 검색 알고리즘으로, 주어진 텍스트에서 특정 패턴이 존재하는 위치를 효율적으로 찾는 데 사용됩니다. 기본적인 브루트포스(Brute Force)방식과 달리, 불필요한 비교를 줄여 더 빠르게 동작하는게 특징입니다.
KMP 알고리즘은 두 가지 주요 과정으로 이루어집니다.
- 전처리 과정 (Partial Match Table 또는 LPS 배열 생성)
- 패턴 내에서 자기 자신을 포함하는 가장 긴 접두사이자 접미사를 찾는 테이블을 만듭니다.
- 이를 활용하면 불일치가 발생했을 때, 패턴을 처음부터 다시 검사할 필요 없이 적절한 위치에서 다시 비교를 시작할 수 있습니다.
- 검색 과정 (텍스트와 패턴 비교)
- 전처리한 LPS(Longest Prefix Suffix) 배열을 이용하여 불필요한 비교 없이 빠르게 패턴을 찾습니다.
(출처 : https://www.geeksforgeeks.org/kmp-algorithm-for-pattern-searching/)
LPS(Longest Prefix Suffix) 배열
LPS 배열은 패턴의 각 인덱스까지에서, 접두사(prefix)와 접미사(suffix)가 일치하는 최대 길이를 저장하는 배열로 KMP 알고리즘에서 불필요한 비교를 줄이기 위해 활용됩니다.
"ABA"
까지의 접두사와 접미사가 일치하는 최대 길이는 1 ("A"
)"ABAB"
의 접두사와 접미사가 일치하는 최대 길이는 2 ("AB"
)"ABABA"
의 접두사와 접미사가 일치하는 최대 길이는 3 ("ABA"
) 이 배열을 이용하면, 텍스트에서 불일치가 발생했을 때 패턴을 LPS 값을 참고해 점프할 수 있습니다.
LPS 배열의 개념
📌LPS 배열의 각 인덱스 i에 대해:
- LPS[i] = X라면, 패턴의 처음 X개 문자(pattern[0:X-1])가 접두사이면서 접미사임을 의미합니다
- 즉, 패턴의 일부를 일치시킨 후, 실패했을 때 어디로 이동해야 하는지를 나타냅니다.
❗ 접두사(Prefix)와 접미사(Suffix)의 개념
- 접두사(Prefix): 문자열의 시작부터 일부까지 (“A”, “AB”, “ABA” 등)
- 접미사(Suffix): 문자열의 끝에서 일부까지 (“A”, “BA”, “ABA” 등)
- 접두사와 접미사는 전체 문자열과 같아서는 안됩니다.
LPS 배열 예제
📌 LPS 배열 계산 과정
- “A”: 접두사, 접미사 없음 → LPS[0] = 0
- “AB”: 접두사 “A”, 접미사 “B” (일치하는 부분 없음) → LPS[1] = 0
- “ABA”: 접두사 “A”, “AB”, 접미사 “BA”, “A” → “A” 일치 → LPS[2] = 1
- “ABAB”: 접두사 “A”, “AB”, “ABA”, 접미사 “BAB”, “AB”, “B” → “AB” 일치 → LPS[3] = 2
- “ABABA”: 접두사 “A”, “AB”, “ABA”, “ABAB”, 접미사 “BABA”, “ABA”, “BA”, “A” → “ABA” 일치 → LPS[4] = 3
- “ABABAC”: 접두사 “A”, “AB”, “ABA”, “ABAB”, “ABABA”, 접미사 “BABAC”, “ABAC”, “BAC”,”AC”, “C” (일치하는 부분 없음) → LPS[5] = 0
➡ 최종 LPS 배열: [0, 0, 1, 2, 3, 0]
LPS 배열을 활용한 KMP 알고리즘 검색 방식
텍스트 "ABABDABACDABABCABAB"
에서 "ABABCABAB"
을 찾을 때, 비교 도중 불일치가 발생하면:
- LPS 배열을 참고해 패턴을 적절히 이동 → 처음부터 다시 비교하지 않습니다
예를 들어,
- “ABABCABAB”의 6번째 문자 “C”에서 불일치 발생
- “C”의 LPS 값이 0이므로 패턴을 완전히 새로 비교
- 이후 “ABAB”에서 “B”와 불일치 발생 시, “B”의 LPS 값이 2 → 앞부분 2칸을 건너뛰고 다시 비교
LPS 배열 생성 코드
#include <iostream>
#include <vector>
using namespace std;
vector<int> computeLPS(string pattern) {
int M = pattern.size();
vector<int> lps(M, 0);
int len = 0; // 현재까지 일치한 접두사의 길이
int i = 1; // 0번 인덱스는 항상 0이므로 1부터 시작
while (i < M) {
if (pattern[i] == pattern[len]) { // 접두사와 접미사가 같다면
len++;
lps[i] = len;
i++;
} else { // 불일치 발생
if (len != 0) {
len = lps[len - 1]; // 이전 LPS 값을 참고하여 이동
} else {
lps[i] = 0;
i++;
}
}
}
return lps;
}
int main() {
string pattern = "ABABAC";
vector<int> lps = computeLPS(pattern);
cout << "LPS 배열: ";
for (int val : lps) {
cout << val << " ";
}
return 0;
}
KMP알고리즘의 동작 과정
- 텍스트 T와 패턴 P를 비교하면서 진행한다.
- 불일치가 발생하면 LPS 값을 참고하여 점프한다.
- 패턴이 완전히 일치하면 시작 위치를 저장하고 계속 탐색한다.
장단점
장점
- 빠른 검색 속도
- 일반적인 브루트포스(Brute Force) 알고리즘이 O(N × M) 인 것과 비교하면 O(N + M) 으로 훨씬 빠릅니다.
- 특히 긴 텍스트에서 반복적인 패턴 검색이 필요한 경우 매우 유리합니다.
- 불필요한 비교 최소화
- 이전 비교 결과를 활용하여 패턴을 이동하기 때문에 중복된 비교를 하지 않습니다.
- 예를 들어, “AAAAA” 같은 반복적인 패턴을 탐색할 때 성능이 극적으로 향상됩니다.
- 텍스트 길이에 비례하는 성능
- 패턴이 길어지더라도, 한 번 계산한 LPS 배열을 사용하므로 텍스트의 길이(N)에 비례하는 성능을 보장합니다.
단점
- LPS 배열을 만드는 전처리 과정이 필요합니다
- 일반적인 문자열 검색보다 구현이 복잡하고, 추가적인 O(M)의 전처리 비용이 발생합니다.
- 짧은 패턴을 한두 번만 찾는 경우, 간단한 브루트포스 방식이 더 유리할 수도 있습니다.
- 패턴이 너무 다양하면 효율성이 떨어질 수 있습니다.
- LPS 배열은 패턴의 자기 반복성(repetitive structure)을 활용하는 방식이므로, 패턴이 지나치게 불규칙한 경우(예: “XYZABC”), 점프 효과가 크지 않을 수 있습니다.
- 메모리 사용량 증가
- LPS 배열을 저장해야 하므로 O(M) 만큼의 추가 공간이 필요합니다. 다만,
- 일반적으로 큰 문제는 아니지만, 메모리 사용이 중요한 환경에서는 고려해야 합니다.
시간 복잡도
- 전처리 (LPS 배열 생성): O(M)
- 검색 과정: O(N)
- 총 시간 복잡도: O(N + M)
로, 브루트포스 O(N × M) 방법보다 훨씬 빠릅니다.
예제
#include <iostream>
#include <vector>
using namespace std;
// LPS 배열을 생성하는 함수
vector<int> computeLPS(string pattern) {
int M = pattern.size();
vector<int> lps(M, 0);
int len = 0;
int i = 1;
while (i < M) {
if (pattern[i] == pattern[len]) {
len++;
lps[i] = len;
i++;
} else {
if (len != 0) {
len = lps[len - 1]; // 이전 LPS 값을 참고하여 이동
} else {
lps[i] = 0;
i++;
}
}
}
return lps;
}
// KMP 검색 함수
void KMPSearch(string text, string pattern) {
int N = text.size();
int M = pattern.size();
vector<int> lps = computeLPS(pattern);
int i = 0, j = 0; // i: 텍스트 인덱스, j: 패턴 인덱스
while (i < N) {
if (text[i] == pattern[j]) {
i++;
j++;
}
if (j == M) { // 패턴이 완전히 일치하는 경우
cout << "패턴이 위치 " << i - j << "에서 발견됨\n";
j = lps[j - 1]; // 다음 비교를 위해 LPS 값을 참고하여 이동
} else if (i < N && text[i] != pattern[j]) {
if (j != 0) {
j = lps[j - 1]; // LPS 값만큼 이동
} else {
i++; // 일치하는 부분이 없으면 다음 문자로 이동
}
}
}
}
int main() {
string text = "ABABDABACDABABCABAB";
string pattern = "ABABCABAB";
KMPSearch(text, pattern);
return 0;
}