Aho-Corasick#
설명#
Aho-Corasick, 아호-코라식은
여러 개의 패턴 문자열을 한 번의 탐색으로 동시에 매칭할 수 있는
멀티 패턴 문자열 매칭 알고리즘이다.
KMP 같은 단일 패턴 매칭 알고리즘은
하나의 패턴에 대해서는 선형 시간에 탐색이 가능하지만,
패턴이 여러 개일 경우 각 패턴마다 탐색을 수행해야 하므로
전체 시간 복잡도는 패턴 개수에 비례하여 증가한다.
반면, 아호-코라식 알고리즘은
모든 패턴을 하나의 Trie 구조로 통합하고,
실패 시 이동할 위치를 미리 계산한 실패 링크를 이용해
입력 문자열을 한 번만 순회하면서
모든 패턴의 출현 여부를 동시에 판별할 수 있다.
활용 예시#
- Bad words filter
욕설, 금칙어 목록이 다수 존재할 때
채팅/댓글 문자열을 한 번만 스캔하여 필터링 가능 - 보안
IDS/IPS에서 시그니쳐 기반 패킷 검사
악성 문자열 패턴 탐지 - 문자열 분석
로그 분석
다수 키워드 동시 검색
장점#
- 멀티 패턴 매칭을 선형 시간에 처리해, 패턴 개수가 많아질수록 압도적으로 효율적
단점#
- Trie + 실패 링크 구성으로 인해 메모리 사용량 증가
- 구현 난이도
- 동적 패턴 추가/삭제에 취약 (실패링크 리빌드 비용 발생)
시간 복잡도#
|S|: S 문자열 길이, text: 비교 문자열, pattern: 패턴 문자열
- Trie 생성: \( O(\Sigma |pattern|) \)
- 실패 링크 구성: \( O(\Sigma |pattern|) \)
- 문자열 탐색: \( O(|text|) \) + (매칭 횟수)
따라서, 전체 시간 복잡도는 \( O(|text| + \Sigma |pattern|) \)이고,
한 번의 빌드 이후 추가적인 탐색에서는 \( O(|text|) \) 로 수렴한다.
Go
import "container/list"
type Result struct {
index int
pattern string
}
type node struct {
children map[rune]*node
fail *node
output []string
}
func newNode() *node {
children := make(map[rune]*node)
return &node{children: children, fail: nil, output: nil}
}
type AhoCorasick struct {
root *node
}
func (a *AhoCorasick) Init(patterns []string) {
a.root = newNode()
for _, pattern := range patterns {
a.insert(pattern)
}
a.connect()
}
func (a *AhoCorasick) insert(pattern string) {
now := a.root
for _, char := range []rune(pattern) {
if _, exists := now.children[char]; !exists {
now.children[char] = newNode()
}
now = now.children[char]
}
if now.output == nil {
now.output = make([]string, 0)
}
now.output = append(now.output, pattern)
}
func (a *AhoCorasick) connect() {
a.root.fail = a.root
q := list.New()
q.PushBack(a.root)
for q.Len() > 0 {
now := q.Remove(q.Front()).(*node)
for key, next := range now.children {
if now == a.root {
next.fail = a.root
} else {
dst := now.fail
for dst != a.root && dst.children[key] == nil {
dst = dst.fail
}
if child, exists := dst.children[key]; exists {
dst = child
}
next.fail = dst
}
next.output = append(next.output, next.fail.output...)
q.PushBack(next)
}
}
}
func (a *AhoCorasick) search(word string) []Result {
now := a.root
res := make([]Result, 0)
for i, char := range []rune(word) {
for now != a.root && now.children[char] == nil {
now = now.fail
}
if _, exists := now.children[char]; exists {
now = now.children[char]
}
for _, p := range now.output {
res = append(res, Result{i - len(p) + 1, p})
}
}
return res
}Python
class Node:
def __init__(self):
self.children = dict()
self.fail = None
self.output = []
class AhoCorasick:
def __init__(self, patterns: List[str]):
self.root = Node()
for pattern in patterns:
self.insert(pattern)
self.connect()
def insert(self, pattern: str):
'''
아호-코라식 자료구조에 패턴을 삽입
:param pattern: 패턴 문자열
:type pattern: str
'''
now = self.root
for char in pattern:
now = now.children.setdefault(char, Node())
now.output.append(pattern)
def connect(self):
'''
실패 링크 연결
'''
from collections import deque
self.root.fail = self.root
q = deque([self.root])
while q:
now = q.popleft()
for char in now.children:
nxt = now.children[char]
if now is self.root:
nxt.fail = self.root
else:
dst = now.fail
while dst is not self.root and char not in dst.children:
dst = dst.fail
if char in dst.children:
dst = dst.children[char]
nxt.fail = dst
nxt.output += nxt.fail.output
q.append(nxt)
def search(self, word: str) -> List[Tuple[int, str]]:
'''
word에서 매칭되는 모든 패턴과, 해당 패턴이 시작하는 위치를 출력
:param word: 검색 문자열
:type word: str
:return: (index, pattern)형태의 패턴과 해당 패턴의 시작 인덱스
:rtype: List[Tuple[int, str]]
'''
now = self.root
res = []
for i, char in enumerate(word):
while now is not self.root and char not in now.children:
now = now.fail
if char in now.children:
now = now.children[char]
for p in now.output:
res.append((i - len(p) + 1, p))
return res코드에 왜 BFS가 있지?#
connect()메서드는 기본적으로 BFS를 통해 실패 링크를 연결한다.
이 BFS는 문자열 탐색을 위한 것이 아니라, 실패 링크를 올바른 순서로 구성하기 위해 필요하다.
실패 링크#
“현재 노드에서 더 이상 문자를 이어갈 수 없을 때, 어디로 돌아가야 가장 긴 접미사를 유지할 수 있는가?”
실패 링크는 기본적으로 KMP의 실패 함수와 동일한 개념이지만,
아호-코라식에서는 이를 Trie 전체로 확장한 구조라고 할 수 있다.
실패 링크를 설정할 때, 부모 노드의 실패 링크가 이미 올바르게 설정되어 있어야 함을 전제로 한다.
그렇기에, BFS의 특성인 레벨 순회를 통해 위의 속성을 만족시키면서,
한 번의 순회로 모든 실패 링크를 구성할 수 있다.