String Parsing & Expression Evaluation
Read text the way a compiler does: one cursor, a few states, and a stack when things nest.
▶ guided lesson⚡ 1-minute summary↻ 14 recall cards
before this: Java Arrays & Strings · Math & Numbers · Two Pointers · Stacks · Recursion
01 · why it exists
Why this exists, and the weight it carries
since1951–1959
Finite-state machine (a scanner with states)
Stephen C. Kleene; Michael O. Rabin & Dana Scott
In a 1951 RAND memo, logician Stephen Kleene studied which patterns of input a machine with a fixed, finite memory can recognise. His "regular events" are the idea behind today's regular expressions; the memo was printed in Automata Studies in 1956. In 1959 Michael Rabin and Dana Scott published "Finite Automata and Their Decision Problems": a finite set of states, and a rule that picks the next state from the current state and the next symbol. The paper also introduced machines that may guess — nondeterministic automata. It won them the 1976 Turing Award. In 1968 Ken Thompson published a way to compile a regular expression into machine code for fast text search.
the problem it solvedDeciding, one character at a time and with a fixed amount of memory, whether input matches a pattern — and splitting it into pieces along the way.
its jobs today
- Lexers: compilers, JSON readers and CSV parsers first cut text into tokens with a small state machine.
- Regular expressions: compile the pattern once (
Pattern.compilein Java), then match text against it — classic engines turn the pattern into a state machine. - App flows with legal moves only: an order screen goes placed → preparing → out for delivery → delivered, never backwards.
the weight it carries
Illustrative: a 1 MB JSON response, 10⁶ characters. A scanner with states reads each character once: 10⁶ steps and O(1) extra memory. A scanner that backs up and re-reads the current token at every character needs up to n²/2 = 5·10¹¹ steps on one long token.
without it
Parsers would become deep if-else ladders that accept bad input like "+-12" or "1 2" — or re-read text again and again.
when · why · how
- when
- Text arrives character by character and a character's meaning depends on what came before it: a sign only before digits, digits after a '.', a quote after a backslash.
- why
- O(1) work per character → O(n) time and O(1) extra space, and each state has a small set of legal next characters that you can check one by one.
- how
- An
int stateplus aswitch, or a tableint[][] NEXT = new int[states][charClasses]; read withs.charAt(i)in one forward pass.
sinceAncient Rome · print-era Europe
Greedy value table (Roman numerals and other number systems)
No single inventor — a convention that settled over centuries
Romans wrote numbers mostly by adding symbols, and they were not consistent: IIII for 4 is common, and the Colosseum's gate numbers mix IIII with XL. According to MathWorld, the subtractive form (IV, IX, …) became popular in Europe only after printing arrived. There has never been one official standard. The strict modern rule — only the six pairs IV, IX, XL, XC, CD, CM — is what LeetCode and Excel's ROMAN() function write by default. Excel's ROMAN even has a 'form' argument for shorter, non-standard spellings: 499 can be CDXCIX or simply ID.
the problem it solvedWriting any number with a handful of symbols — and, for software, converting both ways with one fixed table.
its jobs today
- Amount in words on cheques and invoices: ₹1,23,45,678 → 1 crore, 23 lakh, 45 thousand, 6 hundred, 78 — a table of place values, biggest first.
- Spreadsheet functions such as Excel's ROMAN() and ARABIC(), and i, ii, iii page numbers in document tools.
- Durations: 100,000 seconds = 1 day, 3 hours, 46 minutes, 40 seconds.
the weight it carries
Illustrative: an invoice run printing 10 lakh amounts in words. Each amount walks a 5-row table (crore, lakh, thousand, hundred, units): about 5 divisions each, 5·10⁶ for the whole batch. For Roman numerals the worst case is 13 rows + 15 symbols = 28 steps. The table, not the size of the number, decides the work.
without it
You'd hand-code a branch for every awkward case (4, 9, 40, 90, …) and it is easy to miss one.
when · why · how
- when
- A number must become units from a fixed list: Roman letters, lakh/crore words, h:m:s, note denominations.
- why
- O(table size + output) per number — O(1) for a fixed table. It is only correct when the table is canonical (the biggest piece that fits never blocks the right answer); for odd coin sets greedy fails — see Coin Change.
- how
- Two parallel arrays, biggest first (
int[] VALUES = {1000, 900, …}; String[] SYMBOLS = {"M", "CM", …}) andwhile (num >= VALUES[k]), orTreeMap.floorEntry(num).
since1961
Two stacks for precedence (Dijkstra's shunting-yard)
Edsger W. Dijkstra (Mathematisch Centrum, Amsterdam)
While building an ALGOL 60 translator for the Electrologica X1 computer, Dijkstra described how to turn formulas into machine order in one left-to-right pass. He explained it with a picture: "The translation process shows much resemblance to shunting at a three way railroad junction." Numbers run straight through; operators wait on a side track — a stack — and leave while their priority is greater than or equal to the new operator's. The method appeared in his November 1961 Mathematisch Centrum report on ALGOL 60 translation (that part was first published in the APIC-Bulletin in May 1961). It builds on the stack idea of Bauer and Samelson (Stacks chapter). The railway picture gave the method its name: the shunting-yard algorithm.
the problem it solvedEvaluating — or compiling — infix formulas with precedence and brackets in a single left-to-right pass.
its jobs today
- Formula engines in calculators and spreadsheets turn
=A1+B1*2into steps they can run. - Compilers: javac parses binary operators like
a + b * cwith an operand stack and an operator stack. - Infix → postfix conversion, so a stack machine (like the JVM's operand stack) can run the result.
the weight it carries
Illustrative: a formula of 10⁵ tokens. Shunting-yard pushes and pops each token at most once — about 2·10⁵ stack operations. Repeatedly scanning a list for the highest-precedence operator and collapsing it: about 5·10⁴ rounds, each over a list that starts at 10⁵ tokens and shrinks by 2 — about 2.5·10⁹ steps.
without it
Precedence would need repeated scans or a full syntax tree for every formula — and plain left-to-right evaluation gives 2 + 3 × 4 = 20 instead of 14.
when · why · how
- when
- Infix with precedence: + − * /, maybe ^ and brackets; or converting infix to postfix (RPN).
- why
- O(n) time and O(n) space for the two stacks. Each operator waits exactly until something of lower or equal precedence, a ')' or the end arrives.
- how
Deque<Integer> numsandDeque<Character> ops. Before pushing an operator, pop and apply every operator on top with precedence ≥ it (stop at '('). ')' applies back to '('. At the end, apply the rest.
since1961
Recursive descent (one method per grammar rule)
Peter Lucas (Vienna; his group became IBM's Vienna lab); Ned Irons is also credited
Around 1961, the people writing the first ALGOL 60 compilers found that a grammar can become code directly: one procedure per rule, calling each other — and themselves — exactly as the rules nest. Peter Lucas, of Heinz Zemanek's Vienna group (it became IBM's Vienna lab in late 1961), is usually named as the first to describe it ("The Structure of Formula-Translators", 1961). Jeffrey Kegler's parsing timeline counts Ned Irons's 1961 paper "A syntax directed compiler for ALGOL 60" as an equally early description. Niklaus Wirth helped make it the usual way to teach compilers: the PL/0 compiler in his book Algorithms + Data Structures = Programs (1976) is a recursive-descent parser. The JDK's own source calls javac "a hand-written recursive-descent parser that implements the grammar described in the Java Language Specification".
the problem it solvedTurning a grammar into a working parser by hand, with code that mirrors the rules so it is easy to check.
its jobs today
- javac and many hand-written parsers for JSON, config files and small query languages: each grammar rule is a method.
- Interview calculators with brackets:
expr()handles + −,term()handles * /,factor()reads a number or a bracketedexpr().
the weight it carries
Illustrative: an expression nested 1,000 brackets deep. expr → term → factor uses about 3 calls per level: about 3,000 stack frames — fine on a desktop JVM's default stack, risky in small stacks such as the in-browser runner. Both this and the two-stack method read each of the n characters once: O(n).
without it
Every new operator or rule would mean patching a precedence table by hand, and the parser would stop reading like the language it parses.
when · why · how
- when
- The input follows a grammar: nested brackets, several precedence levels, unary minus, function calls — or you want to explain your parser rule by rule.
- why
- O(n) time, each character read once; O(depth) call-stack space. Precedence is simply which method calls which.
- how
- A field
int ias the cursor and methodsexpr(),term(),factor():exprloops over + and − callingterm,termloops over * and / callingfactor,factorreads a number,'-' factoror'(' expr ')'.
since1997 (netstrings) — the idea is older
Length-prefixed framing
No single inventor — Pascal dialects, ASN.1 (ITU-T X.690), D. J. Bernstein's netstrings
Pascal dialects such as Turbo Pascal stored a string's length in its first byte (index 0), so a string could hold any character — but at most 255 of them.
Binary formats use the same idea. ASN.1's encoding rules (ITU-T X.690) write every value as identifier, length, contents — informally "tag-length-value" (TLV).
In 1997 D. J. Bernstein proposed netstrings for network protocols: "hello world!" travels as 12:hello world!,.
Today Protocol Buffers store each string as a LEN record (a varint length, then the bytes), and Java's DataOutputStream.writeUTF writes a 2-byte length before every string.
the problem it solvedSending several values through one stream so the reader knows exactly where each one ends — even when a value contains your separator.
its jobs today
- Network and file formats: Protocol Buffers strings, HTTP's Content-Length header, records in binary files.
- Java's
DataOutputStream.writeUTF/readUTF, and many message-queue and RPC protocols that send a stream of records over one connection.
the weight it carries
Illustrative, at LeetCode 271's limits: 200 strings × 200 chars = 40,000 characters of data. Headers like "200#" add at most 4 chars each — 800 more, 2%. The decoder examines only those header characters; the 40,000 data characters are copied but never inspected. Escaping must inspect all 40,000 and double every '#'.
without it
You'd pick a delimiter and hope it never appears — the classic bug where a comma inside a name breaks a CSV row.
when · why · how
- when
- Many values must travel as one string or byte stream (a list, a message, a file record) and the values may contain any character.
- why
- Encoding and decoding are O(total length), nothing needs escaping, and the reader jumps over each payload instead of scanning it.
- how
sb.append(s.length()).append('#').append(s); decode withindexOf('#', i),Integer.parseInt, andsubstring(hash + 1, hash + 1 + len). For bytes,DataOutputStream.writeUTF.
since1977–1981
Greedy line filling (and Knuth–Plass, its optimal cousin)
Donald E. Knuth & Michael F. Plass
Filling a line with as many words as fit, then starting the next, is the obvious method. The CSS Text specification notes that web browsers have so far used this kind of first-fit, greedy line breaking.
For TeX, Knuth and Michael Plass designed something better (devised in 1977, generalised in 1980): consider the whole paragraph, give every possible break a cost in "demerits", and choose the breaks with the fewest total demerits — a shortest path through the candidate breaks, found with dynamic programming.
They published it as "Breaking Paragraphs into Lines" (Software: Practice and Experience, 1981), and TeX still uses it.
CSS Text Level 4 now has text-wrap-style: pretty, which asks a browser to consider several lines at once.
the problem it solvedBreaking a paragraph into lines that look even, instead of letting one bad early break spoil the lines after it.
its jobs today
- Word wrap in editors, terminals and browsers, where fast greedy first-fit is the default.
- Fixed-width output: receipt printers, terminal tables and plain-text reports pad every line to the same width.
the weight it carries
Illustrative, at LeetCode 68's limits: 300 words, width 100. Greedy tests each word once to see if it fits — about 300 checks — and writes the output once. Knuth–Plass weighs many candidate breaks per word: if up to 50 words can share a line (one-letter words in 100 columns), that is up to 300 × 50 = 15,000 cost checks — more work, for evener lines.
without it
Lines would overflow or be cut mid-word; and without the greedy shortcut every reflow while you type would cost a whole-paragraph optimisation.
when · why · how
- when
- Pack items, in order, into fixed-size rows: words into lines, messages into packets — when you may not reorder them.
- why
- Greedy is O(n) and gives the fewest lines; it does not give the evenest lines — that needs Knuth–Plass (dynamic programming).
- how
- Word j fits if
letters + words[j].length() + (j - i) <= maxWidth. Thenspaces / gapsandspaces % gapsgive every gap's width.
02 · plain english
The idea in 30 seconds
Parsing means walking the text once with an index and deciding what each character means from what came before it.
Small inputs need a few states (spaces → sign → digits). Formulas need precedence: two stacks, or one method per grammar rule.
When many strings must travel as one, write each one's length in front of it. Then no character inside can fool the reader.
picture this
Type =2+3*4 into an Excel cell and it shows 14, not 20: * binds tighter than +. To get that right while reading left to right, a parser must hold the + aside until the * is done. Every parser in this chapter works like that: one cursor, a little memory of what is waiting, and rules for when it may run.
03 · remember
What you must know
- One cursor, one pass. Keep an index
i, reads.charAt(i), move forward. Read whole tokens: a number isnum = num * 10 + (c - '0')in a loop, not one digit. - States. When a character's meaning depends on what came before it (a sign only before digits), you are writing a finite-state machine — as if-statements or as a table.
- Overflow before it happens:
if (result > (Integer.MAX_VALUE - d) / 10)clamp; only thenresult = result * 10 + d. - Greedy value tables turn numbers into symbols: Roman numerals, lakh/crore words, h/m/s. Take the biggest value that fits, repeat.
- Precedence with two stacks (Dijkstra's shunting-yard): an operator waits on a stack until an operator of lower or equal precedence, or a
), arrives. Or use recursive descent:expr → term → factor. - No brackets, no stack: for
+ − * /,resultholds the finished terms andlastholds the term that*and/can still change. - In-place text tricks: reverse everything, then reverse each piece back (words here, the two parts of a rotation in Rotate Array).
- Framing: to pack strings into one, write
length#string. The reader jumps by the length, so a separator inside the data is harmless. - Greedy line filling gives the fewest lines. Spacing is arithmetic:
spaces / gapseach, and the firstspaces % gapsgaps get one more.
04 · see it
Play with it in 3D
Press Space to play, ←/→ to step, drag to move, ? for all shortcuts. Try your own input.
atoi as a state machine
Spaces, one sign, digits, stop. The guard runs before every append, so the int never overflows.
A greedy value table
13 rows, biggest first. The six subtractive pairs live in the table, so 'take the biggest that fits' always writes a standard numeral.
Precedence with two stacks
Values wait in nums, operators in ops. An operator runs only when something of lower or equal precedence, a ')' or the end arrives.
Length-prefixed framing
Encode writes len#text for each string; decode reads the header, then jumps over exactly len characters without looking at them.
Greedy line filling
Take words while they fit, then share the spare spaces with / and %. Every row of the grid is exactly maxWidth wide.
05 · cost
Time & space, explained
| Brute force | Optimal time | Optimal extra space | why | |
|---|---|---|---|---|
| String to Integer (atoi) | O(n), copies the digits | O(n) | O(1) | Each character is read once. The guard replaces a long or BigInteger, so nothing is copied. |
| Roman to Integer | O(n), six rewrites | O(n), n ≤ 15 | O(1) | Peek one symbol ahead and add or subtract; the brute force builds six new strings first. |
| Integer to Roman | O(1), additive + six rewrites | O(1) | O(1) | At most 13 table rows and 15 symbols for any value 1..3999. |
| Reverse Words in a String | O(n²), prepending | O(n) | O(1) beyond the char[] | Prepending copies the whole answer per word; reversing touches each character about twice. |
| Basic Calculator II | O(n²), collapsing a list | O(n) | O(1) | ArrayList removals shift the tail; result + last never looks back. |
| Basic Calculator | O(n²), rewriting brackets | O(n) | O(depth) | Each rewrite rebuilds the string; the sign stack holds one pair per open bracket. |
| Text Justification (L lines, width W) | O(L·W²), inserting spaces | O(L·W) | O(W) per line | StringBuilder.insert shifts the line every time; computing each gap's width writes each character once. |
| Encode and Decode Strings (N total chars) | O(N), escaping | O(N) | O(N), the output | Both are linear, but escaping inspects every character; length prefixes jump over the payload. |
| Two stacks (shunting-yard) | Recursive descent | why | |
|---|---|---|---|
| Time | O(n) | O(n) | Each token is pushed and popped at most once, or read once by exactly one method. |
| Extra space | O(n) on the heap | O(depth) on the call stack | Very deep nesting is safe in a Deque; recursion can throw StackOverflowError. |
| Unary minus | push 0 when no value comes before '-' | factor := '-' factor | Both turn -x into 0 − x (or −x) without special cases elsewhere. |
| A new precedence level (e.g. ^) | one more case in prec() | one more method in the chain | Both are small changes; the recursive version reads like the grammar. |
06 · java
Raw vs Collections Framework
Scanning: a cursor vs split
/** "12+ 345*(6-7)" → [12, +, 345, *, (, 6, -, 7, )]. One index walks the string once. */static List<String> tokens(String s) { List<String> out = new ArrayList<>(); int i = 0; while (i < s.length()) { char c = s.charAt(i); if (c == ' ') { i++; // spaces only separate tokens } else if (c >= '0' && c <= '9') { int start = i; while (i < s.length() && s.charAt(i) >= '0' && s.charAt(i) <= '9') i++; // a whole number out.add(s.substring(start, i)); } else { out.add(String.valueOf(c)); // an operator or a bracket is one char i++; } } return out;}/** Works only when every token is separated by spaces. */static List<String> splitTokens(String s) { String trimmed = s.trim(); if (trimmed.isEmpty()) return List.of(); // "".split(...) would give [""] return Arrays.asList(trimmed.split("\\s+")); // regex: one or more whitespace chars}- The cursor version reads a whole number with an inner
whileand never needs separators:12+ 345*(6-7)works. split("\\s+")only works when every token is separated by spaces:12+345*6comes back as one token.splittakes a regex and makes a new String per piece. The split traps (split("."), trailing empty strings) are in the Java Arrays & Strings chapter.- Test digits with
c >= '0' && c <= '9'.Character.isDigitalso says yes to other scripts' digits, like Devanagari '५'.
Reading a number: your own loop vs Integer.parseInt
class Solution { public int myAtoi(String s) { int i = 0, n = s.length(); while (i < n && s.charAt(i) == ' ') i++; // state 1: skip leading spaces int sign = 1; if (i < n && (s.charAt(i) == '+' || s.charAt(i) == '-')) { // state 2: at most one sign sign = s.charAt(i) == '-' ? -1 : 1; i++; } int result = 0; while (i < n && s.charAt(i) >= '0' && s.charAt(i) <= '9') { // state 3: digits int d = s.charAt(i) - '0'; if (result > (Integer.MAX_VALUE - d) / 10) { // result * 10 + d would pass 2^31 - 1 return sign == 1 ? Integer.MAX_VALUE : Integer.MIN_VALUE; } result = result * 10 + d; i++; } return sign * result; // any other character ends the number }}/** Integer.parseInt is strict: no spaces, no junk, no overflow. It throws instead of guessing. */static Integer tryParse(String s) { try { return Integer.parseInt(s); // accepts "+42" and "-42" } catch (NumberFormatException e) { // " 42", "42abc", "2147483648", "" return null; }}result > (Integer.MAX_VALUE - d) / 10is exactly the condition forresult * 10 + d > 2³¹ − 1, checked before anything can wrap.Integer.parseIntis strict: a space, a letter or an out-of-range value throwsNumberFormatException. It accepts a leading '+' (since Java 7) or '-'.- Because it reads digits with
Character.digit,Integer.parseInt("१२")returns 12 — your own loop decides what counts as a digit. Math.multiplyExact/Math.addExactare another option: they throwArithmeticExceptionon overflow instead of wrapping.
Precedence: two stacks vs recursive descent
/** + - * / and brackets on non-negative ints; a '-' at the start or right after '(' is unary. */static int evaluate(String s) { Deque<Integer> nums = new ArrayDeque<>(); // values waiting for an operator Deque<Character> ops = new ArrayDeque<>(); // operators waiting for their right operand boolean expectValue = true; // true at the start and right after '(' for (int i = 0; i < s.length(); i++) { char c = s.charAt(i); if (c == ' ') continue; if (c >= '0' && c <= '9') { int num = 0; while (i < s.length() && s.charAt(i) >= '0' && s.charAt(i) <= '9') num = num * 10 + (s.charAt(i++) - '0'); i--; // the for loop will step past the last digit nums.push(num); expectValue = false; } else if (c == '(') { ops.push(c); // a wall: nothing inside may reach past it expectValue = true; } else if (c == ')') { while (ops.peek() != '(') apply(nums, ops.pop()); // finish the group ops.pop(); // drop the '(' itself expectValue = false; } else { if (expectValue) nums.push(0); // unary minus: "-x" is "0 - x" // Before c waits, run every operator on the stack that binds at least as tightly. while (!ops.isEmpty() && ops.peek() != '(' && prec(ops.peek()) >= prec(c)) { apply(nums, ops.pop()); } ops.push(c); expectValue = true; } } while (!ops.isEmpty()) apply(nums, ops.pop()); return nums.pop();} static int prec(char op) { return op == '*' || op == '/' ? 2 : 1;} static void apply(Deque<Integer> nums, char op) { int b = nums.pop(), a = nums.pop(); // the right operand comes off first nums.push(switch (op) { case '+' -> a + b; case '-' -> a - b; case '*' -> a * b; default -> a / b; });}/** One method per grammar rule. The cursor `i` only moves forward. */class Parser { private final String s; private int i; Parser(String s) { this.s = s.replace(" ", ""); } int parse() { return expr(); } // expr := term (('+' | '-') term)* private int expr() { int value = term(); while (i < s.length() && (s.charAt(i) == '+' || s.charAt(i) == '-')) { char op = s.charAt(i++); int right = term(); value = op == '+' ? value + right : value - right; } return value; } // term := factor (('*' | '/') factor)* ← lower in the grammar = binds tighter private int term() { int value = factor(); while (i < s.length() && (s.charAt(i) == '*' || s.charAt(i) == '/')) { char op = s.charAt(i++); int right = factor(); value = op == '*' ? value * right : value / right; } return value; } // factor := number | '(' expr ')' | '-' factor private int factor() { char c = s.charAt(i); if (c == '-') { i++; return -factor(); } if (c == '(') { i++; // skip '(' int value = expr(); // the whole grammar again, one level deeper i++; // skip ')' return value; } int num = 0; while (i < s.length() && s.charAt(i) >= '0' && s.charAt(i) <= '9') num = num * 10 + (s.charAt(i++) - '0'); return num; }}- Two stacks: before pushing an operator, apply every waiting operator with precedence ≥ it. The
=keeps8 − 3 − 2left to right. (is a wall on the ops stack;)applies operators back to it. At the end, apply whatever is left.- Recursive descent: each grammar rule is a method. Lower rules bind tighter —
termhandles*and/, so it runs insideexpr's+and−. - Use
Deque<Integer>/Deque<Character>(ArrayDeque), never the legacyStackclass — see the Stacks chapter.
Number systems: a greedy value table
class Solution { // The 7 symbols plus the 6 subtractive pairs, biggest first. private static final int[] VALUES = {1000, 900, 500, 400, 100, 90, 50, 40, 10, 9, 5, 4, 1}; private static final String[] SYMBOLS = {"M", "CM", "D", "CD", "C", "XC", "L", "XL", "X", "IX", "V", "IV", "I"}; public String intToRoman(int num) { StringBuilder sb = new StringBuilder(); for (int k = 0; k < VALUES.length; k++) { while (num >= VALUES[k]) { // the biggest value that still fits sb.append(SYMBOLS[k]); num -= VALUES[k]; } } return sb.toString(); }}static final TreeMap<Integer, String> ROMAN = new TreeMap<>(Map.ofEntries( Map.entry(1000, "M"), Map.entry(900, "CM"), Map.entry(500, "D"), Map.entry(400, "CD"), Map.entry(100, "C"), Map.entry(90, "XC"), Map.entry(50, "L"), Map.entry(40, "XL"), Map.entry(10, "X"), Map.entry(9, "IX"), Map.entry(5, "V"), Map.entry(4, "IV"), Map.entry(1, "I"))); static String toRoman(int num) { StringBuilder sb = new StringBuilder(); while (num > 0) { Map.Entry<Integer, String> e = ROMAN.floorEntry(num); // the biggest key <= num, in O(log k) sb.append(e.getValue()); num -= e.getKey(); } return sb.toString();}- Order matters: the table must go from the biggest value to the smallest.
floorEntry(num)returns the entry with the largest key ≤ num in O(log k) — a greedy table lookup in one call.new TreeMap<>(Map.ofEntries(…))sorts the table by key once. The arrays version is faster and is what interviewers expect.
Framing: your own length prefix vs DataOutputStream.writeUTF
class Codec { /** ["hi", "", "a#3"] → "2#hi0#3#a#3". Each string travels with its own length in front. */ public String encode(List<String> strs) { StringBuilder sb = new StringBuilder(); for (String s : strs) { sb.append(s.length()).append('#').append(s); // header "len#", then the raw text } return sb.toString(); } public List<String> decode(String s) { List<String> out = new ArrayList<>(); int i = 0; // always the start of a header while (i < s.length()) { int hash = s.indexOf('#', i); // a header is digits then '#': the first '#' ends it int len = Integer.parseInt(s.substring(i, hash)); out.add(s.substring(hash + 1, hash + 1 + len)); // take exactly len chars, '#' or not i = hash + 1 + len; // jump straight to the next header } return out; }}/** Java's own length-prefixed strings: writeUTF puts a 2-byte length in front of every string. */static byte[] writeAll(List<String> strs) throws IOException { ByteArrayOutputStream bytes = new ByteArrayOutputStream(); try (DataOutputStream out = new DataOutputStream(bytes)) { out.writeInt(strs.size()); // how many strings follow for (String s : strs) out.writeUTF(s); // [2-byte length][that many bytes] } return bytes.toByteArray();} static List<String> readAll(byte[] data) throws IOException { try (DataInputStream in = new DataInputStream(new ByteArrayInputStream(data))) { int n = in.readInt(); List<String> out = new ArrayList<>(n); for (int k = 0; k < n; k++) out.add(in.readUTF()); // reads the length, then exactly that much return out; }}- Decode finds the header with
indexOf('#', i): headers are only digits, so the first#afterimust end the header. writeUTFwrites two bytes of length, then the string in modified UTF-8. The length counts bytes: '₹' takes 3.- Two bytes can count up to 65,535, so a string whose encoding needs more bytes than that makes
writeUTFthrowUTFDataFormatException. try (DataOutputStream out = …)closes the stream for you, even if a write throws.
| call | does | cost |
|---|---|---|
s.charAt(i) | Read one char — the scanner's basic move | O(1) |
c >= '0' && c <= '9' · c - '0' | ASCII digit test, and its value 0–9 | O(1) |
Character.isDigit(c) / isLetter(c) | Unicode-aware tests (also true for '५') | O(1) |
Integer.parseInt(s) / Long.parseLong(s) | Strict parse; throws NumberFormatException on spaces, junk or overflow | O(len) |
Math.addExact(a, b) / multiplyExact(a, b) | Throw ArithmeticException on overflow instead of wrapping | O(1) |
s.indexOf(ch, from) | First index of ch at or after from, or −1 | O(n) |
s.substring(a, b) | A new String with chars a..b−1 | O(b − a) |
s.trim().split("\\s+") | Words separated by runs of whitespace (trim first) | O(n) |
String.join(" ", words) | Join with exactly one separator | O(total length) |
" ".repeat(k) | k spaces (Java 11+) | O(k) |
sb.append(x) / sb.insert(i, c) | Append at the end / insert in the middle (shifts the rest) | O(1) amortized / O(n) |
new String(chars, 0, n) | Build a String from the first n chars of a char[] | O(n) |
map.floorEntry(k) | TreeMap: the entry with the largest key ≤ k | O(log size) |
out.writeUTF(s) / in.readUTF() | 2-byte length + modified UTF-8 bytes, and back | O(len) |
Numbers and operators from an expression
→ Index + while loop over digits
Handles 12+3 with no spaces; split can't.
Words separated by any number of spaces
→ s.trim().split("\\s+")
Short and clear when the pieces are all the same kind; trim first or you get an empty first word.
Text you trust to be a valid int
→ Integer.parseInt
Strict and fast; catch NumberFormatException where the text enters your program.
A forgiving atoi-style parse that clamps
→ Your own loop + overflow guard
parseInt won't skip leading spaces or stop at junk, and it throws instead of clamping.
Infix with precedence and brackets
→ Two stacks, or expr / term / factor
Both O(n). Recursion reads like the grammar; stacks never overflow the call stack.
+ − * / without brackets
→ result + last (no stack)
Only the current term can still change, so one variable replaces the stack.
Many strings inside one string or byte stream
→ Length prefix (len#s) or writeUTF
No escaping, and any character is safe.
A number in units from a fixed table
→ Parallel arrays biggest-first, or TreeMap.floorEntry
O(table size) per number, and adding a unit is one more row.
07 · recognise
Spot the pattern
"Implement atoi", "parse this number", "is this a valid number"
A small state machine: spaces → sign → digits (→ '.' → exponent), with an overflow guard
"Evaluate the expression" with + − * / and maybe brackets
Two stacks (shunting-yard), result + last when there are no brackets, or recursive descent
Only + and − with brackets and a unary minus
Running total + a stack of (result, sign) pairs
Roman numerals, "amount in words", seconds → h:m:s
Greedy value table, biggest first
"Reverse the words" or "rotate" text in place
Reverse everything, then reverse each piece back
"Exactly maxWidth characters per line", word wrap
Greedy line filling, then spaces / gaps and spaces % gaps
"Encode a list of strings into one string", "serialize"
Length-prefix framing (len#s), never a bare delimiter
Something like k[...] or (...) that nests
A stack of saved contexts, or recursion with a shared index (Stacks chapter)
08 · traps & tricks
Mistakes to avoid, tricks to use
traps
- Reading one digit at a time:
12+3becomes 1, 2, +, 3. Loop over all the digits of a number. - Checking overflow after
result * 10 + d: by then the int has already wrapped round. Character.isDigitaccepts '५' and other Unicode digits, and thenc - '0'is garbage. Compare with '0' and '9'.charAt(i)without checkingi < nfirst: a lone "-" or a number at the very end throws StringIndexOutOfBoundsException.- Forgetting the last number: in a one-pass calculator nothing comes after it, so commit at
i == n − 1or after the loop. - Popping only operators of higher precedence:
8 − 3 − 2then becomes 8 − (3 − 2) = 7. Equal precedence must pop too. s.split(" ")on text with repeated spaces returns empty strings; a leading space gives an empty first word.- Joining strings with a delimiter you hope never appears: some input always contains it.
tricks
- Before coding, write the states or the grammar in 3 lines. The code is then a translation, and the interviewer can follow it.
- Keep one cursor
ithat only moves forward. If a helper reads a number, let it advancei(or return where it stopped). - Test the boring inputs aloud: empty, only spaces, only a sign, a number at the very end,
2147483647and2147483648. - For calculators, say the rule out loud: "
*and/change the current term;+and−start a new one." - In an interview, ask before you code: unary minus? spaces? brackets? truncating division? clamp or throw on overflow?
- Chalo, dry-run karte hain: trace
3+5*2-8/3in the result + last sim before writing a single line.
09 · why & how
Questions you might be too shy to ask
Why not just use split everywhere?
split is great when every piece is separated the same way and you treat them all alike, like words in a sentence. It can't tokenise 12+3*4 (there are no separators), it makes a new String per piece, and its argument is a regex — in a regex . means "any character", so every character becomes a separator and "a.b".split(".") returns an empty array (you need split("\\.")). A cursor loop is O(n), allocates nothing extra and follows any rule you need.
How does num = num * 10 + (c - '0') turn "345" into the number 345?
A char is stored as a number: '0' is 48, '1' is 49, … '9' is 57. So '3' - '0' is 51 − 48 = 3, the digit's value — while (int) '3' would give 51. Then each new digit pushes the old ones one place left: 0 → 0 × 10 + 3 = 3 → 3 × 10 + 4 = 34 → 34 × 10 + 5 = 345. Simple hai: multiply by 10 to make room, then add the new digit.
Why must i < n come first in while (i < n && s.charAt(i) == ' ')?
&& stops as soon as its left side is false, so s.charAt(i) never runs once i == n. Swap the two sides and, at the end of the text, the loop reads s.charAt(n), which throws StringIndexOutOfBoundsException. Inputs like " " (only spaces) or "-" (a sign, then nothing) hit this straight away. Rule: bounds check first, then look at the character.
Should I read with s.charAt(i), or convert with toCharArray() first?
For reading, charAt(i) is enough: it is O(1) and copies nothing. toCharArray() makes an O(n) copy, which you need only when you must change characters — like Reverse Words, because a String can't be changed. So: just reading → charAt; swapping or overwriting → char[].
Why does LeetCode's atoi clamp instead of throwing like Integer.parseInt?
Its rules look like the C library's, and older versions of the problem said "similar to C/C++'s atoi". The closest match is C's strtol: it skips leading whitespace, reads an optional sign and digits, and on overflow returns LONG_MAX or LONG_MIN (and sets errno to ERANGE). Plain C atoi has undefined behaviour on overflow. Java's Integer.parseInt chose to be strict and throw. In an interview, ask which behaviour they want.
The atoi guard fires at 2147483648 — but −2147483648 is a valid int. Does the guard break it?
No. We build the digits as a positive number and apply the sign only at the end. For "-2147483648", the last digit 8 trips the guard (214748364 > (2147483647 − 8) / 10 = 214748363), and because the sign is negative we return Integer.MIN_VALUE. That is exactly −2147483648 — the right answer, just reached through the clamp. Dhyan do: that's why the guard never needs a long.
Two stacks or recursion for a calculator — which should I write?
The one you can explain without stopping. Recursive descent (expr → term → factor) reads like the grammar and handles unary minus naturally. Two stacks never risk a StackOverflowError on deep nesting. For LC 224 (only + − and brackets) the sign stack is shortest; for LC 227 (no brackets) result + last is shortest.
In Basic Calculator II, why does op start as '+', and why do we apply the old operator when we reach a new one?
When you reach an operator, the number just before it is finally complete — and the operator that belongs to that number is the one before it, saved in op. The first number has no operator in front, so we pretend it had a '+'. Dry-run "3+5*2": at +, num 3 with op + → last = 3. At *, num 5 with op + → result = 3, last = 5. At the last character, num 2 with op * → last = 10. Answer: result + last = 13.
Why is "14-3/2" 13 and not 12? How does Java divide negative numbers?
Java's integer / drops the fraction toward zero: 3 / 2 is 1 and -3 / 2 is −1, not −2. In the one-pass calculator the term is stored as last = -3, then last / 2 gives −1, and 14 + (−1) = 13 — exactly what LeetCode asks for. If you ever need rounding down instead, that is Math.floorDiv(-3, 2), which gives −2.
What is a state machine, really?
A fixed set of states plus a rule: (current state, next character) → next state. atoi has four: start, sign, digits, end. Interview answers usually write it as if-statements; the table form next[state][charClass] makes every case visible, which helps on harder ones like Valid Number (LC 65).
Is the greedy Roman conversion always right? Greedy fails for coin change.
For this table, yes: it lists all 13 building blocks of standard numerals (the pairs included), biggest first, so the biggest piece that fits is always the next piece of the answer. Greedy is not safe for every table: with coins {1, 3, 4}, greedy pays 6 as 4 + 1 + 1, but 3 + 3 uses fewer coins — see Coin Change in Dynamic Programming.
Why does Reverse Words need a char[] in Java?
Java Strings are immutable, so you can't swap characters inside one. toCharArray() gives a copy you can change — O(n) space. The follow-up "O(1) extra space" means "beyond that array" in Java; say so in the interview.
How is a length prefix different from escaping?
Escaping marks special characters inside the data (a real # becomes ##), so the reader must inspect every character. A length prefix says how many characters follow, so the reader jumps over the data without looking at it. Both are O(n); length prefixes are simpler, and they're what Protocol Buffers and DataOutputStream.writeUTF use.
In decode, why substring(hash + 1, hash + 1 + len)? I always get the + 1 wrong.
hash is the index of the '#', so the text starts one place later, at hash + 1. substring(a, b) takes b − a characters (the end index is excluded), so b = a + len takes exactly len. Example "5#Hello": hash = 1, and substring(2, 7) is "Hello". The next header starts at that same end index, hash + 1 + len = 7.
10 · practice
Interview problems
Every problem has brute → better → optimal solutions in Java, with complexity and a dry run.
- #13
- #12Integer to RomanMediumAdobeTCSWalmartMicrosoftAmazon+1
- #151
- #8
- #227
- #224
- #271
- #68Text JustificationHardAirbnbByteDance / TikTokUberAtlassianLinkedIn+1
Also practise this here
Each problem has one home page in the course; these use this chapter's idea too. The note says what to look for.
- #150Evaluate Reverse Polish NotationMediumin Stacks
Shunting-yard's output is postfix; this is the evaluator that runs it with one operand stack.
- #394Decode StringMediumin Stacks
Parsing with a cursor plus a stack of saved contexts: multi-digit counts and nested k[...] groups.
- #71Simplify PathMediumin Stacks
Tokenise on '/', then let '..' pop — a parser and a stack working together.
- #7
- #43Multiply StringsMediumin Math & Numbers
Arithmetic on digit strings, one character at a time — no parseInt allowed.
- #189
- #297Serialize and Deserialize Binary TreeHardin Traversals
Framing a whole tree into one string: separators plus null markers, read back with a cursor.
11 · revise
The whole chapter on one screen
10 cards, about a minute. Read them the night before an interview, or right after the chapter to lock it in.
- what it is
Parsing = one index walks the text once. Read a whole token (number, word, operator), decide what it means from what came before, move on.
- use it when
"Implement atoi", "evaluate this expression", "encode a list into one string", "justify text", "convert numerals" → a scanner, a few states, maybe a stack.
- template
while (i < n && s.charAt(i) >= '0' && s.charAt(i) <= '9') num = num * 10 + (s.charAt(i++) - '0');— read the whole number. - cost
Each character is read once → O(n). Stacks for brackets or precedence: O(n) space worst case; a recursive parser uses O(depth) call stack.
- trap
Overflow: check
result > (MAX - d) / 10beforeresult * 10 + d. After the multiply it has already wrapped. - trap
Character.isDigit('५')is true. In parsers testc >= '0' && c <= '9', or other scripts' digits slip in andc - '0'is garbage. - pro tip
Precedence with two stacks: before pushing an operator, apply every waiting one with precedence ≥ it. Or write
expr → term → factor. - pro tip
No brackets?
result= finished terms,last= the term*and/can still change. On + or −, banklast. - template
Framing:
sb.append(s.length()).append('#').append(s). The reader jumps by the length, so a#in the data never confuses it. - what it is
Greedy table (Roman, lakh/crore, h:m:s): biggest value that fits, repeat. Greedy lines: take words while they fit, then
spaces / gaps,spaces % gaps.
12 · recall
Flip, remember, repeat
atoi: in what order are the parts of the input read?
Question 1: atoi: in what order are the parts of the input read?
13 · check yourself
Quick quiz
What does atoi return for " -0012a42"?
result ≥ 0. Which check correctly stops overflow before `result = result * 10 + d`?
What is MCDXLIV?
Two-stack evaluation of 8 - 3 - 2. The second '-' arrives while the first is on the ops stack. What happens?
Basic Calculator II: what is "14-3/2"?
A 16-wide line holds "example", "of", "text" (13 letters, 2 gaps). The gap widths are…
Decode the length-prefixed string "3#a#b0#1##".
Reverse Words in place on "the sky": after reversing the whole char[] you have…
A recursive-descent parser (expr → term → factor) reads an expression nested 500 brackets deep. About how deep does the call stack go?
14 · learn more
A second teacher, if you want one
- ▶ videoL3. Prefix, Infix, and Postfix Conversion | Stack and Queue Playlist ↗Striver (take U forward) · EnglishConverting expressions between infix, prefix and postfix with a stack. Calculator and expression-parsing problems are built on this.
- ▶ playlistStrings : Popular Interview Problems ↗codestorywithMIK · HindiString problems like String Compression, Count and Say, Reverse Words and Text Justification, each solved intuition-first.