Roman Numeral Conversion to Decimal

AS
Ackshaey Singh
Difficulty Medium
Time Complexity
O(n)
Space Complexity
O(1)
String Math Hash table
Apple Google Amazon Microsoft Bloomberg Oracle

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

  1. Single character: "I" returns 1, "M" returns 1000
  2. All additive: "MMMDCCCLXXXVIII" (3888) has no subtraction pairs
  3. Multiple subtractions: "MCMXCIV" (1994) has three subtraction pairs (CM, XC, IV)
  4. 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

  1. Build a hash map of symbol-to-value: {'I': 1, 'V': 5, 'X': 10, ...}
  2. Initialize total = 0 and prevValue = 0
  3. Iterate from the last character to the first
  4. For each character, look up its value in the map
  5. If currentValue < prevValue, subtract it from total (subtraction case)
  6. Otherwise, add it to total
  7. Update prevValue = currentValue
  8. 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

  1. 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.

  2. Off-by-one in the loop: When iterating right to left, make sure the loop starts at s.length() - 1 and ends at i >= 0, not i > 0 (which would skip the first character).

  3. 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.

  4. 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

  1. Start by clarifying: Confirm the input is guaranteed to be a valid Roman numeral. Ask about the range (1 to 3999).

  2. Mention both traversal directions: Briefly explain that left-to-right and right-to-left both work, and state why you prefer one.

  3. Trace through an example: Walk through "MCMXCIV" on the whiteboard showing the total at each step. This demonstrates that you understand the subtraction rule.

  4. 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.

  5. 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!

Frequently Asked Questions

What is the best approach to convert Roman numerals to integers in a coding interview?
Build a hash map of the seven Roman symbols to their values, then iterate the string from right to left. Track the previous 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) automatically without special-case branching. It runs in O(n) time and O(1) space.
Why does iterating right to left simplify Roman numeral conversion?
Right-to-left iteration lets you compare each symbol against the one you just processed (the symbol to its right). If a smaller symbol appears before a larger one, it must be a subtraction case like IV or XC. Comparing against the previous value in one pass avoids the need to look ahead or handle pairs explicitly.
What are the six subtraction cases in Roman numerals?
The six subtraction pairs are IV (4), IX (9), XL (40), XC (90), CD (400), and CM (900). In each pair, a smaller-value symbol immediately precedes a larger one, meaning you subtract the smaller value instead of adding it. These are the only valid subtraction combinations in standard Roman numeral notation.
What is the time complexity of converting a Roman numeral to an integer?
O(n) where n is the length of the input string. Each character is visited exactly once during the single right-to-left pass. The hash map lookup for each character is O(1), making the total work linear in the string length.
What is the space complexity of the Roman to integer solution?
O(1) because the hash map always contains exactly seven entries regardless of input size. The only other variables are the running total and the previous value tracker. No auxiliary data structures scale with the input.
Can you solve Roman to integer by iterating left to right instead?
Yes. When iterating left to right, compare the current value with the next character's value. If the current value is less than the next, subtract it; otherwise add it. Both directions produce the same result. The right-to-left approach is slightly simpler because you compare against the already-processed previous value rather than peeking ahead.
How do you handle invalid Roman numeral input?
For interview purposes, the problem guarantees valid input in the range 1 to 3999. In production code, you would validate that the string contains only the characters I, V, X, L, C, D, M, check that subtraction pairs are valid (only I before V/X, X before L/C, C before D/M), and verify that no symbol repeats more than three times consecutively.
What is the difference between Roman to Integer and Integer to Roman?
Roman to Integer is a parsing problem where you read symbols and accumulate a numeric value, running in O(n) time. Integer to Roman is a construction problem where you greedily subtract the largest possible Roman value and append its symbol, also O(1) since the output is bounded by 3999. Interviewers sometimes ask both as follow-ups to each other.
How often does Roman to Integer appear in coding interviews?
Roman to Integer is one of the most frequently asked easy-level problems in 2026 interviews, especially at Apple, Google, Amazon, and Microsoft. Its high acceptance rate (around 64%) makes it a popular warm-up question. It tests hash map usage, string traversal, and the ability to translate rules into clean code.
What real-world applications involve Roman numeral parsing?
Roman numerals appear in clock faces, book chapter numbering, movie sequel titles, copyright dates, and legal document sections. Parsing them programmatically is useful in document processing, OCR post-processing, and data normalization pipelines where Roman numerals need to be converted to sortable integers.