상태(state) + 전이(transition)로 구성된 추상 기계. 입력을 읽으며 상태 이동, 최종 상태가 수락 상태면 매칭 성공.
[상태A] --입력--> [상태B] --입력--> [수락상태]
| DFA | NFA | |
|---|---|---|
| 각 입력에 대한 다음 상태 | 정확히 1개 | 0개, 1개, 또는 여러 개 |
| ε-전이 | 없음 | 있음 |
| 현재 상태 | 항상 1개 | 여러 개 동시 가능 |
| 구현 | 단순 | 시뮬레이션 필요 |
ε-전이: 입력 소비 없이 다른 상태로 이동. a*에서 "0번 매칭" 경로가 이것.
NFA가 정규표현식에 적합한 이유: 패턴 → NFA 변환이 직관적. |는 분기, *는 루프로 자연스럽게 대응.
왜 "시뮬레이션"인가: NFA는 "동시에 여러 상태"라는 비물리적 개념. 실제 컴퓨터는 한 번에 한 상태만 처리 가능. 따라서 NFA의 동작을 결정적 알고리즘으로 흉내내는 것.
한 경로 선택 → 끝까지 탐색 → 실패 시 되돌아가서 다른 경로.
경로1 시도 → 실패 → 경로2 시도 → 실패 → ...
문제: 경로 수가 지수적 폭발 가능. a?{n}a{n} 패턴에서 O(2^n).
그럼에도 대부분 언어가 채택한 이유: 역참조(\1), lookahead 등 확장 기능 지원 필요. 이건 순수 NFA로 불가능. 메모이제이션도 캡처 내용이 키에 포함되어 효과 없음.
모든 가능한 상태를 집합으로 동시 추적.
current = {가능한 상태들}
for ch in input:
next = {current에서 ch로 전이 가능한 상태들의 ε-closure}
current = next
return MATCH ∈ current
핵심 최적화 - list_id:
if s.lastlist == list_id: # 이번 라운드에 이미 추가됨
return # 중복 처리 안 함
s.lastlist = list_id같은 상태에 100개 경로로 도달해도 한 번만 처리. O(nm) 보장.
메모이제이션 백트래킹과 비교:
- 시간복잡도 동일: O(nm)
- 공간: Thompson O(m), 메모이제이션 O(nm)
- Thompson이 더 효율적
이 구현에서 .은 일반 정규식의 "아무 문자 매칭"이 아님.
일반 정규식: a.b → a + (아무 문자) + b
이 코드: ab. → a와 b를 연결 (postfix 표기)
re2post()가 암묵적 연결을 명시적 . 연산자로 변환:
infix: ab → postfix: ab.
infix: abc → postfix: ab.c.
match(pattern, text)
│
├─→ re2post(pattern) // "a(b|c)*" → "abc|*.""
│ infix → postfix 변환. 연결을 명시적 '.'로 삽입.
│
├─→ post2nfa(postfix) // postfix → NFA 그래프
│ 스택 기반. 리터럴/연산자마다 Fragment 조립.
│ 최종 Fragment의 매달린 엣지를 matchstate에 연결.
│
└─→ match(start, text) // NFA 실행
│
├─→ startlist() // 초기 상태 집합 (ε-closure)
│
├─→ step() // 문자 하나 처리, 다음 상태 집합 계산
│ 내부에서 addstate() 호출 (ε-closure 포함)
│
└─→ ismatch() // 최종 집합에 matchstate 있는지
enum { Match = 256, Split = 257 };
struct State {
int c; // 문자(0-255), Match, Split 중 하나
State *out; // 다음 상태
State *out1; // Split일 때 두 번째 분기
int lastlist; // 중복 방문 방지
};c < 256: 리터럴. 해당 문자 소비 후out으로 전이.c == Split: ε-전이 2개.out과out1로 동시 이동.c == Match: 수락 상태.
union Ptrlist {
Ptrlist *next;
State *s;
};아직 연결 안 된 State* 포인터들을 linked list로 관리. 메모리 추가 할당 없이 포인터 자체를 저장소로 재활용.
patch(e.out, s); // 매달린 포인터들에 실제 상태 주소 기록void addstate(List *l, State *s) {
if (s == NULL || s->lastlist == listid) return; // 중복 방지
s->lastlist = listid;
if (s->c == Split) {
addstate(l, s->out); // ε-전이 따라감
addstate(l, s->out1);
return;
}
l->s[l->n++] = s; // 실제 문자 상태만 리스트에 추가
}