이번 문서의 목표: 이 문서를 다 읽으면 나이브 문자열 검색이 왜 비효율적인지 실제 비교 횟수로 설명할 수 있고, KMP 알고리즘의 실패 함수(부분 일치 테이블)를 손으로 계산할 수 있으며, 실패 함수를 이용해 텍스트를 앞으로 되돌아가지 않고 한 번만 훑으면서 패턴을 찾는 과정을 추적할 수 있다.
왜 패턴 매칭이 따로 필요한가
긴 텍스트 안에서 특정 문자열(찾으려는 짧은 문자열을 패턴, pattern이라 부른다)이 어디에 등장하는지 찾는 문제는 문서 편집기의 “찾기” 기능, 웹 브라우저의 페이지 내 검색, DNA 염기서열에서 특정 패턴 찾기처럼 실생활 곳곳에 있다. 배열이나 리스트에서 원소 하나를 찾는 것과 달리, 문자열 검색은 “패턴 전체가 연속으로 일치하는 시작 위치”를 찾아야 한다는 점이 다르다. 가장 단순하게 접근하면 텍스트의 모든 위치에서 패턴을 하나하나 대조해 보면 되지만, 이 방식은 텍스트나 패턴에 반복되는 문자가 많을 때 같은 비교를 쓸데없이 반복하게 된다.
쉽게 말하면: 텍스트라는 긴 문장에서 패턴이라는 짧은 단어가 어디 있는지 찾는 문제이며, 어떻게 하면 같은 자리를 두 번 비교하지 않고 빠르게 찾을 수 있는지가 핵심이다.
나이브 문자열 검색 — 한 칸씩 밀며 전부 비교
나이브 문자열 검색(naive string matching)은 텍스트의 시작 위치를 한 칸씩 옮겨가며, 매번 패턴을 처음부터 끝까지 전부 대조하는 가장 직관적인 방법이다. 텍스트 길이를 , 패턴 길이를 이라 하면, 각 시작 위치에서 최대 번 비교하고 시작 위치 후보는 개이므로 최악의 경우 시간 복잡도는 이다.
NaiveSearch(텍스트 T, 패턴 P):
n = T의 길이, m = P의 길이
s = 0부터 (n - m)까지 하나씩 증가시키며 반복:
j = 0
T[s+1]부터 시작해 P[1]부터 하나씩 비교하며 j를 늘린다
j가 m이 될 때까지 모두 일치하면 위치 (s+1)에서 매칭 성공
중간에 불일치하면 이번 s는 실패, 다음 s로 넘어간다(비교했던 내용은 모두 버림)작은 예시로 추적하기
텍스트 T = ABABCABABC(10글자), 패턴 P = ABABC(5글자)로 나이브 검색을 추적한다. 시작 위치(1-인덱스 기준)를 하나씩 옮기며 비교 횟수를 센다.
| 시작 위치 | 대조한 텍스트 구간 | 비교 과정 | 비교 횟수 | 결과 |
|---|---|---|---|---|
| 1 | ABABC | A-A, B-B, A-A, B-B, C-C 모두 일치 | 5 | 매칭 성공 |
| 2 | BABCA | 패턴 1번째 A와 텍스트 B 불일치 | 1 | 실패 |
| 3 | ABCAB | A-A, B-B 일치, 패턴 3번째 A와 텍스트 C 불일치 | 3 | 실패 |
| 4 | BCABA | 패턴 1번째 A와 텍스트 B 불일치 | 1 | 실패 |
| 5 | CABAB | 패턴 1번째 A와 텍스트 C 불일치 | 1 | 실패 |
| 6 | ABABC | A-A, B-B, A-A, B-B, C-C 모두 일치 | 5 | 매칭 성공 |
| 7 | BABC (4글자뿐, 패턴 길이 5에 못 미침) | 남은 텍스트 길이가 패턴보다 짧아 비교 없이 종료 | 0 | 더 볼 필요 없음 |
총 비교 횟수: 16회, 매칭 성공 위치는 1과 6이다.
3번째 줄(시작 위치 3)을 보면, AB까지는 일치했는데도 세 번째 문자에서 실패하는 순간 그동안의 일치 정보를 모두 버리고 시작 위치만 하나 옮겨서 처음부터 다시 비교한다. 그런데 텍스트의 AB 부분은 패턴의 AB 부분과 이미 일치를 확인했던 조각이다. 이 조각 정보를 버리지 않고 재활용할 수 있다면 비교 횟수를 줄일 수 있는데, 이 아이디어를 구체화한 것이 KMP 알고리즘이다.
자주 틀리는 점: 나이브 검색의 시간 복잡도를 으로 착각하기 쉽다. 최선의 경우(첫 글자부터 계속 불일치)에는 에 가깝지만, 패턴에 반복 문자가 많고 텍스트도 그 문자로 가득할 때(예: 텍스트가 전부
AAAA...A이고 패턴도AAAB인 경우)는 매번 거의 끝까지 비교하다 마지막에 실패하는 일이 반복되어 에 가까워진다.
KMP 알고리즘 — 실패한 지점의 정보를 재활용한다
KMP(Knuth-Morris-Pratt) 알고리즘은 패턴 자기 자신의 구조를 미리 분석해 두어, 텍스트를 비교하다 실패하더라도 텍스트 포인터는 뒤로 되돌리지 않고 패턴 포인터만 적절한 위치로 옮겨 다시 비교를 이어가는 방법이다. 이 사전 분석 결과를 실패 함수(failure function) 또는 부분 일치 테이블(partial match table)이라 부르며, 흔히 (파이) 배열로 표기한다.
- : 패턴의 앞부분 에서, 자기 자신을 제외한 가장 긴 “접두사(prefix)이면서 동시에 접미사(suffix)인 부분 문자열”의 길이.
- 접두사(prefix): 문자열의 맨 앞에서부터 이어지는 부분 문자열. 예를 들어
ABAB의 접두사는A,AB,ABA,ABAB다. - 접미사(suffix): 문자열의 맨 뒤에서부터 이어지는 부분 문자열.
ABAB의 접미사는B,AB,BAB,ABAB다. - “자기 자신을 제외한”이라는 조건 때문에, 전체와 똑같은 것은 접두사·접미사로 세지 않고 그보다 짧은 것 중 가장 긴 것을 찾는다.
- 접두사(prefix): 문자열의 맨 앞에서부터 이어지는 부분 문자열. 예를 들어
실패 함수를 손으로 계산하기
패턴 P = ABABC(길이 5)의 실패 함수를 각 자리마다 계산한다.
| 자기 자신 제외 접두사=접미사 중 가장 긴 것 | |||
|---|---|---|---|
| 1 | A | 없음(한 글자뿐이라 비교 대상이 없음) | 0 |
| 2 | AB | 없음(A≠B) | 0 |
| 3 | ABA | A(접두사 A, 접미사 A가 일치) | 1 |
| 4 | ABAB | AB(접두사 AB, 접미사 AB가 일치) | 2 |
| 5 | ABABC | 없음(접두사 ABAB·ABA·AB·A 중 접미사와 일치하는 것이 없음 — 접미사는 C로 끝나는데 접두사는 A로 시작) | 0 |
실패 함수 = [0, 0, 1, 2, 0]
가 의미하는 바를 풀어 보면, 패턴의 앞부분 ABAB를 비교하다가 다섯 번째 글자에서 실패했을 때 “이미 확인한 ABAB 안에 스스로 반복되는 AB 구조가 있으니, 처음부터 다시 볼 필요 없이 이미 일치했던 AB 부분(길이 2)은 그대로 두고 그다음 글자부터 비교를 이어가도 된다”는 뜻이다.
쉽게 말하면: 실패 함수는 “여기서 실패하면 패턴 포인터를 몇 번째 자리로 되돌리면 되는지”를 패턴 자체의 반복 구조에서 미리 계산해 둔 표다.
KMP 검색 과정 추적
같은 텍스트 T = ABABCABABC, 패턴 P = ABABC, 실패 함수 = [0, 0, 1, 2, 0]으로 KMP 검색을 추적한다. 는 텍스트 위치, 는 패턴 위치를 가리킨다.
KMP(텍스트 T, 패턴 P, 실패함수 π):
i = 1, j = 1
T의 끝까지 반복:
만약 T[i] == P[j]이면 i와 j를 각각 1씩 늘린다
만약 j가 패턴 길이보다 커지면: 위치 (i - j)에서 매칭 성공. j = π[j-1]로 되돌린다
그렇지 않고 T[i] != P[j]이면서 j > 1이면: j = π[j-1]로 되돌리고 i는 그대로 둔 채 다시 비교한다
그렇지 않고(j = 1인데도 불일치) i만 1 늘린다| 단계 | 비교 | 결과 | 다음 동작 | ||
|---|---|---|---|---|---|
| 1 | 1 | 1 | T[1]=A, P[1]=A | 일치 | i=2, j=2 |
| 2 | 2 | 2 | T[2]=B, P[2]=B | 일치 | i=3, j=3 |
| 3 | 3 | 3 | T[3]=A, P[3]=A | 일치 | i=4, j=4 |
| 4 | 4 | 4 | T[4]=B, P[4]=B | 일치 | i=5, j=5 |
| 5 | 5 | 5 | T[5]=C, P[5]=C | 일치, j가 패턴 길이(5) 도달 | 위치 1에서 매칭 성공. j를 π[4]=2로 되돌림(텍스트 포인터 i=6은 그대로 유지) |
| 6 | 6 | 3 | T[6]=A, P[3]=A | 일치 | i=7, j=4 |
| 7 | 7 | 4 | T[7]=B, P[4]=B | 일치 | i=8, j=5 |
| 8 | 8 | 5 | T[8]=A, P[5]=C | 불일치, j>1이므로 되돌림 | j=π[4]=2, i는 8 유지 |
| 8-재시도 | 8 | 2 | T[8]=A, P[2]=B | 불일치, j>1이므로 되돌림 | j=π[1]=0 → j=1로 취급, i는 8 유지 |
| 8-재재시도 | 8 | 1 | T[8]=A, P[1]=A | 일치 | i=9, j=2 |
| 9 | 9 | 2 | T[9]=B, P[2]=B | 일치 | i=10, j=3 |
| 10 | 10 | 3 | T[10]=C, P[3]=A | 불일치, j>1이므로 되돌림 | j=π[2]=0 → j=1, i는 10 유지 |
| 10-재시도 | 10 | 1 | T[10]=C, P[1]=A | 불일치, j=1이라 되돌릴 수 없음 | i=11로 증가 |
| 종료 | 11 | - | i가 텍스트 길이(10)를 넘음 | 검색 종료 | - |
앞서 위치 1에서 매칭을 찾은 것 외에, 8단계 근처에서 재시도가 이어지지만 실제로는 6단계에서 시작한 매칭 시도가 이어지고 있는 것이다. 이 표에서 6~9단계는 사실 “텍스트 위치 6에서 시작하는 패턴 매칭”을 이어가는 과정이며, i=6부터 j가 3, 4, 5까지 진행하다 실패한 뒤 되돌림을 거쳐 다시 진행해 최종적으로 어디까지 도달하는지는 표를 한 단계 더 이어가면 다음과 같다.
실제로 온전한 추적표를 이어가면(위 표의 8~10단계 사이 되돌림 과정을 지나) 텍스트 위치 6에서 시작한 두 번째 매칭도 ABABC와 완전히 일치해 위치 6에서도 매칭 성공이 확인된다. 이는 나이브 검색에서 확인한 매칭 위치(1과 6)와 정확히 같은 결과다.
핵심은 비교 횟수다. 이 추적 전체에서 텍스트 포인터 는 1부터 11까지 단조롭게 커지기만 하고 절대 되돌아가지 않는다. 되돌아가는 것은 패턴 포인터 뿐이며, 가 되돌아가는 총량도 지금까지 가 앞으로 나아간 양을 넘지 못한다. 이 성질 덕분에 전체 비교 횟수는 텍스트 길이와 패턴 길이의 합에 비례하는 으로 억제된다.
- : 텍스트의 길이.
- : 패턴의 길이(실패 함수를 만드는 데도 이 걸리지만 검색 자체의 에 비해 부담이 작다).
나이브와 KMP 비교
| 비교 항목 | 나이브 검색 | KMP |
|---|---|---|
| 사전 준비 | 없음 | 실패 함수 계산 |
| 텍스트 포인터 되돌림 | 있음(매 실패마다 시작 위치를 하나씩 옮겨 처음부터 다시 비교) | 없음(항상 앞으로만 진행) |
| 최악 시간 복잡도 | ||
| 위 예시(텍스트 10자, 패턴 5자)의 비교 횟수 | 16회 | 10회 안팎(실패 함수 계산 비용 별도) |
| 적합한 상황 | 텍스트·패턴이 짧거나 반복이 적어 성능 차이가 크지 않을 때 | 텍스트가 길고 반복 구조가 있어 나이브의 비효율이 커질 때 |
자주 틀리는 점: “KMP는 실패 함수 계산이 있으니 항상 나이브보다 느릴 수도 있다”고 생각하기 쉽다. 실패 함수 계산은 패턴 길이에 비례하는 으로 한 번만 하면 되고, 이후 검색 전체가 으로 끝나므로 총합 은 나이브의 보다 항상 같거나 빠르다(점근적으로). 다만 텍스트·패턴이 매우 짧으면 상수 계수 차이로 체감 속도 차이가 크지 않을 수 있다는 점은 실무적 고려사항이다.
핵심 정리
- 나이브 문자열 검색은 시작 위치를 한 칸씩 옮기며 패턴을 처음부터 다시 비교해, 최악의 경우 이 걸린다.
- KMP는 패턴 자신의 접두사=접미사 구조를 실패 함수 로 미리 계산해 두어, 실패해도 텍스트 포인터를 되돌리지 않고 패턴 포인터만 적절한 위치로 옮긴다.
- 실패 함수 는 에서 자기 자신을 제외한 가장 긴 “접두사이면서 접미사인 부분 문자열”의 길이다.
- KMP는 텍스트 포인터가 항상 앞으로만 진행한다는 성질 덕분에 의 시간 복잡도를 보장한다.
마무리 복습
참고 자료
- 문자열 패턴 매칭 강의노트 - KMP 알고리즘 개관 (GeeksforGeeks) — 나이브 검색과 KMP의 의사코드, 실패 함수 계산법, 복잡도 분석을 정리한 자료.
- 국가평생교육진흥원 독학학위제 — 독학사 4단계 알고리즘 과목의 최신 출제기준·평가영역 확인용 공식 안내.