정규 표현식 (Regular Expression) 은 문자열 패턴을 형식 언어로 표현한 표기법. Stephen Kleene 이 1956년 정규 언어 (regular language) 를 정의하며 도입. Ken Thompson 이 1968년 이를 NFA 로 변환하는 알고리즘 (Thompson construction) 을 구현해 Unix grep, ed 에 탑재. RE → NFA → DFA (선택) → 매칭.
문제 상황과 동기
"ab*c" 패턴: "ac", "abc", "abbc", ... 모두 매칭.
naive: 모든 경우의 수 직접 확인. * (0회 이상) 때문에 지수적 폭발.
Thompson NFA: 정규식 길이 |R| 에 대해 O(|R|) 크기 NFA 생성. 매칭 O(N x |R|) (NFA simulation) 또는 O(N) (DFA 변환 후).
핵심 통찰: Kleene star * 는 NFA 의 epsilon 전이로 구현. Thompson construction 은 정규식 각 부분을 소형 NFA 조각으로 만들고 epsilon 으로 연결.
시각화
핵심 아이디어
기본 연산
연산
표기
언어
문자
a
{a}
연결
ab
{ab}
선택
a|b
{a, b}
반복
a*
{ε, a, aa, ...}
그룹
(ab)
하위 표현식
Thompson Construction & NFA Simulation
a: ─→[○]─a─→[●] AB: [A]─ε─→[B]A|B: ─→[A]─→ / └→[B]─→ A*: ─→[A]─→ with ε loop back
NFA Simulation:
현재 가능한 상태 집합 유지. 각 문자마다 epsilon closure 계산 후 문자 전이. O(N x |R|). DFA 로 변환 (powerset construction) 시 O(N) 매칭 (공간 2^|R| 최악).
알고리즘
re_to_nfa(regex): postfix = shunting_yard(regex) stack = [] for t in postfix: if t is char: stack.push(NFA_fragment(t)) elif t == '.': b=stack.pop(); a=stack.pop() a.accept.out = b.start; stack.push(NFA(a.start, b.accept)) elif t == '|': b=stack.pop(); a=stack.pop() s = State(); s.out=a.start; s.out1=b.start acc = State(); a.accept.out=acc; b.accept.out=acc stack.push(NFA(s, acc)) elif t == '*': a = stack.pop() s = State(); acc = State() s.out=a.start; s.out1=acc a.accept.out=a.start; a.accept.out1=acc stack.push(NFA(s, acc)) return stack[0]
# Thompson NFA 기반 regex 매칭import sysclass State: def __init__(self): self.c = None; self.out = None; self.out1 = Noneclass NFA: def __init__(self, s, a): self.start = s; self.accept = adef to_nfa(post): stk = [] for c in post: if c == '.': b, a = stk.pop(), stk.pop() a.accept.out = b.start; a.accept.c = None stk.append(NFA(a.start, b.accept)) elif c == '|': b, a = stk.pop(), stk.pop() s, acc = State(), State() s.out = a.start; s.out1 = b.start a.accept.out = acc; b.accept.out = acc stk.append(NFA(s, acc)) elif c == '*': a = stk.pop(); s, acc = State(), State() s.out = a.start; s.out1 = acc a.accept.out = a.start; a.accept.out1 = acc stk.append(NFA(s, acc)) else: s, acc = State(), State(); s.c = c; s.out = acc stk.append(NFA(s, acc)) return stk[0]def eps(states): stk = list(states) while stk: s = stk.pop() if s.c is None: if s.out and s.out not in states: states.add(s.out); stk.append(s.out) if s.out1 and s.out1 not in states: states.add(s.out1); stk.append(s.out1) return statesdef match(nfa, text): states = eps({nfa.start}) for ch in text: nxt = set() for s in states: if s.c == ch or s.c == '.': nxt.add(s.out) states = eps(nxt) return nfa.accept in statesprec = {'*':3, '.':2, '|':1}def postfix(re): out, stk, i, p = [], [], 0, '' while i < len(re): c = re[i] if c == '(': if p and p not in '(|': stk.append('.') stk.append(c) elif c == ')': while stk and stk[-1] != '(': out.append(stk.pop()) stk.pop() elif c in '*|': if c == '|' and out and out[-1] in '(|': out.append('') while stk and stk[-1] != '(' and prec.get(stk[-1],0) >= prec.get(c,0): out.append(stk.pop()) stk.append(c) else: if p and p not in '(|*': stk.append('.') out.append(c) p = c; i += 1 while stk: out.append(stk.pop()) return ''.join(out)re = sys.stdin.readline().strip()print(1 if match(to_nfa(postfix(re)), sys.stdin.readline().strip()) else 0)
// Regex 매칭 (백트래킹, 문자/./*/|/()/연결)import java.io.*;public class Main { static boolean match(String s, int si, String p, int pi) { if (pi == p.length()) return si == s.length(); boolean star = pi+1 < p.length() && p.charAt(pi+1) == '*'; if (star) { if (match(s, si, p, pi+2)) return true; while (si < s.length() && (p.charAt(pi)=='.' || s.charAt(si)==p.charAt(pi))) { if (match(s, si+1, p, pi+2)) return true; si++; } return false; } if (si < s.length() && (p.charAt(pi)=='.' || s.charAt(si)==p.charAt(pi))) return match(s, si+1, p, pi+1); return false; } public static void main(String[] args) throws IOException { BufferedReader br = new BufferedReader(new InputStreamReader(System.in)); System.out.println(match(br.readLine(), 0, br.readLine(), 0) ? 1 : 0); }}
stdin
ab*cac
결과
1
stdin
a.cabc
결과
1
stdin
a.cac
결과
0
복잡도
항목
값
RE to NFA (Thompson)
O(
NFA simulation (전체)
O(N x
NFA to DFA
O(2^
DFA 매칭
O(N)
변형 / 활용
POSIX vs Perl 호환: ERE 는 +, ?, {m,n}. PCRE 는 역참조, lookahead/lookbehind 포함 (정규 언어 초과).
DFA vs 백트래킹: RE2/grep = DFA (선형). PCRE/Python re = 백트래킹 (지수 가능).
Lex/Flex: 정규식을 DFA 로 컴파일해 tokenizer 생성.
Kleene 정리: 정규 언어는 합집합, 연결, Kleene star 에 닫힘. DFA = 정규 표현식.
함정
지수적 백트래킹: (a|a)*b 에서 "aaaa...ac" 매칭 시 지수 시간. DFA 엔진은 선형.
빈 문자열 epsilon: a* 0회 반복 케이스 누락 시 "ac" 가 a*c 에 매칭 안 됨.
💬 댓글