Roman Numerals Leetcode

Posted by

Cracking LeetCode 12: The Ultimate Guide to Integer to Roman Conversion

Preparing for a technical interview can be stressful. If you have been practicing on LeetCode, you have probably come across a Roman Numeral problem. LeetCode 12: Integer to Roman is an extremely common interview question asked by top tech companies like Amazon, Microsoft, and Adobe.

To the untrained eye, it looks like a simple math puzzle. But try to code it too fast and you will have a major headache coping with the tricky numbers that use subtraction, like IV (4) or CM (900). If you lack a solid plan, your code can easily degenerate into a confusing mess of endless if-else statements.

In this guide, we are going to completely break down this problem. No complicated textbook talk—just a basic, human way of understanding the logic, finding the pattern, and writing clean code that passes every test.

The Rules We Have to Obey

To fix this, all we need to remember are two basic Roman numeral conventions:

  • Usually they are additive: You just line them up from greatest to smallest and add them up. For example, $15$ is written as XV ($10 + 5$).

  • They hate repetitions: You can never repeat the same symbol four times in a row (so $4$ is not IIII). Instead, you put a smaller letter before a bigger one to subtract it (so $4$ becomes IV, or “$5$ minus $1$“).

The problem provides us with seven basic symbols:

Symbol Value
I 1
V 5
X 10
L 50
C 100
D 500
M 1000

The Trap: Subtractive Forms

Normally, Roman numerals are additive. You write them from highest to lowest value. For example, $58$ becomes LVIII ($50 + 5 + 1 + 1 + 1$).

However, Roman numerals strictly forbid repeating the same symbol more than three times in a row. You can’t write $4$ as IIII. Instead, you use subtractive notation, placing a smaller value before a larger value to indicate subtraction.

The problem specifies exactly six subtractive pairs we need to worry about:

  • IV = 4

  • IX = 9

  • XL = 40

  • XC = 90

  • CD = 400

  • CM = 900

Crucial Hint from the Problem: Conversion is based on decimal places. So we process the thousands, hundreds, tens, and units separately. For example, $49$ is not IL ($50 – 1$). It has to be divided into $40$ (XL) and $9$ (IX). So it will be XLIX.

Hands-On Experience: The Intuition to be Greedy

My first instinct when I saw this question in an interview prep session was to write a loop that would check if the number was larger than 1000, then 500, then 100, etc. I quickly got bogged down trying to handle the “4s and 9s” with special flags. My code looked like an explosion in an if-else factory.

The breakthrough happens when you realize this is fundamentally a Greedy Algorithm problem, very similar to making change with coins.

If you owe someone $37$ in cash, and you want to use the fewest bills possible, what do you do? You grab the largest bill that is less than or equal to $37$ (a $20 bill), subtract it, and look at the remainder ($17). Then you grab a $10 bill, leaving $7. Then a $5 bill, leaving $2. Finally, you use two $1 bills.

We can do the exact same thing with Roman numerals if we treat the subtractive forms as their own unique symbols. Instead of dealing with 7 symbols, we expand our vocabulary to 13 symbols:

Value Roman Numeral Type
1000 M Standard
900 CM Subtractive
500 D Standard
400 CD Subtractive
100 C Standard
90 XC Subtractive
50 L Standard
40 XL Subtractive
10 X Standard
9 IX Subtractive
5 V Standard
4 IV Subtractive
1 I Standard

By mapping out these 13 values in descending order, our logic becomes incredibly simple:

  1. Look at the largest available Roman value.

  2. See how many times it can fit into our current number.

  3. Append that Roman symbol to our result string that many times.

  4. Reduce our number by the amount we just processed.

  5. Move to the next smallest Roman value and repeat until the number reaches 0.

Step-by-Step Code Implementations

Let’s look at how to implement this clean, greedy approach across three major programming languages: Python, JavaScript, and Java.

1. Python Solution

Python handles this beautifully using a list of tuples. We loop through our mapped values, and a nested while loop handles the subtraction.

Python

class Solution:
    def intToRoman(self, num: int) -> str:
        # Map values to their Roman numeral representations in descending order
        roman_mapping = [
            (1000, "M"), (900, "CM"), (500, "D"), (400, "CD"),
            (100, "C"), (90, "XC"), (50, "L"), (40, "XL"),
            (10, "X"), (9, "IX"), (5, "V"), (4, "IV"), (1, "I")
        ]
        
        result = []
        
        for value, symbol in roman_mapping:
            # If our number is 0, we can stop early
            if num == 0:
                break
            
            # Count how many times the current value fits into num
            count = num // value
            if count > 0:
                result.append(symbol * count)
                num %= value  # Keep the remainder
                
        return "".join(result)

2. JavaScript Solution

In JavaScript, we can use an array of objects or two parallel arrays. Using an array of objects keeps the data neatly encapsulated.

JavaScript

var intToRoman = function(num) {
    const romanMapping = [
        { value: 1000, symbol: "M" }, { value: 900, symbol: "CM" },
        { value: 500, symbol: "D" },  { value: 400, symbol: "CD" },
        { value: 100, symbol: "C" },  { value: 90, symbol: "XC" },
        { value: 50, symbol: "L" },   { value: 40, symbol: "XL" },
        { value: 10, symbol: "X" },   { value: 9, symbol: "IX" },
        { value: 5, symbol: "V" },    { value: 4, symbol: "IV" },
        { value: 1, symbol: "I" }
    ];
    
    let result = "";
    
    for (let i = 0; i < romanMapping.length; i++) {
        // Repeat the symbol while the number is greater than or equal to the value
        while (num >= romanMapping[i].value) {
            result += romanMapping[i].symbol;
            num -= romanMapping[i].value;
        }
    }
    
    return result;
};

3. Java Solution

For Java, using parallel primitive arrays (int[] and String[]) is highly optimized and memory-efficient. We also use a StringBuilder to avoid creating multiple immutable string objects in memory.

Java

class Solution {
    public String intToRoman(int num) {
        int[] values = {1000, 900, 500, 400, 100, 90, 50, 40, 10, 9, 5, 4, 1};
        String[] symbols = {"M", "CM", "D", "CD", "C", "XC", "L", "XL", "X", "IX", "V", "IV", "I"};
        
        StringBuilder result = new StringBuilder();
        
        for (int i = 0; i < values.length; i++) {
            // Deplete the current value from num as much as possible
            while (num >= values[i]) {
                result.append(symbols[i]);
                num -= values[i];
            }
        }
        
        return result.toString();
    }
}

Complexity Analysis

When explaining your solution to an interviewer, you must walk through time and space complexities. A fun twist is the analysis of this problem.

  • Time Complexity: $\mathcal{O}(1)$ (Constant Time)

    At first glance, you could say it is $\mathcal{O}(N)$ because of the loops. But the catch in this problem is that the input num will always be in between $1$ and $3999$. The maximum number of loops and operations is limited by a constant number (the maximum possible length of a Roman numeral in this range is small, at 15 characters, such as MMMCMXCIX for 3999), so the runtime is not dependent on an arbitrarily large input. The time complexity is thus strictly constant.

  • Space Complexity: $\mathcal{O}(1)$ (Constant Space)

    The additional memory we use to store our 13 mappings does not depend on the input value. The memory used for the output string is also bounded by the maximum possible length of 15 characters.

Alternative Approach: The Hardcoded “Radix” Strategy

If you want to completely eliminate loops and write something blindingly fast, there is an alternative approach: Hardcoding by place values.

Because the input is limited to 3999, we only have four decimal places to care about: Thousands, Hundreds, Tens, and Ones. We can predefine the entire Roman numeral options for each position.

Python

class Solution:
    def intToRoman(self, num: int) -> str:
        thousands = ["", "M", "MM", "MMM"]
        hundreds  = ["", "C", "CC", "CCC", "CD", "D", "DC", "DCC", "DCCC", "CM"]
        tens      = ["", "X", "XX", "XXX", "XL", "L", "LX", "LXX", "LXXX", "XC"]
        ones      = ["", "I", "II", "III", "IV", "V", "VI", "VII", "VIII", "IX"]
        
        return (thousands[num // 1000] + 
                hundreds[(num % 1000) // 100] + 
                tens[(num % 100) // 10] + 
                ones[num % 10])

Which approach is better in an interview?

Always start with the Greedy approach. It demonstrates that you can identify algorithmic patterns (like coin change mechanics) and translate them into code. The hardcoded strategy is very efficient and correct, but diving straight into it without explaining the math can feel like a “party trick”.

Frequently Asked Questions (FAQs)

1. Why is the problem constraint that the number must be < 4000?

There is no standard, universally agreed upon way to write numbers 4000 and above in traditional Roman numerals without using special overlines (called a vinculum) to multiply values by 1000. Because standard computer text strings cannot easily handle these overlines, LeetCode limits the scope to 3999 (MMMCMXCIX).

2. Is there a difference between the “Greedy” solution and the “Hardcoded” solution in terms of efficiency?

Practically, both run in $\mathcal{O}(1)$ time and space. The hardcoded solution is a bit faster at runtime since it uses direct indexing on the array and thus has fewer math operations. However, the greedy approach is much more maintainable and easy to extend if the symbol system ever changes.

3. What if I don’t use an Array of Tuples but a normal Hash Map/Dictionary?

If you use a regular hash map (like a Python dict or a JavaScript Object), you have to be careful. Standard hash maps do not guarantee the order of keys in all languages and versions. Since our greedy approach depends entirely on checking the values from largest to smallest, an ordered data structure such as an array or list is safer and more deliberate.

4. Why is 49 written as “XLIX” and not “IL”?

The rules of Roman numeral subtraction only allow a power of 10 (I, X, C) to come before the next two highest symbols. I can only be placed before V and X. It cannot be placed before L or C. Therefore, numbers must be processed strictly digit by digit according to their decimal place values.

5. How do I handle this problem if it was reversed (Roman to Integer)?

LeetCode 13 is “Roman to Integer.” You go through the string from left to right, not dividing and subtracting. If you have a smaller symbol before a larger symbol, you subtract its value from your running total; otherwise, you add it.

6. Is this problem solvable by recursion?

Yes! You can solve it recursively. Find the largest Roman symbol that fits into the number, append it, then recursively call the function with num - symbol_value. The base case occurs when num == 0. It is a clean approach but uses more memory because of the call stack.

7. Why do symbols like “V”, “L” and “D” never repeat consecutively?

VV has no point in Roman numerals, because you have X (10). Similarly, LL is replaced by C (100) and DD is replaced by M (1000). The rules say clearly that only powers of 10 (I, X, C, M) can be repeated in a row up to 3 times.

8. What is the biggest mistake people make when answering this interview question?

The biggest mistake is over-complicating the conditional logic. Candidates often write separate, deeply nested conditional statements for values ending in 4 and 9, which leads to bugs. The problem is avoided entirely by treating subtractive forms as independent, stand-alone symbols.

9. How would you test this code thoroughly in an interview?

You should offer your interviewer three distinct types of test cases:

  • Base units: Single symbols like 1 (I), 5 (V), 10 (X).

  • Edge cases (Subtractive forms): Numbers like 4 (IV), 90 (XC), 944 (CMXLIV).

  • Maximum boundary: 3999 (MMMCMXCIX), to ensure the loop bounds don’t trigger errors.

10. Does this problem come up in real software development or just interviews?

While you rarely need to output Roman numerals in day-to-day web or mobile apps, the core skill tested here—mapping values, greedy reduction, and parsing structures based on custom token systems—is very common in writing compilers, parsers, financial calculators, and inventory deduction systems.

Final Thoughts

LeetCode 12 is a classic example of a problem that looks daunting due to text-heavy rules, but collapses into an elegant few lines of code once you find the right data structure pattern.

By expanding your mapping to include the subtractive pairs, you turn a complex conditional logic nightmare into a simple, readable, greedy countdown. Keep this trick in your back pocket for your next technical interview!

Leave a Reply

Your email address will not be published. Required fields are marked *