"ABABCABABD"에서 "ABABD"를 찾을 때, 불일치가 발생했다고 처음부터 다시 비교해야 할까요?

개념 정의

문자열 매칭은 텍스트(text)에서 패턴(pattern)이 등장하는 위치를 찾는 문제 입니다. 텍스트 길이를 N, 패턴 길이를 M이라 하면, 가장 단순한 방법은 O(N × M)이지만 KMP는 이를 O(N + M)으로 줄입니다.

왜 필요한가

  • **텍스트 에디터 **: Ctrl+F 검색 기능
  • ** 로그 분석 **: 특정 패턴의 에러 메시지 검색
  • **DNA 서열 분석 **: 특정 유전자 패턴 탐색
  • ** 네트워크 보안 **: 패킷에서 악성 패턴 탐지 (IDS/IPS)

브루트포스의 문제

JAVA
// 브루트포스 — O(N × M)
public int bruteForce(String text, String pattern) {
    int n = text.length(), m = pattern.length();
    for (int i = 0; i <= n - m; i++) {
        int j = 0;
        while (j < m && text.charAt(i + j) == pattern.charAt(j)) {
            j++;
        }
        if (j == m) return i; // 매칭 성공
    }
    return -1;
}

불일치가 발생하면 텍스트의 다음 위치로 이동하고 패턴의 처음부터 다시 비교합니다. 이전에 비교했던 정보를 전혀 활용하지 못하는 것이 비효율의 원인입니다.

동작 원리 — 실패 함수(Pi 배열)

KMP의 핵심은 ** 실패 함수(failure function)**입니다. pi[i]는 패턴의 0~i 부분 문자열에서 ** 접두사(prefix)와 접미사(suffix)가 일치하는 최대 길이 **를 저장합니다.

예시

PLAINTEXT
패턴: A B A B D
인덱스: 0 1 2 3 4

pi[0] = 0  "A" → 접두사=접미사 없음
pi[1] = 0  "AB" → 없음
pi[2] = 1  "ABA" → "A" = "A" (길이 1)
pi[3] = 2  "ABAB" → "AB" = "AB" (길이 2)
pi[4] = 0  "ABABD" → 없음

pi = [0, 0, 1, 2, 0]

실패 함수의 의미

불일치가 발생했을 때, ** 패턴을 어디까지 되돌려야 하는지** 알려줍니다. pi[j-1]이 가리키는 위치부터 비교를 재개하면 됩니다.

PLAINTEXT
텍스트:  A B A B C A B A B D
패턴:    A B A B D
                 ↑ 불일치! (j=4)

pi[3] = 2이므로, 패턴의 인덱스 2부터 비교 재개
→ 이미 "AB"가 매칭되었다는 정보를 활용

텍스트:  A B A B C A B A B D
패턴:            A B A B D
                     ↑ j=2부터 시작

코드 예제 — Java

실패 함수 구성

JAVA
// 실패 함수(pi 배열) 구성 — O(M)
private int[] buildFailureFunction(String pattern) {
    int m = pattern.length();
    int[] pi = new int[m];
    int len = 0; // 이전까지 일치한 접두사/접미사 길이
    int i = 1;

    while (i < m) {
        if (pattern.charAt(i) == pattern.charAt(len)) {
            len++;
            pi[i] = len;
            i++;
        } else {
            if (len > 0) {
                len = pi[len - 1]; // 이전 실패 함수 값으로 점프
            } else {
                pi[i] = 0;
                i++;
            }
        }
    }
    return pi;
}

len = pi[len - 1]이 핵심입니다. 불일치 시 처음으로 돌아가지 않고, 이전에 매칭된 정보를 활용해 적절한 위치로 점프합니다.

KMP 매칭

JAVA
public List<Integer> kmpSearch(String text, String pattern) {
    List<Integer> result = new ArrayList<>();
    int n = text.length(), m = pattern.length();
    int[] pi = buildFailureFunction(pattern);

    int i = 0; // 텍스트 포인터
    int j = 0; // 패턴 포인터

    while (i < n) {
        if (text.charAt(i) == pattern.charAt(j)) {
            i++;
            j++;
        }

        if (j == m) {
            result.add(i - j); // 매칭 위치 기록
            j = pi[j - 1];    // 다음 매칭을 위해 점프
        } else if (i < n && text.charAt(i) != pattern.charAt(j)) {
            if (j > 0) {
                j = pi[j - 1]; // 실패 함수 활용
            } else {
                i++;
            }
        }
    }
    return result;
}

시간 복잡도 분석

  • 실패 함수 구성: O(M)
  • 매칭: O(N) — i는 항상 증가하고, j는 감소할 수 있지만 총 감소량이 증가량을 넘지 않습니다
  • 전체: O(N + M)

Java String.indexOf()와의 비교

Java의 String.indexOf()는 기본적으로 브루트포스 방식입니다. 그런데도 실무에서 충분히 빠른 이유가 있습니다.

JAVA
// Java 내부 — 실제로는 네이티브 메서드로 최적화
public int indexOf(String str) {
    // OpenJDK: 기본적으로 단순 비교
    // 하지만 JIT 컴파일러가 SIMD 명령어로 최적화할 수 있음
}
  • 대부분의 실제 문자열은 최악 케이스가 아닙니다 (패턴이 "AAAA...AB" 같지 않음)
  • JVM의 JIT 컴파일러가 하드웨어 수준에서 최적화합니다
  • 짧은 패턴에서는 브루트포스의 상수 팩터가 작아 KMP보다 빠를 수 있습니다

KMP가 진가를 발휘하는 것은 ** 최악 케이스가 보장되어야 할 때 **입니다. 보안 관련 패턴 매칭이나 매우 긴 텍스트 처리에서 중요합니다.

다른 문자열 매칭 알고리즘

알고리즘시간 복잡도특징
브루트포스O(N × M)단순, 짧은 패턴에 효율적
KMPO(N + M)최악 보장, 스트리밍에 적합
Boyer-MooreO(N/M) 최선긴 패턴에 효율적, 실무에서 많이 사용
Rabin-KarpO(N + M) 평균다중 패턴 매칭에 적합

백엔드/실무 연결

로그 검색

대량의 로그 파일에서 특정 에러 패턴을 검색할 때, KMP 계열 알고리즘이 사용됩니다. grep은 내부적으로 Boyer-Moore와 비슷한 알고리즘을 사용합니다.

네트워크 침입 탐지(IDS)

네트워크 패킷을 실시간으로 스캔하면서 악성 패턴을 탐지하는 시스템에서, 최악 시간이 보장되는 KMP가 유리합니다. 공격자가 의도적으로 최악 케이스를 만들 수 있기 때문입니다.

스트리밍 매칭

KMP는 텍스트를 한 번만 순회하므로 ** 스트리밍 환경 **에 적합합니다. 네트워크 패킷이 순서대로 도착할 때, 도착하는 대로 매칭을 진행할 수 있습니다.

주의할 점

실패 함수(pi 배열) 구성에서 off-by-one 에러

pi 배열의 인덱스와 패턴 인덱스의 관계를 혼동하면 매칭 위치가 한 칸씩 어긋납니다. pi[i]는 패턴의 0~i 부분 문자열에서 접두사와 접미사가 일치하는 최대 길이입니다.

Java의 String.indexOf()가 KMP가 아닌 것을 모르는 실수

OpenJDK의 String.indexOf()는 기본적으로 브루트포스입니다. 대량 텍스트에서 빈번한 매칭이 필요하면 직접 KMP를 구현하거나 라이브러리를 사용해야 합니다.


정리

  • KMP 는 실패 함수(pi 배열)를 이용해 불일치 시 되돌아갈 위치를 미리 계산합니다
  • pi[i]는 패턴의 0~i 부분 문자열에서 접두사=접미사인 최대 길이입니다
  • 시간 복잡도 O(N + M)으로 브루트포스의 O(N × M)을 크게 개선합니다
  • Java의 String.indexOf()는 브루트포스지만 JVM 최적화로 대부분의 경우 충분히 빠릅니다
  • 최악 케이스 보장이 필요한 보안/스트리밍 환경에서 KMP의 가치가 빛납니다
댓글 로딩 중...