13. Roman to Integer
Problem Description
This problem asks you to convert a Roman numeral string into its corresponding integer value.
Roman numerals use seven symbols with specific values:
I
= 1V
= 5X
= 10L
= 50C
= 100D
= 500M
= 1000
Normally, Roman numerals are written from largest to smallest, left to right, and you simply add up all the values. For example:
II
= 1 + 1 = 2XII
= 10 + 1 + 1 = 12XXVII
= 10 + 10 + 5 + 1 + 1 = 27
However, there's a special subtraction rule for certain combinations. When a smaller value appears before a larger value, you subtract the smaller from the larger:
IV
= 5 - 1 = 4 (notIIII
)IX
= 10 - 1 = 9 (notVIIII
)
The valid subtraction pairs are:
I
beforeV
orX
(making 4 or 9)X
beforeL
orC
(making 40 or 90)C
beforeD
orM
(making 400 or 900)
The solution uses a clever approach: it examines consecutive pairs of characters in the string. If the first character in a pair has a smaller value than the second, it subtracts that value; otherwise, it adds it. The last character is always added since it has no character after it to compare with.
For example, with MCMXCIV
(1994):
M
(1000) <C
(100)? No, add 1000C
(100) <M
(1000)? Yes, subtract 100M
(1000) <X
(10)? No, add 1000X
(10) <C
(100)? Yes, subtract 10C
(100) <I
(1)? No, add 100I
(1) <V
(5)? Yes, subtract 1V
(5) is the last character, add 5- Total: 1000 - 100 + 1000 - 10 + 100 - 1 + 5 = 1994
Intuition
The key insight comes from observing the pattern in Roman numeral subtraction cases. When we see IV
(4), we're essentially computing V - I
= 5 - 1
. This happens whenever a smaller value precedes a larger one.
Let's think about how we'd manually process a Roman numeral like MCMXCIV
:
- We read left to right
- Most of the time, we add values together
- But occasionally, we need to subtract when we spot patterns like
CM
orIV
The clever realization is that we can determine whether to add or subtract by comparing adjacent characters. For any character in the string (except the last one), we can ask: "Is my value less than the character to my right?"
- If yes, subtract my value (because I'm part of a subtraction pair)
- If no, add my value (normal addition case)
For example, in XIV
:
X
(10) compared toI
(1): 10 > 1, so add 10I
(1) compared toV
(5): 1 < 5, so subtract 1V
(5) is last, so add 5- Result:
10 - 1 + 5 = 14
This approach elegantly handles all subtraction cases without explicitly checking for IV
, IX
, XL
, XC
, CD
, or CM
. We don't need to memorize these special pairs - the rule "smaller before larger means subtract" covers them all.
The pairwise
function creates overlapping pairs (s[0], s[1])
, (s[1], s[2])
, etc., allowing us to examine each character with its neighbor. The final character is handled separately since it has no right neighbor and is always added.
Learn more about Math patterns.
Solution Approach
The implementation uses a hash table combined with a clever traversal pattern to convert Roman numerals to integers.
Step 1: Create a Hash Table
First, we build a dictionary d
that maps each Roman numeral character to its integer value:
d = {'I': 1, 'V': 5, 'X': 10, 'L': 50, 'C': 100, 'D': 500, 'M': 1000}
This allows us to quickly look up the value of any character in O(1) time.
Step 2: Process Adjacent Pairs
The core logic uses Python's pairwise
function to create overlapping pairs from the string. For a string like "XIV"
, pairwise(s)
generates:
('X', 'I')
('I', 'V')
For each pair (a, b)
, we determine whether to add or subtract the value of a
:
(-1 if d[a] < d[b] else 1) * d[a]
This expression works as follows:
- If
d[a] < d[b]
(smaller value before larger), multiplyd[a]
by-1
(subtract) - Otherwise, multiply
d[a]
by1
(add)
Step 3: Handle the Last Character
Since pairwise
only processes characters that have a neighbor to the right, the last character isn't included in any pair. We always add it to our sum:
+ d[s[-1]]
Complete Process Example
For s = "MCMXCIV"
(1994):
- Pairs generated:
('M','C')
,('C','M')
,('M','X')
,('X','C')
,('C','I')
,('I','V')
- Processing each pair:
M,C
: 1000 > 100, add 1000C,M
: 100 < 1000, subtract 100M,X
: 1000 > 10, add 1000X,C
: 10 < 100, subtract 10C,I
: 100 > 1, add 100I,V
: 1 < 5, subtract 1
- Add last character
V
: 5 - Sum:
1000 - 100 + 1000 - 10 + 100 - 1 + 5 = 1994
The entire computation is done in a single line using sum()
with a generator expression, making it both concise and efficient with O(n) time complexity where n is the length of the string.
Ready to land your dream job?
Unlock your dream job with a 5-minute evaluator for a personalized learning plan!
Start EvaluatorExample Walkthrough
Let's walk through converting the Roman numeral "XLIV"
(which equals 44) using our solution approach.
Step 1: Set up our value mapping
d = {'I': 1, 'V': 5, 'X': 10, 'L': 50, 'C': 100, 'D': 500, 'M': 1000}
Step 2: Generate pairs from the string "XLIV"
Using pairwise("XLIV")
, we get three pairs:
- Pair 1:
('X', 'L')
- Pair 2:
('L', 'I')
- Pair 3:
('I', 'V')
Step 3: Process each pair
For each pair (a, b)
, we check if d[a] < d[b]
:
-
Pair 1:
('X', 'L')
d['X']
= 10,d['L']
= 50- Is 10 < 50? Yes → Subtract 10
- Contribution: -10
-
Pair 2:
('L', 'I')
d['L']
= 50,d['I']
= 1- Is 50 < 1? No → Add 50
- Contribution: +50
-
Pair 3:
('I', 'V')
d['I']
= 1,d['V']
= 5- Is 1 < 5? Yes → Subtract 1
- Contribution: -1
Step 4: Add the last character
The last character 'V'
has no pair partner, so we always add it:
d['V']
= 5- Contribution: +5
Step 5: Calculate the total
Sum all contributions:
-10 + 50 - 1 + 5 = 44
The algorithm correctly identifies that:
XL
represents 40 (50 - 10)IV
represents 4 (5 - 1)- Together they form 44
This demonstrates how comparing adjacent characters automatically handles the subtraction cases without explicitly checking for specific patterns like XL
or IV
.
Solution Implementation
1class Solution:
2 def romanToInt(self, s: str) -> int:
3 # Dictionary mapping Roman numerals to their integer values
4 roman_to_value = {
5 'I': 1,
6 'V': 5,
7 'X': 10,
8 'L': 50,
9 'C': 100,
10 'D': 500,
11 'M': 1000
12 }
13
14 # Import pairwise from itertools (Python 3.10+)
15 from itertools import pairwise
16
17 # Calculate the sum by iterating through consecutive pairs
18 # If current value is less than next value, subtract it (e.g., IV = 4)
19 # Otherwise, add it to the total
20 result = sum(
21 (-1 if roman_to_value[current] < roman_to_value[next_char] else 1) * roman_to_value[current]
22 for current, next_char in pairwise(s)
23 )
24
25 # Add the value of the last character (it's always added, never subtracted)
26 result += roman_to_value[s[-1]]
27
28 return result
29
1class Solution {
2 public int romanToInt(String s) {
3 // Define Roman numeral characters and their corresponding values
4 String romanChars = "IVXLCDM";
5 int[] romanValues = {1, 5, 10, 50, 100, 500, 1000};
6
7 // Create a mapping from Roman characters to their integer values
8 Map<Character, Integer> romanToValueMap = new HashMap<>();
9 for (int i = 0; i < romanValues.length; i++) {
10 romanToValueMap.put(romanChars.charAt(i), romanValues[i]);
11 }
12
13 // Get the length of the input string
14 int length = s.length();
15
16 // Initialize result with the value of the last character
17 // (Last character is always added, never subtracted)
18 int result = romanToValueMap.get(s.charAt(length - 1));
19
20 // Process each character from left to right (except the last one)
21 for (int i = 0; i < length - 1; i++) {
22 // If current character's value is less than the next character's value,
23 // subtract it (e.g., IV = 4, IX = 9)
24 // Otherwise, add it to the result
25 int sign = romanToValueMap.get(s.charAt(i)) < romanToValueMap.get(s.charAt(i + 1)) ? -1 : 1;
26 result += sign * romanToValueMap.get(s.charAt(i));
27 }
28
29 return result;
30 }
31}
32
1class Solution {
2public:
3 int romanToInt(string s) {
4 // Create a mapping of Roman numeral characters to their integer values
5 unordered_map<char, int> romanValues{
6 {'I', 1},
7 {'V', 5},
8 {'X', 10},
9 {'L', 50},
10 {'C', 100},
11 {'D', 500},
12 {'M', 1000}
13 };
14
15 // Initialize result with the value of the last character
16 // This handles the last character which has no successor to compare with
17 int result = romanValues[s.back()];
18
19 // Iterate through the string from left to right, excluding the last character
20 for (int i = 0; i < s.size() - 1; ++i) {
21 // Check if current character represents a smaller value than the next one
22 // If yes, it's a subtraction case (like IV = 4, IX = 9)
23 // If no, it's a normal addition case
24 int sign = romanValues[s[i]] < romanValues[s[i + 1]] ? -1 : 1;
25
26 // Add or subtract the current character's value based on the sign
27 result += sign * romanValues[s[i]];
28 }
29
30 return result;
31 }
32};
33
1/**
2 * Converts a Roman numeral string to its integer value
3 * @param s - The Roman numeral string to convert
4 * @returns The integer value of the Roman numeral
5 */
6function romanToInt(s: string): number {
7 // Map of Roman numeral characters to their corresponding integer values
8 const romanToValueMap: Map<string, number> = new Map<string, number>([
9 ['I', 1],
10 ['V', 5],
11 ['X', 10],
12 ['L', 50],
13 ['C', 100],
14 ['D', 500],
15 ['M', 1000],
16 ]);
17
18 // Initialize result with the value of the last character
19 // This allows us to process the string from left to right without special handling for the last character
20 let result: number = romanToValueMap.get(s[s.length - 1])!;
21
22 // Iterate through the string from left to right, excluding the last character
23 for (let i = 0; i < s.length - 1; i++) {
24 // Get the current and next character values
25 const currentValue: number = romanToValueMap.get(s[i])!;
26 const nextValue: number = romanToValueMap.get(s[i + 1])!;
27
28 // If current value is less than next value, subtract (handles cases like IV, IX, XL, XC, CD, CM)
29 // Otherwise, add the current value to the result
30 const sign: number = currentValue < nextValue ? -1 : 1;
31 result += sign * currentValue;
32 }
33
34 return result;
35}
36
Time and Space Complexity
Time Complexity: O(n)
The algorithm iterates through the string using pairwise(s)
, which generates pairs of consecutive characters. This creates n-1
pairs for a string of length n
. Each pair operation involves:
- Two dictionary lookups:
d[a]
andd[b]
- bothO(1)
operations - One comparison:
d[a] < d[b]
-O(1)
operation - One multiplication:
(-1 if ... else 1) * d[a]
-O(1)
operation
After processing all pairs, there's one additional dictionary lookup for the last character: d[s[-1]]
- O(1)
operation.
The sum()
function iterates through all n-1
pair results plus the final character value, performing n
additions in total.
Since we perform a constant amount of work for each character in the string, the overall time complexity is O(n)
, where n
is the length of the input string s
.
Space Complexity: O(m)
The space usage consists of:
- The dictionary
d
containing 7 key-value pairs representing Roman numerals -O(7)
=O(m)
wherem
is the size of the character set (7 in this case) - The
pairwise()
iterator generates pairs on-the-fly without storing all pairs in memory -O(1)
additional space - The generator expression inside
sum()
produces values one at a time -O(1)
additional space
Therefore, the overall space complexity is O(m)
, where m
represents the size of the character set used in the Roman numeral system.
Learn more about how to find time and space complexity quickly.
Common Pitfalls
1. Python Version Compatibility Issue
The pairwise
function is only available in Python 3.10+. If you're using an earlier version or submitting to a platform that doesn't support Python 3.10, the code will fail with an ImportError
.
Solution: Implement your own pairwise logic using zip:
class Solution:
def romanToInt(self, s: str) -> int:
roman_to_value = {
'I': 1, 'V': 5, 'X': 10, 'L': 50,
'C': 100, 'D': 500, 'M': 1000
}
result = 0
# Use zip to create pairs manually
for i in range(len(s) - 1):
if roman_to_value[s[i]] < roman_to_value[s[i + 1]]:
result -= roman_to_value[s[i]]
else:
result += roman_to_value[s[i]]
# Add the last character
result += roman_to_value[s[-1]]
return result
2. Forgetting to Handle the Last Character
A common mistake is only processing the pairs and forgetting that the last character isn't part of any pair. Since pairwise
creates overlapping pairs, the last character never appears as the first element of a pair.
What happens if you forget: For "XIV" (14), without adding the last character 'V', you'd get 10 - 1 = 9 instead of 14.
Solution:
Always remember to add roman_to_value[s[-1]]
after processing all pairs.
3. Incorrect Subtraction Logic
Mixing up when to subtract vs. add. The rule is: subtract when a smaller value appears BEFORE a larger value, not after.
Wrong approach:
# Incorrect: checking if current is greater than next if roman_to_value[current] > roman_to_value[next_char]: result -= roman_to_value[current] # Wrong!
Correct approach:
# Correct: subtract when current is less than next if roman_to_value[current] < roman_to_value[next_char]: result -= roman_to_value[current]
4. Edge Case: Single Character Input
If the input is a single character like "V", using s[-1]
works fine, but iterating through pairs might cause issues if not handled properly.
Solution:
The current implementation handles this correctly since pairwise
of a single character returns an empty iterator, and the sum would be 0, then we add s[-1]
which gives the correct result. However, if implementing manually, consider adding a check:
if len(s) == 1:
return roman_to_value[s[0]]
5. Using Dictionary Name 'd' Instead of Descriptive Name
While the original solution uses d
as the dictionary name, this can lead to confusion when debugging or maintaining code.
Solution:
Use a descriptive name like roman_to_value
or roman_map
to make the code more readable and self-documenting.
What does the following code do?
1def f(arr1, arr2):
2 i, j = 0, 0
3 new_arr = []
4 while i < len(arr1) and j < len(arr2):
5 if arr1[i] < arr2[j]:
6 new_arr.append(arr1[i])
7 i += 1
8 else:
9 new_arr.append(arr2[j])
10 j += 1
11 new_arr.extend(arr1[i:])
12 new_arr.extend(arr2[j:])
13 return new_arr
14
1public static List<Integer> f(int[] arr1, int[] arr2) {
2 int i = 0, j = 0;
3 List<Integer> newArr = new ArrayList<>();
4
5 while (i < arr1.length && j < arr2.length) {
6 if (arr1[i] < arr2[j]) {
7 newArr.add(arr1[i]);
8 i++;
9 } else {
10 newArr.add(arr2[j]);
11 j++;
12 }
13 }
14
15 while (i < arr1.length) {
16 newArr.add(arr1[i]);
17 i++;
18 }
19
20 while (j < arr2.length) {
21 newArr.add(arr2[j]);
22 j++;
23 }
24
25 return newArr;
26}
27
1function f(arr1, arr2) {
2 let i = 0, j = 0;
3 let newArr = [];
4
5 while (i < arr1.length && j < arr2.length) {
6 if (arr1[i] < arr2[j]) {
7 newArr.push(arr1[i]);
8 i++;
9 } else {
10 newArr.push(arr2[j]);
11 j++;
12 }
13 }
14
15 while (i < arr1.length) {
16 newArr.push(arr1[i]);
17 i++;
18 }
19
20 while (j < arr2.length) {
21 newArr.push(arr2[j]);
22 j++;
23 }
24
25 return newArr;
26}
27
Recommended Readings
Math for Technical Interviews How much math do I need to know for technical interviews The short answer is about high school level math Computer science is often associated with math and some universities even place their computer science department under the math faculty However the reality is that you
Coding Interview Patterns Your Personal Dijkstra's Algorithm to Landing Your Dream Job The goal of AlgoMonster is to help you get a job in the shortest amount of time possible in a data driven way We compiled datasets of tech interview problems and broke them down by patterns This way
Recursion Recursion is one of the most important concepts in computer science Simply speaking recursion is the process of a function calling itself Using a real life analogy imagine a scenario where you invite your friends to lunch https assets algo monster recursion jpg You first call Ben and ask
Want a Structured Path to Master System Design Too? Don’t Miss This!