Facebook Pixel

13. Roman to Integer

EasyHash TableMathString
Leetcode Link

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 = 1
  • V = 5
  • X = 10
  • L = 50
  • C = 100
  • D = 500
  • M = 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 = 2
  • XII = 10 + 1 + 1 = 12
  • XXVII = 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 (not IIII)
  • IX = 10 - 1 = 9 (not VIIII)

The valid subtraction pairs are:

  • I before V or X (making 4 or 9)
  • X before L or C (making 40 or 90)
  • C before D or M (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 1000
  • C (100) < M (1000)? Yes, subtract 100
  • M (1000) < X (10)? No, add 1000
  • X (10) < C (100)? Yes, subtract 10
  • C (100) < I (1)? No, add 100
  • I (1) < V (5)? Yes, subtract 1
  • V (5) is the last character, add 5
  • Total: 1000 - 100 + 1000 - 10 + 100 - 1 + 5 = 1994
Quick Interview Experience
Help others by sharing your interview experience
Have you seen this problem before?

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 or IV

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 to I (1): 10 > 1, so add 10
  • I (1) compared to V (5): 1 < 5, so subtract 1
  • V (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), multiply d[a] by -1 (subtract)
  • Otherwise, multiply d[a] by 1 (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):

  1. Pairs generated: ('M','C'), ('C','M'), ('M','X'), ('X','C'), ('C','I'), ('I','V')
  2. Processing each pair:
    • M,C: 1000 > 100, add 1000
    • C,M: 100 < 1000, subtract 100
    • M,X: 1000 > 10, add 1000
    • X,C: 10 < 100, subtract 10
    • C,I: 100 > 1, add 100
    • I,V: 1 < 5, subtract 1
  3. Add last character V: 5
  4. 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 Evaluator

Example 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] and d[b] - both O(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) where m 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.

Discover Your Strengths and Weaknesses: Take Our 5-Minute Quiz to Tailor Your Study Plan:

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

Want a Structured Path to Master System Design Too? Don’t Miss This!

Load More