166. Fraction to Recurring Decimal
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)
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 remainder1
- Multiply remainder by 10:
10 ÷ 3 = 3
with remainder1
- Multiply remainder by 10:
10 ÷ 3 = 3
with remainder1
- We keep getting remainder
1
, so the digit3
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
- If yes, we've found a repeating cycle. Insert
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
- Remainder
- 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 EvaluatorExample 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 remainder4
- 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 remainder40
- 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 remainder67
- 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 remainder4
- Append digit: Result =
"0.012"
- Check:
4
IS in hash table at position2
!
Detecting the Cycle:
- Remainder
4
appeared at position2
and now again - The repeating part is from position
2
to current end - Insert
"("
at position2
:"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 lengthl
- The dictionary
d
to store remainders and their positions, which can have at mostO(denominator)
entries, but since this is bounded by the output lengthl
, it'sO(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)
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
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!