출시·고도화 중
문자열 알고리즘 안내서 · 1/6
문자열 알고리즘은 글자의 나열을 빠르고 정확하게 다루는 방법을 모은 분야입니다. 편집기의 찾기 기능, 로그 검색, 검색창의 자동 완성, 유전체 서열 비교처럼 "어떤 글자 묶음이 어디에 있는가"를 묻는 일은 모두 문자열 알고리즘 위에서 돌아갑니다. 이 장에서는 이후 장에서 계속 쓸 용어를 정리하고, 가장 단순한 방법인 순진한 매칭부터 출발해 왜 더 나은 알고리즘이 필요한지 살펴봅니다.
| 용어 | 뜻 | 예(문자열 abcab) |
|---|---|---|
| 텍스트(text) | 검색 대상이 되는 긴 문자열, 길이 n | 로그 파일 전체 |
| 패턴(pattern) | 찾으려는 짧은 문자열, 길이 m | 검색어 |
| 접두사(prefix) | 앞에서부터 잘라 낸 부분 | a, ab, abc |
| 접미사(suffix) | 뒤에서부터 잘라 낸 부분 | b, ab, cab |
| 경계(border) | 접두사이면서 접미사인 진부분 문자열 | ab |
| 출현(occurrence) | 패턴이 텍스트에서 시작하는 위치 | 0부터 세는 인덱스 |
| 알파벳(alphabet) | 쓰일 수 있는 글자 집합 | ASCII, DNA의 ACGT |
"진(proper)" 접두사는 문자열 전체를 뺀 접두사를 말합니다. 경계는 KMP 알고리즘의 핵심이므로 이 장에서 확실히 익혀 두는 것이 좋습니다.
문자열 알고리즘이 푸는 문제는 크게 네 가지로 나눌 수 있습니다.
가장 직관적인 방법은 텍스트의 모든 시작 위치에서 패턴을 한 글자씩 비교하는 것입니다.
def naive_search(text: str, pattern: str) -> list[int]:
n, m = len(text), len(pattern)
hits = []
for start in range(n - m + 1):
j = 0
while j < m and text[start + j] == pattern[j]:
j += 1
if j == m:
hits.append(start)
return hits
print(naive_search("abracadabra", "abra")) # [0, 7]이 방법은 짧은 텍스트에는 충분합니다. 하지만 텍스트가 aaaa...a이고 패턴이 aaa...ab이면 시작 위치마다 거의 m번을 비교한 뒤 실패하므로 최악의 경우 O(n·m)이 됩니다. 문제는 실패할 때마다 이미 비교해서 알게 된 정보를 버리고 다음 위치에서 처음부터 다시 비교한다는 점입니다. KMP는 이 정보를 경계 표로 저장해 재사용하고, Rabin-Karp는 비교 자체를 해시값 비교로 바꿉니다.
경계 개념을 손에 익히기 위해 가장 긴 경계를 무식하게 구해 봅니다. 다음 장에서 이 값을 O(m)에 구하는 접두사 함수를 다룹니다.
def longest_border(s: str) -> int:
for length in range(len(s) - 1, 0, -1):
if s[:length] == s[-length:]:
return length
return 0
for word in ["abcab", "aaaa", "abcd", "ababa"]:
print(word, longest_border(word))
# abcab 2 / aaaa 3 / abcd 0 / ababa 3ababa의 가장 긴 경계는 aba로 길이 3입니다. 경계는 서로 겹칠 수 있다는 점에 주의합니다.
Python의 in, str.find, str.count, 그리고 정규 표현식 모듈 re는 이미 잘 최적화되어 있습니다. 실무에서 단순 검색을 위해 KMP를 다시 짤 일은 드뭅니다. 그래도 직접 구현을 알아야 하는 경우가 있습니다.
"글자"의 정의는 생각보다 단순하지 않습니다. Python의 str은 코드 포인트(code point)의 나열이고, 화면에 보이는 한 글자가 코드 포인트 여러 개일 수 있습니다.
import unicodedata
a = "café" # é 하나(U+00E9)
b = "café" # e + 결합 악센트(U+0301)
print(a == b, len(a), len(b)) # False 4 5
print(unicodedata.normalize("NFC", a) == unicodedata.normalize("NFC", b)) # True
print(len(a.encode("utf-8"))) # 5 (바이트 수)검색 전에 텍스트와 패턴을 같은 정규화 형식(NFC 등)으로 맞추지 않으면, 사람 눈에는 같은 단어가 검색되지 않습니다. 이 문제는 마지막 장에서 다시 다룹니다.
n, 패턴 길이는 m으로 쓰며, 경계는 접두사이면서 접미사인 진부분 문자열입니다.O(n·m)이며, 실패할 때 얻은 정보를 버리는 것이 약점입니다.
댓글 0개
로그인 · 로그인하면 댓글을 남길 수 있습니다.
첫 댓글을 남겨 보세요.