Valid Number asks you to decide whether a messy string is a well-formed number — integers, decimals, and scientific notation all count. The cleanest way to nail every edge case is to stop writing tangled if checks and instead build a finite automaton: a tiny state machine that reads the string one character at a time.
Problem. Given a string s, return True if it is a valid number. A valid number may have an
optional leading sign, digits, an optional decimal point with more digits, and an optional exponent
e/E followed by an optionally-signed integer.
Example: s = "3.5e-2" → True. Counter-examples: "e9", "1." followed by "e", and "abc" are
all False.
The slow way first
The tempting approach is a pile of boolean flags: seen_digit, seen_dot, seen_e, seen_sign, plus a thicket of if statements to decide when each is allowed. It technically works, but it is a bug factory — every new rule ("a dot is illegal after the e", "a sign only follows e or starts the string") adds another tangled condition, and it is nearly impossible to convince yourself you covered every case.
The question to ask: what context do I actually need while reading a character? Only one thing — what kind of thing have I parsed so far. That is a single value, not a bag of flags.
The idea: one state, one transition per character
Model the parser as a deterministic finite automaton (DFA). There is exactly one current state describing what we have read so far. For each character we look at the state and the character, and either move to a new state or reject. The states we need are:
START, thenINTEGER(the integer part),DOT,FRACTION(digits after the dot),EXP(just sawe),EXP_SIGN(a sign aftere), andEXPONENT(the exponent digits).
Three of these are accepting: INTEGER, FRACTION, and EXPONENT. The string is valid only if, after consuming every character, we land in one of those.
The key insight: the state carries all the context, so each character is a simple, local lookup. No flags, no second-guessing.
Walk through it
Step through the animation with s = "3.5e-2". The read pointer scans left to right and the state label updates on every character. 3 lands us in INTEGER, . moves to DOT, 5 to FRACTION, e to EXP, - to EXP_SIGN, and 2 to EXPONENT. The string ends in EXPONENT, an accepting state, so the answer is True.
Pseudocode
state = START
for each character ch in s:
look at (state, ch):
if the pair has a defined transition: state = that next state
else: return False # rejected — illegal character here
return True if state is one of {INTEGER, FRACTION, EXPONENT} else FalseThe Python solution
def is_number(s):
state = "START"
for ch in s:
if state == "START":
state = step_start(ch) # digit/sign/dot
elif state == "INTEGER":
state = step_integer(ch) # digit/dot/e
elif state == "DOT":
state = step_dot(ch) # digit
elif state in ("FRACTION",):
state = step_fraction(ch) # digit/e
elif state == "EXP":
state = step_exp(ch) # sign/digit
elif state == "EXP_SIGN":
state = step_exp_sign(ch) # digit
else:
state = step_exponent(ch) # digit
return state in ACCEPTINGstatestarts atSTARTand is the single source of truth for what we have parsed.- The loop reads one character at a time; each branch dispatches to the transition function for the current state.
- Each
step_*helper maps a character to the next state, or to a rejecting state (which makes the final check fail) when the character is illegal here. - The final line returns
Trueonly whenstateis inACCEPTING = {INTEGER, FRACTION, EXPONENT}— ending inDOT,EXP, orEXP_SIGNmeans something was left dangling.
Complexity
| Case | Time | Notes |
|---|---|---|
| Scan every character once | O(n) (moderate) | one transition per character |
| Tangled flags (brute) | O(n) (moderate) | same speed but far more bug-prone |
O(1) (fast)The DFA is O(n) time and O(1) space — we keep just one state variable regardless of how long the string is. The win over the flag soup is not speed; it is correctness you can reason about.
When this pattern shows up
When a problem is really about parsing or validating a string with lots of fiddly rules — valid number,
string-to-integer (atoi), decode/validate a format — reach for a state machine. Naming the states
and the legal transitions turns a maze of ifs into a small, checkable table.
Accepting states are the trap. Reaching the end of the string is not enough — you must end in
INTEGER, FRACTION, or EXPONENT. Strings like "1e", ".", and "+" consume cleanly but stop in
a non-accepting state, so they are invalid.
Practice
Reading 3.5e-2, after the e we read the - character. Which state does the dash move us into, and is it accepting?
1. Why model Valid Number as a finite automaton instead of boolean flags?
2. Which states are accepting?
3. Why is the string '1e' invalid?
4. What is the space complexity of the DFA solution?