Facebook Pixel

166. Fraction to Recurring Decimal

MediumHash TableMathString
Leetcode Link

Problem Description

This problem asks you to convert a fraction (given as numerator and denominator) into its decimal string representation.

Given two integers numerator and denominator representing a fraction, you need to return the fraction as a string in decimal format.

The key challenge is handling repeating decimals. When the decimal part repeats indefinitely (like 1/3 = 0.333... or 1/6 = 0.1666...), you must identify the repeating pattern and enclose it in parentheses.

For example:

  • 1/2 = "0.5" (non-repeating decimal)
  • 2/1 = "2" (integer result)
  • 2/3 = "0.(6)" (the 6 repeats infinitely)
  • 4/333 = "0.(012)" (the sequence 012 repeats infinitely)
  • 1/6 = "0.1(6)" (only the 6 repeats after 0.1)

The solution uses long division simulation combined with a hash table to detect when a remainder repeats. When performing long division manually, if you encounter the same remainder twice, you know the decimal will start repeating from that point. The hash table tracks each remainder and its position in the result string. When a remainder appears again, we know where to insert the opening parenthesis (at the position where this remainder first appeared) and where to add the closing parenthesis (at the current end).

The algorithm handles several edge cases:

  • Zero numerator results in "0"
  • Negative results (when numerator and denominator have different signs)
  • Integer results (no decimal part)
  • Finite decimals (decimal terminates without repeating)
  • Infinite repeating decimals (the main challenge)
Quick Interview Experience
Help others by sharing your interview experience
Have you seen this problem before?

Intuition

The key insight comes from understanding how long division works. When we divide two numbers manually, we repeatedly multiply the remainder by 10 and divide again. This process continues until either the remainder becomes 0 (finite decimal) or we encounter a remainder we've seen before (repeating decimal).

Think about dividing 1/3 by hand:

  • 1 ÷ 3 = 0 with remainder 1
  • Multiply remainder by 10: 10 ÷ 3 = 3 with remainder 1
  • Multiply remainder by 10: 10 ÷ 3 = 3 with remainder 1
  • We keep getting remainder 1, so the digit 3 repeats forever

This observation leads to the crucial realization: if we ever see the same remainder twice during long division, the decimal will repeat from that point onward. The pattern between the first and second occurrence of the same remainder will repeat infinitely.

Why does this happen? Because once we have the same remainder, the subsequent division steps will be identical to what we did before. It's like being stuck in a loop - same input (remainder) leads to same output (quotient digit and new remainder).

To detect this repetition, we need to remember which remainders we've seen and where they occurred in our result. A hash table is perfect for this - we store each remainder as a key and its position in the result string as the value.

When we encounter a remainder that's already in our hash table, we know:

  • The repeating part starts at the position stored in the hash table
  • The repeating part ends at the current position
  • We need to insert parentheses around this repeating section

The beauty of this approach is that it naturally handles all cases:

  • If remainder becomes 0, we have a finite decimal
  • If remainder never repeats (within reasonable bounds), we have a non-repeating decimal
  • If remainder repeats, we've found our cycle and can mark it with parentheses

Solution Approach

Let's walk through the implementation step by step:

Step 1: Handle Edge Cases First, check if the numerator is 0. If it is, return "0" directly since any zero divided by a non-zero number equals zero.

Step 2: Handle Sign Determine if the result should be negative by checking if the numerator and denominator have different signs using XOR operation: (numerator > 0) ^ (denominator > 0). If true, append "-" to the result.

Step 3: Work with Absolute Values Convert both numerator and denominator to their absolute values to simplify the division process. Store them as a and b respectively.

Step 4: Calculate Integer Part Calculate the integer part using integer division: a // b. Convert it to a string and append to the result. Then update a to the remainder: a %= b.

If a becomes 0 after this step, the division is exact (no decimal part), so return the result.

Step 5: Process Decimal Part If there's a remainder, append "." to the result and start processing the decimal part.

Create a hash table d to track remainders. The key is the remainder value, and the value is the position in the result string where this remainder first appeared.

Step 6: Long Division Simulation Perform long division using a while loop:

  • Record the current remainder a and its position in the result: d[a] = len(ans)
  • Multiply the remainder by 10: a *= 10
  • Calculate the next digit: a // b and append it to the result
  • Update the remainder: a %= b
  • Check if this new remainder exists in the hash table:
    • If yes, we've found a repeating cycle. Insert "(" at the position where this remainder first appeared (d[a]) and append ")" at the end, then break
    • If no, continue the loop

Step 7: Return Result Join all parts of the result array into a final string and return it.

Example Walkthrough: 1/6

  • Integer part: 1 // 6 = 0, remainder = 1
  • Decimal processing:
    • Remainder 1: position 2, 1 * 10 = 10, 10 // 6 = 1, new remainder = 4
    • Remainder 4: position 3, 4 * 10 = 40, 40 // 6 = 6, new remainder = 4
    • Remainder 4 already seen at position 3! Insert "(" at position 3 and ")" at end
  • Result: "0.1(6)"

The time complexity is O(denominator) in the worst case since there can be at most denominator unique remainders. The space complexity is also O(denominator) for storing the hash table and result 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 work through the example 4/333 step by step to see how we get "0.(012)".

Initial Setup:

  • numerator = 4, denominator = 333
  • Both positive, so no negative sign needed
  • Integer part: 4 ÷ 333 = 0 with remainder 4
  • Result so far: "0."
  • Create hash table d = {} to track remainders

Long Division Process:

First iteration:

  • Current remainder: 4
  • Record position: d[4] = 2 (position after decimal point)
  • Multiply by 10: 4 × 10 = 40
  • Divide: 40 ÷ 333 = 0 with remainder 40
  • Append digit: Result = "0.0"
  • Check: 40 not in hash table, continue

Second iteration:

  • Current remainder: 40
  • Record position: d[40] = 3
  • Multiply by 10: 40 × 10 = 400
  • Divide: 400 ÷ 333 = 1 with remainder 67
  • Append digit: Result = "0.01"
  • Check: 67 not in hash table, continue

Third iteration:

  • Current remainder: 67
  • Record position: d[67] = 4
  • Multiply by 10: 67 × 10 = 670
  • Divide: 670 ÷ 333 = 2 with remainder 4
  • Append digit: Result = "0.012"
  • Check: 4 IS in hash table at position 2!

Detecting the Cycle:

  • Remainder 4 appeared at position 2 and now again
  • The repeating part is from position 2 to current end
  • Insert "(" at position 2: "0.(012"
  • Append ")" at end: "0.(012)"

Final Result: "0.(012)"

The pattern "012" will repeat forever because we're back to remainder 4, which will produce the same sequence of digits again.

Solution Implementation

1class Solution:
2    def fractionToDecimal(self, numerator: int, denominator: int) -> str:
3        # Handle zero numerator case
4        if numerator == 0:
5            return "0"
6      
7        result = []
8      
9        # Determine if result should be negative
10        # XOR returns True if signs are different
11        is_negative = (numerator > 0) ^ (denominator > 0)
12        if is_negative:
13            result.append("-")
14      
15        # Work with absolute values to simplify calculation
16        dividend, divisor = abs(numerator), abs(denominator)
17      
18        # Add the integer part
19        result.append(str(dividend // divisor))
20      
21        # Calculate remainder
22        remainder = dividend % divisor
23      
24        # If no remainder, return the integer result
25        if remainder == 0:
26            return "".join(result)
27      
28        # Add decimal point for fractional part
29        result.append(".")
30      
31        # Dictionary to track remainders and their positions
32        # Used to detect repeating decimals
33        remainder_positions = {}
34      
35        # Process the fractional part
36        while remainder:
37            # Store current remainder's position in result
38            remainder_positions[remainder] = len(result)
39          
40            # Perform long division: multiply remainder by 10
41            remainder *= 10
42          
43            # Add the next digit to result
44            result.append(str(remainder // divisor))
45          
46            # Calculate new remainder
47            remainder = remainder % divisor
48          
49            # Check if we've seen this remainder before (repeating pattern)
50            if remainder in remainder_positions:
51                # Insert opening parenthesis at the start of repeating sequence
52                result.insert(remainder_positions[remainder], "(")
53                # Add closing parenthesis at the end
54                result.append(")")
55                break
56      
57        return "".join(result)
58
1class Solution {
2    public String fractionToDecimal(int numerator, int denominator) {
3        // Handle zero numerator case
4        if (numerator == 0) {
5            return "0";
6        }
7      
8        StringBuilder result = new StringBuilder();
9      
10        // Determine if result should be negative
11        // XOR returns true only when signs are different
12        boolean isNegative = (numerator > 0) ^ (denominator > 0);
13        if (isNegative) {
14            result.append("-");
15        }
16      
17        // Convert to long to avoid integer overflow
18        long dividend = Math.abs((long) numerator);
19        long divisor = Math.abs((long) denominator);
20      
21        // Append integer part
22        result.append(dividend / divisor);
23      
24        // Calculate remainder
25        dividend %= divisor;
26      
27        // If remainder is 0, there's no fractional part
28        if (dividend == 0) {
29            return result.toString();
30        }
31      
32        // Append decimal point for fractional part
33        result.append(".");
34      
35        // Map to store remainder and its corresponding position in result
36        // Used to detect repeating patterns
37        Map<Long, Integer> remainderToPosition = new HashMap<>();
38      
39        // Process fractional part
40        while (dividend != 0) {
41            // Store current remainder and its position
42            remainderToPosition.put(dividend, result.length());
43          
44            // Multiply remainder by 10 for next digit calculation
45            dividend *= 10;
46          
47            // Append next digit of fractional part
48            result.append(dividend / divisor);
49          
50            // Calculate new remainder
51            dividend %= divisor;
52          
53            // Check if this remainder has appeared before (repeating pattern found)
54            if (remainderToPosition.containsKey(dividend)) {
55                // Insert opening parenthesis at the start of repeating sequence
56                int repeatStartPosition = remainderToPosition.get(dividend);
57                result.insert(repeatStartPosition, "(");
58                // Append closing parenthesis at the end
59                result.append(")");
60                break;
61            }
62        }
63      
64        return result.toString();
65    }
66}
67
1class Solution {
2public:
3    string fractionToDecimal(int numerator, int denominator) {
4        // Handle zero numerator case
5        if (numerator == 0) {
6            return "0";
7        }
8      
9        string result;
10      
11        // Determine if result should be negative
12        // XOR returns true when signs are different
13        bool isNegative = (numerator > 0) ^ (denominator > 0);
14        if (isNegative) {
15            result += "-";
16        }
17      
18        // Convert to long long to avoid overflow when taking absolute values
19        long long dividend = abs(static_cast<long long>(numerator));
20        long long divisor = abs(static_cast<long long>(denominator));
21      
22        // Add the integer part
23        result += to_string(dividend / divisor);
24      
25        // Calculate remainder
26        dividend %= divisor;
27      
28        // If no remainder, we have a terminating decimal
29        if (dividend == 0) {
30            return result;
31        }
32      
33        // Add decimal point for fractional part
34        result += ".";
35      
36        // Map to store remainder positions for detecting repeating patterns
37        // Key: remainder value, Value: position in result string
38        unordered_map<long long, int> remainderToPosition;
39      
40        // Process the fractional part
41        while (dividend != 0) {
42            // Record current remainder and its position in result
43            remainderToPosition[dividend] = result.size();
44          
45            // Multiply remainder by 10 for next digit calculation
46            dividend *= 10;
47          
48            // Append next digit to result
49            result += to_string(dividend / divisor);
50          
51            // Update remainder for next iteration
52            dividend %= divisor;
53          
54            // Check if we've seen this remainder before (indicates repeating pattern)
55            if (remainderToPosition.count(dividend)) {
56                // Insert opening parenthesis at the start of repeating sequence
57                result.insert(remainderToPosition[dividend], "(");
58                // Add closing parenthesis at the end
59                result += ")";
60                break;
61            }
62        }
63      
64        return result;
65    }
66};
67
1/**
2 * Converts a fraction to its decimal representation as a string
3 * Handles repeating decimals by enclosing them in parentheses
4 * @param numerator - The numerator of the fraction
5 * @param denominator - The denominator of the fraction
6 * @returns The decimal representation as a string
7 */
8function fractionToDecimal(numerator: number, denominator: number): string {
9    // Handle zero numerator case
10    if (numerator === 0) {
11        return '0';
12    }
13  
14    // Result string builder array
15    const result: string[] = [];
16  
17    // Check if result should be negative (XOR of signs)
18    const isNegative: boolean = (numerator > 0) !== (denominator > 0);
19    if (isNegative) {
20        result.push('-');
21    }
22  
23    // Work with absolute values to simplify calculation
24    let dividend: number = Math.abs(numerator);
25    let divisor: number = Math.abs(denominator);
26  
27    // Add the integer part of the division
28    result.push(Math.floor(dividend / divisor).toString());
29  
30    // Calculate remainder
31    dividend %= divisor;
32  
33    // If no remainder, return the integer result
34    if (dividend === 0) {
35        return result.join('');
36    }
37  
38    // Add decimal point for fractional part
39    result.push('.');
40  
41    // Map to track remainders and their positions for detecting repeating patterns
42    const remainderToPosition: Map<number, number> = new Map();
43  
44    // Process the decimal part
45    while (dividend !== 0) {
46        // Store current remainder and its position in result array
47        remainderToPosition.set(dividend, result.length);
48      
49        // Multiply remainder by 10 for next decimal digit
50        dividend *= 10;
51      
52        // Add next decimal digit to result
53        result.push(Math.floor(dividend / divisor).toString());
54      
55        // Calculate new remainder
56        dividend %= divisor;
57      
58        // Check if we've seen this remainder before (indicates repeating pattern)
59        if (remainderToPosition.has(dividend)) {
60            // Insert opening parenthesis at the start of repeating sequence
61            const repeatStartPosition: number = remainderToPosition.get(dividend)!;
62            result.splice(repeatStartPosition, 0, '(');
63          
64            // Add closing parenthesis at the end
65            result.push(')');
66            break;
67        }
68    }
69  
70    // Join all parts and return the final decimal string
71    return result.join('');
72}
73

Time and Space Complexity

The time complexity is O(l), where l is the length of the resulting decimal string. The algorithm performs long division, and each iteration processes one digit of the decimal representation. The key insight is that the remainder must be less than the denominator, so there are at most denominator - 1 possible remainders. Once a remainder repeats (detected by checking if a in d), we've found the repeating cycle and the algorithm terminates. Therefore, the loop runs at most O(denominator) times, but since the output length l is bounded by the number of unique remainders plus the integer part, the complexity is effectively O(l).

The space complexity is O(l) as well. The algorithm uses:

  • The ans list to store the result string, which has length l
  • The dictionary d to store remainders and their positions, which can have at most O(denominator) entries, but since this is bounded by the output length l, it's O(l)

In this problem, the constraint ensures l < 10^4 because the decimal representation either terminates or has a repeating cycle with period at most denominator - 1.

Common Pitfalls

1. Integer Overflow When Using 32-bit Integers

One of the most critical pitfalls is integer overflow when working with extreme values. In languages with fixed-size integers (like Java with int), operations like Math.abs(Integer.MIN_VALUE) can cause overflow because the absolute value of -2^31 is 2^31, which exceeds the maximum value for a 32-bit signed integer (2^31 - 1).

Example Problem Case:

  • numerator = -2147483648 (Integer.MIN_VALUE)
  • denominator = 1
  • Math.abs(-2147483648) returns -2147483648 due to overflow!

Solution: Use 64-bit integers (long in Java, long long in C++) from the start to handle the conversion safely:

# Python handles arbitrary precision automatically, but in Java/C++:
# long dividend = Math.abs((long)numerator);
# long divisor = Math.abs((long)denominator);

2. Incorrect Sign Handling with XOR Logic

The XOR approach (numerator > 0) ^ (denominator > 0) fails to properly identify negative results when comparing with zero directly in some implementations.

Example Problem Case:

  • numerator = -1, denominator = -3
  • Both negative, should give positive result
  • Incorrect implementation might add "-" sign

Solution: Be explicit about the sign check:

# More explicit and readable approach
is_negative = (numerator < 0 and denominator > 0) or (numerator > 0 and denominator < 0)
# Or use multiplication check
is_negative = (numerator * denominator) < 0

3. Not Handling the Remainder Position Correctly

A subtle but important pitfall is recording the remainder position AFTER appending the digit instead of BEFORE. This leads to incorrect parenthesis placement.

Incorrect Implementation:

while remainder:
    remainder *= 10
    result.append(str(remainder // divisor))
    remainder_positions[remainder] = len(result)  # Wrong! Position recorded after append
    remainder = remainder % divisor

Correct Implementation:

while remainder:
    remainder_positions[remainder] = len(result)  # Record position BEFORE append
    remainder *= 10
    result.append(str(remainder // divisor))
    remainder = remainder % divisor

4. Forgetting to Check Zero Remainder in the Loop

Not checking if the remainder becomes zero during decimal processing can lead to attempting to access a non-existent key in the dictionary.

Solution: The while loop condition while remainder: automatically handles this, but if using a different loop structure, explicitly check:

if remainder == 0:
    break  # Decimal terminates, no repeating pattern

5. String Concatenation Performance Issues

Using string concatenation in a loop (like result += str(digit)) can be inefficient in some languages due to string immutability.

Solution: Use a list/array to collect parts and join at the end (as shown in the correct implementation):

result = []
# ... append to result ...
return "".join(result)
Discover Your Strengths and Weaknesses: Take Our 5-Minute Quiz to Tailor Your Study Plan:

What's the output of running the following function using input [30, 20, 10, 100, 33, 12]?

1def fun(arr: List[int]) -> List[int]:
2    import heapq
3    heapq.heapify(arr)
4    res = []
5    for i in range(3):
6        res.append(heapq.heappop(arr))
7    return res
8
1public static int[] fun(int[] arr) {
2    int[] res = new int[3];
3    PriorityQueue<Integer> heap = new PriorityQueue<>();
4    for (int i = 0; i < arr.length; i++) {
5        heap.add(arr[i]);
6    }
7    for (int i = 0; i < 3; i++) {
8        res[i] = heap.poll();
9    }
10    return res;
11}
12
1class HeapItem {
2    constructor(item, priority = item) {
3        this.item = item;
4        this.priority = priority;
5    }
6}
7
8class MinHeap {
9    constructor() {
10        this.heap = [];
11    }
12
13    push(node) {
14        // insert the new node at the end of the heap array
15        this.heap.push(node);
16        // find the correct position for the new node
17        this.bubble_up();
18    }
19
20    bubble_up() {
21        let index = this.heap.length - 1;
22
23        while (index > 0) {
24            const element = this.heap[index];
25            const parentIndex = Math.floor((index - 1) / 2);
26            const parent = this.heap[parentIndex];
27
28            if (parent.priority <= element.priority) break;
29            // if the parent is bigger than the child then swap the parent and child
30            this.heap[index] = parent;
31            this.heap[parentIndex] = element;
32            index = parentIndex;
33        }
34    }
35
36    pop() {
37        const min = this.heap[0];
38        this.heap[0] = this.heap[this.size() - 1];
39        this.heap.pop();
40        this.bubble_down();
41        return min;
42    }
43
44    bubble_down() {
45        let index = 0;
46        let min = index;
47        const n = this.heap.length;
48
49        while (index < n) {
50            const left = 2 * index + 1;
51            const right = left + 1;
52
53            if (left < n && this.heap[left].priority < this.heap[min].priority) {
54                min = left;
55            }
56            if (right < n && this.heap[right].priority < this.heap[min].priority) {
57                min = right;
58            }
59            if (min === index) break;
60            [this.heap[min], this.heap[index]] = [this.heap[index], this.heap[min]];
61            index = min;
62        }
63    }
64
65    peek() {
66        return this.heap[0];
67    }
68
69    size() {
70        return this.heap.length;
71    }
72}
73
74function fun(arr) {
75    const heap = new MinHeap();
76    for (const x of arr) {
77        heap.push(new HeapItem(x));
78    }
79    const res = [];
80    for (let i = 0; i < 3; i++) {
81        res.push(heap.pop().item);
82    }
83    return res;
84}
85

Recommended Readings

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

Load More