Algorithm Search⌘K

Proofs: Dijkstra's two-stack expression evaluation

These are the human proofs of Dijkstra's two-stack expression evaluation, at full length.

The two-stack algorithm returns the value of a fully parenthesized expression full

This proof shows: correctness.

Setting. The input is a token sequence of an expression EE of the grammar E→number∣( E  op  E )E \to \textit{number} \mid (\, E \;\textit{op}\; E \,). The value val(E)\text{val}(E) is the number for a number, and val(E1) op val(E2)\text{val}(E_1) \textit{ op } \text{val}(E_2) for (E1 op E2)(E_1 \textit{ op } E_2). We write a stack as a sequence from the bottom to the top, and V⋅xV \cdot x for VV with xx pushed. One step is one pass of the loop of lines L2 to L8 on one token.

Lemma. Let FF be an expression of the grammar, and suppose the loop is about to read the first token of FF with value stack VV and operator stack OO. Then after the loop has read all tokens of FF, and no others, the value stack is V⋅val(F)V \cdot \text{val}(F) and the operator stack is OO. No step on a token of FF pops from an empty stack, and no step reads or changes the entries of VV and OO.

Proof. By induction on the length of FF.

Case FF is a number xx. FF is one token. Line L8 pushes xx, so the stacks become V⋅xV \cdot x and OO, and x=val(F)x = \text{val}(F).

Case F=( F1 op F2 )F = (\, F_1 \textit{ op } F_2 \,). F1F_1 and F2F_2 are shorter than FF, so the induction hypothesis holds for them. The token ( is handled by line L3 and changes nothing. By the hypothesis for F1F_1, started on (V,O)(V, O), the stacks after F1F_1 are V⋅v1V \cdot v_1 and OO, where v1=val(F1)v_1 = \text{val}(F_1), and the steps on F1F_1 did not touch VV and OO. The token op\textit{op} is pushed by line L4: the stacks are V⋅v1V \cdot v_1 and O⋅opO \cdot \textit{op}. By the hypothesis for F2F_2, started on these stacks, the stacks after F2F_2 are V⋅v1⋅v2V \cdot v_1 \cdot v_2 and O⋅opO \cdot \textit{op}, with v2=val(F2)v_2 = \text{val}(F_2). The last token is ), handled by lines L6 and L7. Line L6 pops op\textit{op} from the operator stack, then b=v2b = v_2 and a=v1a = v_1 from the value stack; none of these stacks is empty. Line L7 pushes a op b=val(F1) op val(F2)=val(F)a \textit{ op } b = \text{val}(F_1) \textit{ op } \text{val}(F_2) = \text{val}(F). The stacks are V⋅val(F)V \cdot \text{val}(F) and OO. The pops removed only the entries pushed while reading FF, so the entries of VV and OO were not read or changed. □\square

Theorem. On the tokens of an expression EE, EVALUATE returns val(E)\text{val}(E), and when it returns, both stacks are empty.

Proof. Line L1 makes both stacks empty. The loop reads exactly the tokens of EE. By the lemma with VV and OO empty, after the loop the value stack holds the single entry val(E)\text{val}(E) and the operator stack is empty. Line L9 pops and returns val(E)\text{val}(E), which leaves the value stack empty. □\square

Without left parentheses. Line L3 does nothing on (, so deleting every ( from the input deletes only steps that do not change the state. The other steps see the same stacks as before, so the algorithm returns the same value on the input with all ( removed.

Running time. Each token causes one step of O(1)O(1) work, and each ) causes three pops and one push. So the time is Θ(n)\Theta(n). By the lemma, the stacks during the steps on FF hold at most the entries of the enclosing calls plus those of FF; by induction on the nesting depth dd of EE, the value stack never holds more than d+1d + 1 entries and the operator stack never more than dd. ■\blacksquare

Back to Dijkstra's two-stack expression evaluation