Roman Numeral Conversion to Decimal
You're in an Apple phone screen and the interviewer pulls up a short string problem to kick things off. "Given a Roman numeral like MCMXCIV, convert it to an integer." This one shows up constantly at Apple, Google, Amazon, and Microsoft as an early-round warm-up. It looks straightforward, but the subtraction rule (IV is 4, not 6) trips up candidates who rush into a left-to-right accumulator without thinking about ordering. The trick is recognizing that a single right-to-left pass with one comparison handles every case cleanly.
TL;DR
Map each Roman symbol to its integer value, then walk the string from right to left. Keep track of the previous character's value. If the current value is less than the previous, subtract it from the running total; otherwise, add it. This handles all six subtraction cases (IV, IX, XL, XC, CD, CM) without any special-case branching. O(n) time, O(1) space.
Prefer a different language? Jump to solutions in other languages.
Why This Problem Matters
Roman to Integer is one of the most popular easy-level string problems in technical interviews. It tests three things at once: hash map construction, string traversal direction, and translating a set of rules into clean code. The subtraction rule is what separates a correct solution from a buggy one, and interviewers pay attention to how you handle it. Get comfortable with this pattern, because the same "compare current to neighbor" logic appears in problems like stock trading, trapping rain water, and monotonic stacks.
Roman Numeral Basics
Roman numerals use seven symbols, each with a fixed value:
Loading visualization...
Normally, symbols are written from largest to smallest, left to right. You add them up: LVIII = 50 + 5 + 1 + 1 + 1 = 58. But when a smaller symbol appears immediately before a larger one, you subtract it instead of adding it. There are exactly six valid subtraction pairs:
Loading visualization...
These six pairs are the only exceptions to the "add everything" rule. The input is guaranteed to be a valid Roman numeral between 1 and 3999.
Understanding the Problem
Given: A string s containing Roman numeral characters (I, V, X, L, C, D, M)
Return: The integer value that string represents
Constraints: 1 <= s.length <= 15, the string is a valid Roman numeral in the range [1, 3999]
romanToInt("III") => 3
romanToInt("LVIII") => 58
romanToInt("MCMXCIV") => 1994
Edge Cases
- Single character:
"I"returns 1,"M"returns 1000 - All additive:
"MMMDCCCLXXXVIII"(3888) has no subtraction pairs - Multiple subtractions:
"MCMXCIV"(1994) has three subtraction pairs (CM, XC, IV) - Maximum value:
"MMMCMXCIX"(3999) is the largest valid Roman numeral
Solution Approach
The brute-force way would be to scan for each of the six subtraction pairs explicitly, handle them as two-character tokens, then sum the remaining single characters. That works, but it's messy and error-prone.
A cleaner approach comes from one observation: in a valid Roman numeral, a subtraction case always shows a smaller value immediately before a larger value. If you process the string from right to left and compare each symbol to the one you just handled, the logic reduces to a single if statement.
The Right-to-Left Algorithm
- Build a hash map of symbol-to-value:
{'I': 1, 'V': 5, 'X': 10, ...} - Initialize
total = 0andprevValue = 0 - Iterate from the last character to the first
- For each character, look up its value in the map
- If
currentValue < prevValue, subtract it fromtotal(subtraction case) - Otherwise, add it to
total - Update
prevValue = currentValue - Return
total
Walking Through "IV" (= 4)
This is the simplest subtraction example:
Loading visualization...
Starting from the right, we first process V (value 5). Since 5 >= 0 (prevValue), we add it: total becomes 5. Then we process I (value 1). Since 1 < 5 (prevValue), we subtract: total becomes 5 - 1 = 4. That's the correct answer.
Walking Through "LVIII" (= 58)
A purely additive case with no subtraction:
Loading visualization...
Every symbol is greater than or equal to the one to its right, so we add them all. The running total builds up to 58.
Walking Through "MCMXCIV" (= 1994)
This example hits three subtraction pairs (CM, XC, IV):
Loading visualization...
The right-to-left pass handles each subtraction pair naturally. When we reach I and see that prevValue is 5 (from V), we subtract. When we reach X and see prevValue is 100 (from C), we subtract. When we reach C and see prevValue is 1000 (from M), we subtract. No special-case code needed.
Edge Case: Single Character
Loading visualization...
A one-character input has nothing to subtract from. The algorithm just adds the single value and returns.
Implementation
Here's the Java solution:
import java.util.*;
public class Solution {
public int romanToInt(String s) {
Map<Character, Integer> romanMap = new HashMap<>();
romanMap.put('I', 1);
romanMap.put('V', 5);
romanMap.put('X', 10);
romanMap.put('L', 50);
romanMap.put('C', 100);
romanMap.put('D', 500);
romanMap.put('M', 1000);
int total = 0;
int prevValue = 0;
for (int i = s.length() - 1; i >= 0; i--) {
char currentChar = s.charAt(i);
int currentValue = romanMap.get(currentChar);
if (currentValue < prevValue) {
total -= currentValue;
} else {
total += currentValue;
}
prevValue = currentValue;
}
return total;
}
}
The structure is direct: build the map, walk right to left, compare, add or subtract, update. Each character is processed exactly once.
Complexity Analysis
Time Complexity: O(n)
The algorithm makes a single pass through the string, visiting each character once. The hash map lookup for each character is O(1), so the total work is O(n) where n is the length of the input string. Since the maximum length is 15 (for the number 3888, MMMDCCCLXXXVIII), this is effectively constant, but the algorithm generalizes to any string length.
Space Complexity: O(1)
The hash map always stores exactly seven entries, regardless of input size. The only other variables are total, prevValue, and the loop counter. No auxiliary space scales with the input.
Why Not Left to Right?
You can also solve this left to right by comparing the current character's value to the next character's value. If the current is smaller, subtract it; otherwise add it. Both approaches are O(n) and O(1). The right-to-left version is slightly cleaner because you compare against prevValue (already computed) rather than peeking ahead at s.charAt(i + 1) and handling the last-character boundary.
Common Pitfalls
-
Forgetting the subtraction rule: Adding all values without checking for subtraction pairs gives wrong results for any input containing IV, IX, XL, XC, CD, or CM.
-
Off-by-one in the loop: When iterating right to left, make sure the loop starts at
s.length() - 1and ends ati >= 0, noti > 0(which would skip the first character). -
Using a switch/if-else chain instead of a map: Hardcoding the seven values in a switch statement works but is harder to read and maintain. A map is cleaner.
-
Trying to handle pairs explicitly: Scanning for two-character tokens like "IV" or "CM" is more complex and error-prone than the single-comparison approach.
Interview Tips
-
Start by clarifying: Confirm the input is guaranteed to be a valid Roman numeral. Ask about the range (1 to 3999).
-
Mention both traversal directions: Briefly explain that left-to-right and right-to-left both work, and state why you prefer one.
-
Trace through an example: Walk through "MCMXCIV" on the whiteboard showing the total at each step. This demonstrates that you understand the subtraction rule.
-
Know the follow-up: The reverse problem, Integer to Roman, is a natural follow-up. For that one, you greedily subtract the largest value and append symbols.
-
Discuss production considerations: In real code, you'd validate the input string and reject malformed numerals. Mention this to show engineering maturity.
Solutions in Other Languages
Python
class Solution:
def roman_to_int(self, s: str) -> int:
roman_to_int_map = {
'I': 1, 'V': 5, 'X': 10, 'L': 50,
'C': 100, 'D': 500, 'M': 1000
}
total = 0
prev_value = 0
for char in reversed(s):
current_value = roman_to_int_map[char]
if current_value < prev_value:
total -= current_value
else:
total += current_value
prev_value = current_value
return total
Python's reversed() built-in makes the right-to-left iteration read naturally without manual index math.
JavaScript
class Solution {
romanToInt(s) {
const romanMap = {
'I': 1, 'V': 5, 'X': 10, 'L': 50,
'C': 100, 'D': 500, 'M': 1000
};
let total = 0;
let prevValue = 0;
for (let i = s.length - 1; i >= 0; i--) {
const currentValue = romanMap[s[i]];
if (currentValue < prevValue) {
total -= currentValue;
} else {
total += currentValue;
}
prevValue = currentValue;
}
return total;
}
}
TypeScript
class Solution {
romanToInt(s: string): number {
const romanMap: { [key: string]: number } = {
'I': 1, 'V': 5, 'X': 10, 'L': 50,
'C': 100, 'D': 500, 'M': 1000
};
let total = 0;
let prevValue = 0;
for (let i = s.length - 1; i >= 0; i--) {
const currentValue = romanMap[s[i]];
if (currentValue < prevValue) {
total -= currentValue;
} else {
total += currentValue;
}
prevValue = currentValue;
}
return total;
}
}
C++
#include <string>
#include <unordered_map>
class Solution {
public:
int romanToInt(std::string &s) {
std::unordered_map<char, int> romanMap = {
{'I', 1}, {'V', 5}, {'X', 10}, {'L', 50},
{'C', 100}, {'D', 500}, {'M', 1000}
};
int total = 0;
int prevValue = 0;
for (auto it = s.rbegin(); it != s.rend(); ++it) {
int currentValue = romanMap[*it];
if (currentValue < prevValue) {
total -= currentValue;
} else {
total += currentValue;
}
prevValue = currentValue;
}
return total;
}
};
C++ uses rbegin()/rend() reverse iterators for clean right-to-left traversal.
Go
func RomanToInt(roman string) int {
romanToIntMap := map[byte]int{
'I': 1, 'V': 5, 'X': 10, 'L': 50,
'C': 100, 'D': 500, 'M': 1000,
}
total := 0
prevValue := 0
for i := len(roman) - 1; i >= 0; i-- {
currentValue := romanToIntMap[roman[i]]
if currentValue < prevValue {
total -= currentValue
} else {
total += currentValue
}
prevValue = currentValue
}
return total
}
Scala
class Solution {
def romanToInt(s: String): Int = {
val romanMap = Map(
'I' -> 1, 'V' -> 5, 'X' -> 10, 'L' -> 50,
'C' -> 100, 'D' -> 500, 'M' -> 1000
)
var total = 0
var prevValue = 0
for (char <- s.reverse) {
val currentValue = romanMap(char)
if (currentValue < prevValue) {
total -= currentValue
} else {
total += currentValue
}
prevValue = currentValue
}
total
}
}
Kotlin
class Solution {
fun romanToInt(s: String): Int {
val romanMap = mapOf(
'I' to 1, 'V' to 5, 'X' to 10, 'L' to 50,
'C' to 100, 'D' to 500, 'M' to 1000
)
var total = 0
var prevValue = 0
for (i in s.length - 1 downTo 0) {
val currentValue = romanMap[s[i]] ?: 0
if (currentValue < prevValue) {
total -= currentValue
} else {
total += currentValue
}
prevValue = currentValue
}
return total
}
}
Kotlin's downTo keyword makes the reverse iteration concise and readable.
Swift
import Foundation
class Solution {
func romanToInt(_ s: String) -> Int {
let romanMap: [Character: Int] = [
"I": 1, "V": 5, "X": 10, "L": 50,
"C": 100, "D": 500, "M": 1000
]
var total = 0
var prevValue = 0
let characters = Array(s)
for i in stride(from: characters.count - 1, through: 0, by: -1) {
let currentValue = romanMap[characters[i]] ?? 0
if currentValue < prevValue {
total -= currentValue
} else {
total += currentValue
}
prevValue = currentValue
}
return total
}
}
Swift strings don't support integer subscript access, so converting to an array first avoids repeated O(n) character lookups.
Ruby
class Solution
def roman_to_int(s)
roman_map = {
'I' => 1, 'V' => 5, 'X' => 10, 'L' => 50,
'C' => 100, 'D' => 500, 'M' => 1000
}
total = 0
prev_value = 0
(s.length - 1).downto(0) do |i|
current_value = roman_map[s[i]]
if current_value < prev_value
total -= current_value
else
total += current_value
end
prev_value = current_value
end
total
end
end
Rust
use std::collections::HashMap;
impl Solution {
pub fn roman_to_int(&self, s: String) -> i32 {
let mut roman_map = HashMap::new();
roman_map.insert('I', 1);
roman_map.insert('V', 5);
roman_map.insert('X', 10);
roman_map.insert('L', 50);
roman_map.insert('C', 100);
roman_map.insert('D', 500);
roman_map.insert('M', 1000);
let mut total = 0;
let mut prev_value = 0;
for c in s.chars().rev() {
let current_value = roman_map[&c];
if current_value < prev_value {
total -= current_value;
} else {
total += current_value;
}
prev_value = current_value;
}
total
}
}
Rust's chars().rev() iterator chain gives a clean right-to-left traversal over Unicode characters.
C#
public class Solution {
public int romanToInt(string s) {
var romanMap = new Dictionary<char, int>();
romanMap['I'] = 1;
romanMap['V'] = 5;
romanMap['X'] = 10;
romanMap['L'] = 50;
romanMap['C'] = 100;
romanMap['D'] = 500;
romanMap['M'] = 1000;
int total = 0;
int prevValue = 0;
for (int i = s.Length - 1; i >= 0; i--) {
int currentValue = romanMap[s[i]];
if (currentValue < prevValue) {
total -= currentValue;
} else {
total += currentValue;
}
prevValue = currentValue;
}
return total;
}
}
Dart
class Solution {
int romanToInt(String s) {
Map<String, int> romanMap = {
'I': 1, 'V': 5, 'X': 10, 'L': 50,
'C': 100, 'D': 500, 'M': 1000,
};
int total = 0;
int prevValue = 0;
for (int i = s.length - 1; i >= 0; i--) {
int currentValue = romanMap[s[i]]!;
if (currentValue < prevValue) {
total -= currentValue;
} else {
total += currentValue;
}
prevValue = currentValue;
}
return total;
}
}
PHP
class Solution {
public function romanToInt(string $s): int {
$romanMap = [
'I' => 1, 'V' => 5, 'X' => 10, 'L' => 50,
'C' => 100, 'D' => 500, 'M' => 1000
];
$total = 0;
$prevValue = 0;
for ($i = strlen($s) - 1; $i >= 0; $i--) {
$currentValue = $romanMap[$s[$i]];
if ($currentValue < $prevValue) {
$total -= $currentValue;
} else {
$total += $currentValue;
}
$prevValue = $currentValue;
}
return $total;
}
}
Key Takeaways
- A hash map of seven symbol-to-value pairs plus a single right-to-left pass is all you need. The subtraction rule reduces to one comparison: if
currentValue < prevValue, subtract; otherwise add. - Right-to-left traversal avoids lookahead logic. You compare against the value you already processed rather than peeking at the next character and worrying about bounds.
- The algorithm is O(n) time and O(1) space. In practice, n is at most 15 characters, so performance is never an issue. Interviewers care more about clean logic than optimization here.
- Know the six subtraction pairs (IV, IX, XL, XC, CD, CM) cold. Being able to explain why these pairs exist and verify them against your algorithm shows solid understanding.
- The natural follow-up is Integer to Roman, which uses a greedy approach. Having both solutions ready makes you well-prepared for this family of problems.
Practice Makes Perfect
Once you have Roman to Integer down, try these related problems to build on the same skills:
- Integer to Roman (greedy construction, the reverse operation)
- Valid Number (string parsing with rules)
- Decode Ways (character-by-character processing with lookahead)
Remember, consistent practice is the key to succeeding in coding interviews. This problem and thousands of others are available on Firecode, where over 50,000 users have successfully prepared for technical interviews and landed six and seven-figure jobs at top tech companies. Whether you're just starting or preparing for your dream job, getting these fundamentals locked in will set you up for success.
Happy coding, and may all your Roman numerals convert on the first try!