expr → term (('+' | '-') term)*term → factor (('*' | '/') factor)*factor → NUMBER | '(' expr ')'
Recursive Descent
parse_expr(): left = parse_term() while peek in ('+', '-'): op = consume() right = parse_term() left = Node(op, left, right) return leftparse_term(): left = parse_factor() while peek in ('*', '/'): op = consume(); right = parse_factor() left = Node(op, left, right) return leftparse_factor(): if peek == NUM: return Leaf(consume().val) if peek == '(': consume('('); node = parse_expr(); consume(')'); return node
invariant: 각 parse 함수는 자신의 우선순위 노드를 소비. 다음 토큰은 더 높은 우선순위의 첫 토큰.
구현
// Recursive descent expression parser#include <bits/stdc++.h>using namespace std;struct Token { enum { NUM, OP, LP, RP } type; char op; int val; };vector<Token> tokenize(const string& s) { vector<Token> res; for (size_t i = 0; i < s.size(); i++) { if (s[i] == ' ') continue; if (isdigit(s[i])) { int v = 0; while (i < s.size() && isdigit(s[i])) v = v*10 + (s[i++]-'0'); res.push_back({Token::NUM, 0, v}); i--; } else if (s[i] == '(') res.push_back({Token::LP, 0, 0}); else if (s[i] == ')') res.push_back({Token::RP, 0, 0}); else res.push_back({Token::OP, s[i], 0}); } return res;}struct Node { char op; int val; Node *l, *r; Node(int v) : op(0), val(v), l(0), r(0) {} Node(char o, Node* a, Node* b) : op(o), val(0), l(a), r(b) {}};int eval(Node* n) { if (!n->op) return n->val; int l = eval(n->l), r = eval(n->r); if (n->op == '+') return l+r; if (n->op == '-') return l-r; if (n->op == '*') return l*r; return l/r;}class Parser { vector<Token> t; int pos = 0; Token peek() { return t[pos]; } Token consume() { return t[pos++]; } Node* expr(); Node* term(); Node* factor();public: Parser(vector<Token> tok) : t(tok) {} int run() { return eval(expr()); }};Node* Parser::expr() { Node* left = term(); while (peek().type == Token::OP && (peek().op == '+' || peek().op == '-')) { char op = consume().op; left = new Node(op, left, term()); } return left;}Node* Parser::term() { Node* left = factor(); while (peek().type == Token::OP && (peek().op == '*' || peek().op == '/')) { char op = consume().op; left = new Node(op, left, factor()); } return left;}Node* Parser::factor() { if (peek().type == Token::NUM) return new Node(consume().val); consume(); Node* n = expr(); consume(); return n;}int main() { string s; getline(cin, s); cout << Parser(tokenize(s)).run() << "\n";}
# Recursive descent expression parserimport sysdef tokenize(s): tokens, i = [], 0 while i < len(s): if s[i] == ' ': i += 1; continue if s[i].isdigit(): j = i while j < len(s) and s[j].isdigit(): j += 1 tokens.append(('NUM', int(s[i:j]))); i = j else: tokens.append(('OP', s[i])); i += 1 return tokensclass Parser: def __init__(self, tok): self.tok = tok; self.pos = 0 def peek(self): return self.tok[self.pos] if self.pos < len(self.tok) else None def consume(self): t = self.tok[self.pos]; self.pos += 1; return t def parse_expr(self): left = self.parse_term() while self.peek() and self.peek()[1] in '+-': op = self.consume()[1]; right = self.parse_term() left = (op, left, right) return left def parse_term(self): left = self.parse_factor() while self.peek() and self.peek()[1] in '*/': op = self.consume()[1]; right = self.parse_factor() left = (op, left, right) return left def parse_factor(self): t = self.peek() if t[0] == 'NUM': return ('NUM', self.consume()[1]) self.consume(); node = self.parse_expr(); self.consume(); return nodedef eval_ast(n): if n[0] == 'NUM': return n[1] l, r = eval_ast(n[1]), eval_ast(n[2]) if n[0] == '+': return l+r; if n[0] == '-': return l-r if n[0] == '*': return l*r; return l//rs = sys.stdin.readline().strip()print(eval_ast(Parser(tokenize(s)).parse_expr()))
stdin
3 + 5 * (2 - 1)
결과
8
stdin
10 / 2 + 3 * 4
결과
17
복잡도
항목
값
Tokenization
O(L) 시간, O(L) 공간
Recursive Descent
O(L) 시간, O(D) 호출 스택 (D = nesting depth)
전체
O(L) 시간, O(L) 공간
변형 / 활용
1. Pratt Parsing
Recursive descent 일반화. 각 토큰에 binding power (precedence) 할당. 연산자 추가가 쉽고 코드 중복 적음.
2. Shunting Yard (Dijkstra)
중위 → 후위 표기법 변환. 연산자 스택만 사용. Recursive descent 없이 O(L).
3. PEG / Packrat
Parsing Expression Grammar. | 에 순서가 있어 모호성 해결. Memoization 으로 O(L) 보장.
4. LR / LALR (Yacc, Bison)
Bottom-up. 더 넓은 문법 클래스 허용. 대규모 언어 (C, Java) 에 사용.
함정
1. 좌측 재귀 (Left Recursion)
expr → expr '+' term 은 recursive descent 에서 무한 재귀. expr → term ('+' term)* 로 변환 필요.
2. 연산자 우선순위 누락
expr/term/factor 분리 안 하면 3 + 5 * 2 를 (3+5)*2 = 16 으로 계산.
💬 댓글