Topics › Strings › Medium String Problems
Roman to Integer
LeetCode ↗ Java source ↗ Edit this article ↗
Jump to code:
Problem
Roman numerals use seven symbols: I = 1, V = 5, X = 10, L = 50, C = 100, D = 500, M = 1000. A numeral is normally written with its symbols in non-increasing order and its value is the sum of the symbols: XXVII is 10 + 10 + 5 + 1 + 1 = 27. There are exactly six exceptions, the subtractive pairs, where a smaller symbol placed immediately before a larger one is subtracted instead of added: IV = 4, IX = 9, XL = 40, XC = 90, CD = 400, CM = 900.
Given a string s that is a valid Roman numeral, return the integer it represents.
Constraints: 1 ≤ |s| ≤ 15; s contains only the seven symbols above and is guaranteed to be a valid numeral in the range 1 to 3999.
Example 1
s = "LVIII"
answer = 58 // L = 50, V = 5, III = 3
Example 2
s = "MCMXCIV"
answer = 1994 // M = 1000, CM = 900, XC = 90, IV = 4
Example 3
s = "III"
answer = 3 // three symbols, all added
Approach 1: Brute force, a table of all thirteen tokens
Intuition. Treat the numeral as a sequence of tokens drawn from a fixed vocabulary of thirteen: the seven single symbols plus the six subtractive pairs. Reading left to right, at every position we must decide whether the next token is one letter or two. The rule is simple: if the next two characters form one of the six pairs, they belong together, because in a valid numeral a smaller symbol is never followed by a larger one except inside a subtractive pair. So “try the two-letter token first, fall back to one letter” is a correct greedy tokeniser.
Algorithm.
- Build a map from each of the 13 tokens to its value.
- Set
total = 0andi = 0. - While
i < n: ifi + 1 < nands[i..i+1]is in the map, add its value and advance by2; otherwise add the value ofs[i]and advance by1. - Return
total.
static int bruteForce(String s) {
Map<String, Integer> table = new HashMap<>();
table.put("I", 1); table.put("IV", 4); table.put("V", 5); table.put("IX", 9);
table.put("X", 10); table.put("XL", 40); table.put("L", 50); table.put("XC", 90);
table.put("C", 100); table.put("CD", 400); table.put("D", 500); table.put("CM", 900);
table.put("M", 1000);
int total = 0, i = 0, n = s.length();
while (i < n) {
if (i + 1 < n && table.containsKey(s.substring(i, i + 2))) {
total += table.get(s.substring(i, i + 2));
i += 2;
} else {
total += table.get(s.substring(i, i + 1));
i += 1;
}
}
return total;
}
Complexity. Time O(n): each character is consumed once, and the map has constant size. Space O(1): thirteen entries regardless of the input. The approach does allocate a short substring per step, which is its only practical cost. It is also the most literal transcription of the rules, which makes it a good first answer in an interview before refining.
The thirteen-entry table encodes the subtraction rule by enumeration. The next approach encodes it by a single comparison, which removes the table and the substrings.
Approach 2: Optimal, compare each symbol with its right neighbour
Intuition. Look at what a subtractive pair really does. In CM, the M is added as usual and the C is subtracted: -100 + 1000 = 900. So the only symbols that are ever subtracted are those that are immediately followed by a strictly larger symbol. Everything else is added. That turns the whole problem into one pass with one comparison per character: subtract value(s[i]) if value(s[i]) < value(s[i+1]), otherwise add it.
Why is this exact? A valid numeral is a sequence of tokens as in Approach 1. Inside a subtractive pair the first symbol is smaller than the second, so the rule subtracts it, and the second symbol is either the last character or followed by something no larger (tokens appear in non-increasing order of value), so it is added. A single-symbol token is never followed by a larger symbol, otherwise the two would have formed a subtractive pair. Hence the rule assigns every character the sign it has in the token decomposition.
The helper converts one symbol to its value with a switch expression and rejects anything that is not a Roman symbol:
static int value(char c) {
return switch (c) {
case 'I' -> 1;
case 'V' -> 5;
case 'X' -> 10;
case 'L' -> 50;
case 'C' -> 100;
case 'D' -> 500;
case 'M' -> 1000;
default -> throw new IllegalArgumentException("not a Roman symbol: " + c);
};
}
Algorithm.
- Set
total = 0. - For each index
i: letcur = value(s[i]).- If
i + 1 < nandcur < value(s[i+1]), dototal -= cur. - Otherwise do
total += cur.
- If
- Return
total.
static int optimal(String s) {
int total = 0, n = s.length();
for (int i = 0; i < n; i++) {
int cur = value(s.charAt(i));
if (i + 1 < n && cur < value(s.charAt(i + 1))) total -= cur;
else total += cur;
}
return total;
}
Dry run on s = "MCMXCIV":
| i | s[i] | cur | next | action | total |
|---|---|---|---|---|---|
| 0 | M | 1000 | 100 | add | 1000 |
| 1 | C | 100 | 1000 | subtract | 900 |
| 2 | M | 1000 | 10 | add | 1900 |
| 3 | X | 10 | 100 | subtract | 1890 |
| 4 | C | 100 | 1 | add | 1990 |
| 5 | I | 1 | 5 | subtract | 1989 |
| 6 | V | 5 | none | add | 1994 |
Complexity. Time O(n): one comparison and one addition per character. Space O(1): no table, no substrings.
Approach 3: Optimal, scan from the right
Intuition. The same rule can be applied while walking from the end of the string towards the front. The symbol processed in the previous step is the right neighbour of the current one, so remembering a single value prev replaces the look-ahead s[i+1] and removes the bounds check. The very last symbol is always added, which is what happens naturally when prev starts at 0: nothing is smaller than 0.
Algorithm.
- Set
total = 0andprev = 0. - For
ifromn - 1down to0: letcur = value(s[i]). Ifcur < prevsubtract it, otherwise add it. Then setprev = cur. - Return
total.
static int optimalRightToLeft(String s) {
int total = 0, prev = 0;
for (int i = s.length() - 1; i >= 0; i--) {
int cur = value(s.charAt(i));
if (cur < prev) total -= cur;
else total += cur;
prev = cur;
}
return total;
}
Complexity. Time O(n), space O(1), identical to Approach 2. The two are interchangeable; the right-to-left form is slightly tidier because the loop body has no index arithmetic.
Note that the comparison is strict in both directions of scanning. Equal neighbours, as in III or XX, must be added, and cur < prev is false for equal values.
Edge cases and pitfalls
- A single symbol.
"I"returns1and"M"returns1000. In Approach 2 thei + 1 < nguard prevents reading past the end; in Approach 3prev = 0makes the last symbol additive. - The extremes.
"MMMCMXCIX"is3999, the largest valid numeral, and threeMin a row show that repeated equal symbols are simply summed. - Several subtractive pairs in a row.
"CDXLIV"is400 + 40 + 4 = 444; each pair is handled independently because the subtraction decision only looks one character ahead. - Overflow is impossible. The maximum value is
3999, far insideint. There is no need forlonghere. - Invalid numerals. The problem guarantees validity. Fed
"IIII"every method returns4; fed"IC"the comparison-based methods return99while the token table returns101(it seesIthenC, sinceICis not one of the six pairs). Neither input is a real Roman numeral, and none of the functions detects that. Validating the grammar (at most three repeats, only the six legal pairs, non-increasing tokens) is a separate task. - Unknown characters.
valuethrowsIllegalArgumentExceptionfor anything outside the seven symbols, including lowercase letters. Convert withtoUpperCase()first if the input may be lowercase. - Do not special-case the pairs with string comparisons inside the optimal approach; the numeric comparison already covers all six and is less error prone.
Related problems
- String to Integer (atoi) is the next problem on the sheet: another left-to-right parse that turns characters into a number, this time with overflow to worry about.
- Implement ATOI/STRSTR is the SDE-sheet version of that parse.
- Count and say is also a run-based reading of a string where you look at the neighbour of each character to decide what to emit.