Facebook Pixel

68. Text Justification

HardArrayStringSimulation
Leetcode Link

Problem Description

This problem asks you to format text like a word processor would, creating fully justified text (aligned on both left and right margins) with a specific line width.

Given an array of words and a maximum width maxWidth, you need to format the text so that:

  1. Each line contains exactly maxWidth characters - no more, no less.

  2. Pack words greedily - fit as many words as possible on each line. Words must be separated by at least one space.

  3. Distribute extra spaces evenly - When a line has fewer characters than maxWidth, add extra spaces between words to reach the exact width. If the extra spaces don't divide evenly among the gaps between words, the leftmost gaps get one more space than the rightmost gaps.

  4. Special case for the last line - The last line should be left-aligned (all words at the beginning with single spaces between them, and all extra spaces at the end).

  5. Special case for single-word lines - If a line contains only one word, it should be left-aligned with all extra spaces at the end.

Example walkthrough:

If words = ["This", "is", "an", "example"] and maxWidth = 16:

  • First line: "This" + "is" + "an" can fit (4 + 2 + 2 = 8 characters plus 2 spaces = 10 total)
  • We need 16 - 10 = 6 more spaces to distribute between 2 gaps
  • Each gap gets 3 spaces: "This is an"
  • Second line: "example" fits alone, so left-align: "example "

The algorithm builds lines by:

  1. Greedily adding words until no more can fit
  2. Calculating how many extra spaces are needed
  3. Distributing those spaces evenly (with leftmost gaps getting extras when uneven)
  4. Handling special cases for last line and single-word lines
Quick Interview Experience
Help others by sharing your interview experience
Have you seen this problem before?

Intuition

The key insight is to break this problem into two main phases: line formation and space distribution.

First, we need to figure out which words belong on each line. Since we want to pack as many words as possible (greedy approach), we keep adding words to the current line until adding one more would exceed maxWidth. We need to account for the minimum required spaces between words - if we have k words on a line, we need at least k-1 spaces between them.

Once we know which words go on a line, we face the challenge of distributing spaces. If a line has total character count cnt (including minimum spaces) and we need to reach maxWidth, then we have maxWidth - cnt extra spaces to distribute among the gaps between words.

The clever part is using division with remainder to distribute these spaces evenly. If we have gaps number of gaps between words and extra_spaces to distribute, then:

  • Each gap gets at least extra_spaces // gaps spaces
  • The first extra_spaces % gaps gaps get one additional space

For example, if we need to distribute 7 extra spaces among 3 gaps:

  • 7 // 3 = 2 spaces per gap minimum
  • 7 % 3 = 1 remaining space
  • So the gaps get: 3, 2, 2 spaces respectively

The special cases (last line and single-word lines) are simpler - we just left-align by joining words with single spaces and padding the right side with the remaining spaces.

This approach naturally handles all requirements: we maximize words per line through greedy packing, we achieve exact width through calculated space distribution, and we handle edge cases with conditional logic. The simulation approach works because we're essentially mimicking what a text editor would do when justifying text.

Solution Approach

The implementation follows a simulation approach where we process words sequentially and build justified lines one by one.

Main Loop Structure: We use an index i to traverse through the words array. For each iteration, we build one complete line of text.

Line Formation Phase:

  1. Start with an empty list t to hold words for the current line
  2. Initialize cnt with the length of the first word
  3. Keep adding words while checking: cnt + 1 + len(words[i]) <= maxWidth
    • The +1 accounts for the minimum space needed before the next word
  4. Update cnt by adding 1 (for space) plus the length of the new word

Space Distribution Phase: Once we have all words for a line, we handle three cases:

Case 1: Last line or single-word line (i == n or len(t) == 1):

  • Simply join words with single spaces: ' '.join(t)
  • Pad the right with spaces: ' ' * (maxWidth - len(left))
  • Append to result

Case 2: Regular line with multiple words:

  • Calculate total extra spaces needed: space_width = maxWidth - (cnt - len(t) + 1)

    • cnt includes minimum spaces between words
    • We subtract len(t) - 1 to get just the character count
    • The difference from maxWidth gives us extra spaces to distribute
  • Divide extra spaces among gaps: w, m = divmod(space_width, len(t) - 1)

    • w: base number of spaces each gap gets
    • m: number of leftmost gaps that get one extra space
  • Build the justified line:

    for j, s in enumerate(t[:-1]):
        row.append(s)
        row.append(' ' * (w + (1 if j < m else 0)))
    row.append(t[-1])
    • Add each word (except the last)
    • Add w spaces, plus 1 extra if this is one of the first m gaps
    • Finally add the last word

Data Structures:

  • ans: List to store the final justified lines
  • t: Temporary list to collect words for the current line
  • row: List to build the justified string with proper spacing

The algorithm runs in O(n) time where n is the total number of words, as each word is processed exactly once. The space complexity is O(m) where m is the total length of the output.

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 a small example with words = ["The", "quick", "brown"] and maxWidth = 10.

Building Line 1:

  • Start with i = 0, empty line t = []
  • Add "The" (3 chars): t = ["The"], cnt = 3
  • Check next word "quick": 3 + 1 + 5 = 9 ≤ 10
  • Add "quick": t = ["The", "quick"], cnt = 9
  • Check next word "brown": 9 + 1 + 5 = 15 > 10
  • Stop here, line 1 contains: ["The", "quick"]

Justifying Line 1:

  • We have 2 words with total cnt = 9 (including 1 minimum space)
  • Character count only: 3 + 5 = 8
  • Extra spaces needed: 10 - 8 = 2
  • Number of gaps: 2 - 1 = 1
  • Distribute: 2 // 1 = 2 spaces per gap, 2 % 1 = 0 remainder
  • Result: "The" + " " + "quick" = "The quick"

Building Line 2:

  • Start with i = 2, empty line t = []
  • Add "brown" (5 chars): t = ["brown"], cnt = 5
  • No more words, i = 3 = n

Justifying Line 2 (Last Line):

  • This is the last line, so left-align
  • Join with single spaces: "brown"
  • Pad right: 10 - 5 = 5 spaces
  • Result: "brown "

Final Output:

["The  quick",
 "brown     "]

Each line has exactly 10 characters. The first line distributes extra spaces evenly between words, while the last line is left-aligned with trailing spaces.

Solution Implementation

1class Solution:
2    def fullJustify(self, words: List[str], maxWidth: int) -> List[str]:
3        """
4        Formats text with full justification where each line has exactly maxWidth characters.
5      
6        Args:
7            words: List of words to be justified
8            maxWidth: Maximum width of each line
9          
10        Returns:
11            List of fully justified text lines
12        """
13        result = []
14        word_index = 0
15        total_words = len(words)
16      
17        # Process words until all are placed in lines
18        while word_index < total_words:
19            # Collect words that fit in the current line
20            current_line_words = []
21            current_line_length = len(words[word_index])
22            current_line_words.append(words[word_index])
23            word_index += 1
24          
25            # Keep adding words while they fit (accounting for minimum one space between words)
26            while word_index < total_words and current_line_length + 1 + len(words[word_index]) <= maxWidth:
27                current_line_length += 1 + len(words[word_index])
28                current_line_words.append(words[word_index])
29                word_index += 1
30          
31            # Handle last line or single-word line (left-justified)
32            if word_index == total_words or len(current_line_words) == 1:
33                # Join words with single spaces and pad right with spaces
34                left_justified = ' '.join(current_line_words)
35                right_padding = ' ' * (maxWidth - len(left_justified))
36                result.append(left_justified + right_padding)
37                continue
38          
39            # Calculate space distribution for full justification
40            # Total spaces needed = maxWidth - sum of word lengths
41            total_spaces = maxWidth - (current_line_length - len(current_line_words) + 1)
42          
43            # Distribute spaces evenly between words
44            gaps_between_words = len(current_line_words) - 1
45            base_spaces_per_gap, extra_spaces = divmod(total_spaces, gaps_between_words)
46          
47            # Build the justified line
48            justified_line = []
49            for word_position, word in enumerate(current_line_words[:-1]):
50                justified_line.append(word)
51                # Add base spaces plus one extra space for the first 'extra_spaces' gaps
52                spaces_to_add = base_spaces_per_gap + (1 if word_position < extra_spaces else 0)
53                justified_line.append(' ' * spaces_to_add)
54          
55            # Add the last word (no spaces after it)
56            justified_line.append(current_line_words[-1])
57            result.append(''.join(justified_line))
58          
59        return result
60
1class Solution {
2    public List<String> fullJustify(String[] words, int maxWidth) {
3        List<String> result = new ArrayList<>();
4        int wordIndex = 0;
5        int totalWords = words.length;
6      
7        // Process words until all are consumed
8        while (wordIndex < totalWords) {
9            // Collect words that can fit in the current line
10            List<String> currentLineWords = new ArrayList<>();
11            currentLineWords.add(words[wordIndex]);
12            int currentLineLength = words[wordIndex].length();
13            wordIndex++;
14          
15            // Add more words to current line while they fit (including minimum single space between words)
16            while (wordIndex < totalWords && 
17                   currentLineLength + 1 + words[wordIndex].length() <= maxWidth) {
18                currentLineLength += 1 + words[wordIndex].length();
19                currentLineWords.add(words[wordIndex]);
20                wordIndex++;
21            }
22          
23            // Handle last line or single word line (left-justified)
24            if (wordIndex == totalWords || currentLineWords.size() == 1) {
25                String leftJustifiedText = String.join(" ", currentLineWords);
26                String rightPadding = " ".repeat(maxWidth - leftJustifiedText.length());
27                result.add(leftJustifiedText + rightPadding);
28                continue;
29            }
30          
31            // Calculate spaces for full justification
32            // Total spaces needed = maxWidth - sum of word lengths
33            int totalSpacesNeeded = maxWidth - (currentLineLength - currentLineWords.size() + 1);
34            // Base spaces between each pair of words
35            int baseSpacesBetweenWords = totalSpacesNeeded / (currentLineWords.size() - 1);
36            // Extra spaces to distribute from left to right
37            int extraSpaces = totalSpacesNeeded % (currentLineWords.size() - 1);
38          
39            // Build the fully justified line
40            StringBuilder currentLine = new StringBuilder();
41            for (int i = 0; i < currentLineWords.size() - 1; i++) {
42                currentLine.append(currentLineWords.get(i));
43                // Add base spaces plus one extra space for the first 'extraSpaces' gaps
44                int spacesToAdd = baseSpacesBetweenWords + (i < extraSpaces ? 1 : 0);
45                currentLine.append(" ".repeat(spacesToAdd));
46            }
47            // Append the last word without trailing spaces
48            currentLine.append(currentLineWords.get(currentLineWords.size() - 1));
49          
50            result.add(currentLine.toString());
51        }
52      
53        return result;
54    }
55}
56
1class Solution {
2public:
3    vector<string> fullJustify(vector<string>& words, int maxWidth) {
4        vector<string> result;
5        int wordCount = words.size();
6      
7        for (int i = 0; i < wordCount;) {
8            // Collect words that can fit in the current line
9            vector<string> currentLineWords = {words[i]};
10            int currentLineLength = words[i].size();
11            i++;
12          
13            // Keep adding words while they fit within maxWidth (including minimum spaces)
14            while (i < wordCount && currentLineLength + 1 + words[i].size() <= maxWidth) {
15                currentLineLength += 1 + words[i].size();
16                currentLineWords.push_back(words[i]);
17                i++;
18            }
19          
20            // Handle last line or single word line (left-justified)
21            if (i == wordCount || currentLineWords.size() == 1) {
22                string leftJustifiedLine = currentLineWords[0];
23                for (int j = 1; j < currentLineWords.size(); j++) {
24                    leftJustifiedLine += " " + currentLineWords[j];
25                }
26                // Pad with spaces on the right to reach maxWidth
27                string rightPadding(maxWidth - leftJustifiedLine.size(), ' ');
28                result.push_back(leftJustifiedLine + rightPadding);
29                continue;
30            }
31          
32            // Calculate space distribution for fully justified line
33            // Total characters used by words (excluding spaces between them)
34            int totalWordChars = currentLineLength - (currentLineWords.size() - 1);
35            // Total spaces to distribute
36            int totalSpaces = maxWidth - totalWordChars;
37            // Base spaces between each word pair
38            int baseSpaces = totalSpaces / (currentLineWords.size() - 1);
39            // Extra spaces to distribute from left to right
40            int extraSpaces = totalSpaces % (currentLineWords.size() - 1);
41          
42            // Build the fully justified line
43            string justifiedLine;
44            for (int j = 0; j < currentLineWords.size() - 1; j++) {
45                justifiedLine += currentLineWords[j];
46                // Add base spaces plus one extra if within the first 'extraSpaces' gaps
47                int spacesToAdd = baseSpaces + (j < extraSpaces ? 1 : 0);
48                justifiedLine += string(spacesToAdd, ' ');
49            }
50            // Add the last word without trailing spaces
51            justifiedLine += currentLineWords.back();
52          
53            result.push_back(justifiedLine);
54        }
55      
56        return result;
57    }
58};
59
1/**
2 * Text justification algorithm that formats words into lines with full justification
3 * @param words - Array of words to be justified
4 * @param maxWidth - Maximum width of each line
5 * @returns Array of fully justified text lines
6 */
7function fullJustify(words: string[], maxWidth: number): string[] {
8    const result: string[] = [];
9    const totalWords = words.length;
10    let wordIndex = 0;
11  
12    // Process words until all are placed in lines
13    while (wordIndex < totalWords) {
14        // Build current line by adding words that fit within maxWidth
15        const currentLineWords: string[] = [words[wordIndex]];
16        let currentLineLength = words[wordIndex].length;
17        wordIndex++;
18      
19        // Keep adding words while they fit (including minimum one space between words)
20        while (wordIndex < totalWords && 
21               currentLineLength + 1 + words[wordIndex].length <= maxWidth) {
22            currentLineWords.push(words[wordIndex]);
23            currentLineLength += 1 + words[wordIndex].length;
24            wordIndex++;
25        }
26      
27        // Handle last line or single word line - left justified
28        if (wordIndex === totalWords || currentLineWords.length === 1) {
29            const leftJustifiedText = currentLineWords.join(' ');
30            const rightPadding = ' '.repeat(maxWidth - leftJustifiedText.length);
31            result.push(leftJustifiedText + rightPadding);
32            continue;
33        }
34      
35        // Calculate space distribution for full justification
36        // Total spaces needed minus the minimum single spaces between words
37        const totalSpacesToDistribute = maxWidth - (currentLineLength - currentLineWords.length + 1);
38        const gapsBetweenWords = currentLineWords.length - 1;
39        const baseSpacesPerGap = Math.floor(totalSpacesToDistribute / gapsBetweenWords);
40        const extraSpaces = totalSpacesToDistribute % gapsBetweenWords;
41      
42        // Build the fully justified line
43        const justifiedLine: string[] = [];
44        for (let i = 0; i < currentLineWords.length - 1; i++) {
45            justifiedLine.push(currentLineWords[i]);
46            // Add base spaces plus one extra space for the first 'extraSpaces' gaps
47            const spacesToAdd = baseSpacesPerGap + (i < extraSpaces ? 1 : 0);
48            justifiedLine.push(' '.repeat(spacesToAdd));
49        }
50        // Add the last word without trailing spaces
51        justifiedLine.push(currentLineWords[currentLineWords.length - 1]);
52      
53        result.push(justifiedLine.join(''));
54    }
55  
56    return result;
57}
58

Time and Space Complexity

Time Complexity: O(L) where L is the sum of the lengths of all words.

The algorithm iterates through each word exactly once using the outer while loop with index i. For each word, we:

  • Add it to the current line (constant time operations)
  • Calculate the length (accessing already known lengths)
  • When a line is complete, we format it by iterating through the words in that line once

Even though there are nested loops (the inner while loop and the formatting loop), each word is processed a constant number of times:

  • Once when added to a line
  • Once when the line is formatted

Therefore, the total time is proportional to the number of words and their lengths, giving us O(L).

Space Complexity: O(L) where L is the sum of the lengths of all words.

The space is used for:

  • The output list ans which stores all formatted lines. In the worst case, this contains all the input words plus the necessary spaces, which is bounded by O(L)
  • The temporary list t which holds words for the current line being processed. This is at most O(maxWidth) which is typically less than O(L)
  • The row list used for formatting, which is also bounded by the current line's content

Since the output must contain all the input words, the space complexity is O(L).

Learn more about how to find time and space complexity quickly.

Common Pitfalls

1. Incorrect Space Calculation Formula

One of the most common mistakes is incorrectly calculating the number of extra spaces to distribute. Many developers confuse the total character count with the space calculation.

Wrong approach:

# Incorrect: Using current_line_length directly
total_spaces = maxWidth - current_line_length

Why it's wrong: The current_line_length already includes minimum spaces between words. When we want to calculate extra spaces to distribute, we need to first remove those minimum spaces from consideration.

Correct approach:

# First, get the total character count of words only
word_chars = sum(len(word) for word in current_line_words)
# Or equivalently: current_line_length - (len(current_line_words) - 1)

# Then calculate total spaces needed
total_spaces = maxWidth - word_chars

# Finally, distribute among gaps
gaps = len(current_line_words) - 1
spaces_per_gap, extra_spaces = divmod(total_spaces, gaps)

2. Off-by-One Error in Extra Space Distribution

Another frequent mistake is incorrectly determining which gaps should receive extra spaces when they don't divide evenly.

Wrong approach:

# Incorrect: Using <= instead of <
spaces_to_add = base_spaces + (1 if word_position <= extra_spaces else 0)

Why it's wrong: If we have 3 extra spaces and 4 gaps (positions 0, 1, 2, 3), using <= would distribute extra spaces to positions 0, 1, 2, AND 3 (4 spaces total), which is one too many.

Correct approach:

# Correct: First 'extra_spaces' gaps get one additional space
spaces_to_add = base_spaces + (1 if word_position < extra_spaces else 0)

3. Forgetting to Handle Edge Cases Properly

The problem has two critical edge cases that are easy to miss or handle incorrectly:

Missing edge case handling:

# Wrong: Only checking for last line
if word_index == total_words:
    # Left justify

Why it's wrong: Single-word lines that aren't the last line also need to be left-justified, not center or right-justified.

Correct approach:

# Check for BOTH last line AND single-word lines
if word_index == total_words or len(current_line_words) == 1:
    left_justified = ' '.join(current_line_words)
    result.append(left_justified + ' ' * (maxWidth - len(left_justified)))

4. Division by Zero When Line Has Only One Word

If not handled properly, attempting to distribute spaces when there's only one word can cause a division by zero error.

Wrong approach:

# Dangerous: No check for single word case
gaps = len(current_line_words) - 1  # Could be 0!
spaces_per_gap = total_spaces // gaps  # Division by zero!

Correct approach:

# Handle single word case before space distribution
if len(current_line_words) == 1:
    # Left justify with padding
    result.append(current_line_words[0] + ' ' * (maxWidth - len(current_line_words[0])))
else:
    # Safe to divide by gaps now
    gaps = len(current_line_words) - 1
    spaces_per_gap, extra_spaces = divmod(total_spaces, gaps)
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