← 題庫 / Archive
2026-05-19 TI150 Hard ArrayStringSimulation

68. Text Justification

題目 / Problem

中文: 給定字串陣列 words 與一個寬度 maxWidth,將文字排版,使每一行恰好有 maxWidth 個字元,並且左右兩端對齊(fully justified)。 - 採用「貪心」策略:每一行盡可能塞入越多單字越好。 - 不足的位置用空格 ' ' 補滿,使每行長度都剛好等於 maxWidth。 - 單字之間的「多餘空格」要儘量平均分配;若不能整除,左邊的空隙比右邊多分一格。 - 最後一行例外:靠左對齊,單字之間只用一個空格,剩下的空格全補在最右邊。 - 一行只有一個單字時,也視同靠左對齊(後面補空格)。

English: Given an array of strings words and a width maxWidth, lay out the text so every line is exactly maxWidth characters and is fully (left + right) justified. - Greedy packing: cram as many words into the current line as will fit. - Pad with spaces ' ' so each line is exactly maxWidth long. - Between-word spaces are spread as evenly as possible; if they don't divide evenly, the left gaps get one extra space each. - Last line is special: left-justified, single spaces between words, all remaining padding pushed to the right. - A line that contains only one word is also left-justified.

Constraints - 1 <= words.length <= 300 - 1 <= words[i].length <= 20 - 1 <= maxWidth <= 100 - words[i].length <= maxWidth

Worked example

Input : words = ["This","is","an","example","of","text","justification."], maxWidth = 16
Output:
[
  "This    is    an",   // 8 letters + 8 extra spaces split over 2 gaps → 4 + 4
  "example  of text",   // 13 letters + 3 extra spaces over 2 gaps → 2 + 1 (left gap gets the extra)
  "justification.  "    // last line: left-justified, trailing spaces
]

名詞解釋 / Glossary

  • 貪心 / Greedy:每一步都做「當下看起來最好的選擇」,不回頭。這題每行盡量塞滿單字就是貪心。Make the locally best choice (cram in one more word if it fits) without backtracking.
  • 完全對齊 / Full justification:一行的最左字元在最左、最右字元在最右,中間以空格擴展。In typesetting, both left and right edges align to the margins; extra spaces sit between words.
  • 左對齊 / Left justification:單字之間單空格,行尾補空格。Single space between words, padding placed at the end of the line.
  • 整數除法與餘數 / Integer division & moduloa / b(C/C++ 下整數)取商,a % b 取餘。我們用商當作「每個空隙的基本空格數」,用餘數當作「前幾個空隙要多 1 空格」。Quotient = base gap size; remainder = number of left-most gaps that get one extra space.
  • strlen / std::string::size():求字串長度。C 用 strlen(O(L) 掃 \0),C++ 用 size()(O(1) 存好)。Both return the length of the string; the C++ version is constant-time because the size is cached.
  • malloc / memcpy:C 的動態記憶體配置與位元組複製。LeetCode 的 C 介面要求結果在堆上分配,呼叫方負責釋放。Allocate raw memory and copy bytes; required because LeetCode's C harness expects heap-allocated strings.
  • 指標陣列 / Array of pointerschar** 代表「指向多個字串的指標」,每個元素自己是一條已配置的 char*。An array where each entry points to a separately allocated C-string.
  • std::string::append(n, ch):把字元 ch 重複 n 次接到字串尾巴。Appends n copies of ch — convenient for padding without a manual loop.

思路

這題乍看是字串題,其實是模擬 + 貪心分組。把流程拆成兩個獨立子問題就很清楚:第一步「決定哪些單字放在同一行」,第二步「給定這幾個單字,把這行排版出來」。第一步用雙指標:用 i 標記這一行第一個單字,往後試著放入 words[j],只要「目前已放字母總數 + 即將加入的單字長度 + 至少需要的空格數 (j - i)」不超過 maxWidth 就繼續放(j - i 表示已有 j - i 個單字,再多放一個就需要 j - i 個分隔空格)。一旦放不下就停下,這行的單字就是 words[i..j-1]。第二步分兩種情況:若 j == n(最後一行)或只有一個單字,就用單空格串起來、右邊補滿;否則計算剩餘空格 totalSpaces = maxWidth - 字母總長,平均分到 gaps = wordCount - 1 個空隙:每隙至少 totalSpaces / gaps 個空格,前 totalSpaces % gaps 個空隙再多分一個。為什麼這樣分配?題目說「左邊空隙比右邊多」,餘數從最左邊的 gap 開始一個一個發即可。整個過程沒有 DP、沒有遞迴,只是把規則照實寫出來,這也是為什麼這題被歸在 simulation。

The trick is to see this as simulation + a greedy grouping pass, not a clever string problem. Split it into two sub-problems. First, decide which words land on each line. Walk a two-pointer window: i marks the first word of the current line, advance j while (running letter count) + words[j].length + (j - i)maxWidth. The extra (j - i) term is the minimum number of single-space separators we'd need if we added one more word — we must reserve those spaces or we're cheating. Second, lay out the line. If we hit the end of the input or the line has only one word, it's left-justified: join with single spaces, pad the right. Otherwise compute totalSpaces = maxWidth - letterCount and split it across gaps = wordCount - 1 slots. Each slot gets totalSpaces / gaps spaces, and the first totalSpaces % gaps slots get one extra. That's exactly the "left-most gaps get the leftover" rule from the spec. No DP, no recursion — just translate the typesetting rules into code. The reason a greedy line-fill is optimal here is that the problem defines the layout as greedy, so we don't need to prove anything; we just follow the recipe.

逐步走查 / Walkthrough

Input: words = ["This","is","an","example","of","text","justification."], maxWidth = 16.

Phase 1 — choose words per line (i = first word index, j = first word that doesn't fit).

step i j candidate fit check action
1 0 0 0 + 4("This") + 0 = 4 ≤ 16 take, lineLen=4, j=1
2 0 1 4 + 2("is") + 1 = 7 ≤ 16 take, lineLen=6, j=2
3 0 2 6 + 2("an") + 2 = 10 ≤ 16 take, lineLen=8, j=3
4 0 3 8 + 7("example") + 3 = 18 > 16 stop. Line 1 = words[0..2]

Build line 1: wordCount=3, totalSpaces=16-8=8, gaps=2, base=4, extra=0. Result: "This" + 4sp + "is" + 4sp + "an""This is an" (length 16 ✓).

step i j fit check action
5 3 3 0 + 7 + 0 = 7 ≤ 16 take "example", lineLen=7, j=4
6 3 4 7 + 2 + 1 = 10 ≤ 16 take "of", lineLen=9, j=5
7 3 5 9 + 4 + 2 = 15 ≤ 16 take "text", lineLen=13, j=6
8 3 6 13 + 14 + 3 = 30 > 16 stop. Line 2 = words[3..5]

Build line 2: totalSpaces=3, gaps=2, base=1, extra=1. Gap #0 gets 1+1=2, gap #1 gets 1. Result: "example" + 2sp + "of" + 1sp + "text""example of text" (length 16 ✓).

step i j fit check action
9 6 6 0 + 14 + 0 = 14 ≤ 16 take "justification.", j=7
10 6 7 j == n, stop Line 3 = last line

Build line 3 (last line → left-justify): "justification." + " ""justification. " (length 16 ✓).

Final output matches the expected three lines.

Solution — C

// 演算法:兩階段 / Two-phase algorithm.
// 1) 用雙指標貪心地把單字分組到每一行 / Two-pointer greedy grouping per line.
// 2) 對每一行依「最後一行 / 單字行 / 一般行」三種情況排版 / Format each line by case.
// 時間 O(總字元數), 空間 O(輸出大小) / Time O(total chars), space O(output size).

#include <stdlib.h>   // malloc, free / 動態記憶體
#include <string.h>   // strlen, memcpy / 字串長度與位元組複製

char** fullJustify(char** words, int wordsSize, int maxWidth, int* returnSize) {
    // 最多每個單字一行,所以行數上限 = wordsSize / At most one word per line, so cap = wordsSize.
    char** result = (char**)malloc(sizeof(char*) * wordsSize);
    int lineCount = 0;            // 已產生的行數 / number of lines produced so far
    int i = 0;                    // 這一行的第一個單字 index / first word index of current line

    while (i < wordsSize) {
        // ---- 階段 1:決定這一行放哪些單字 / Pick words that fit on this line ----
        int j = i;                // j 會掃到第一個放不下的單字 / j scans to the first word that won't fit
        int lineLen = 0;          // 只計算字母長度,不含空格 / letter length only, no spaces yet
        // 條件:放完 words[j] 後仍需 (j - i) 個分隔空格 / after taking words[j] we still need (j-i) separators
        while (j < wordsSize &&
               lineLen + (int)strlen(words[j]) + (j - i) <= maxWidth) {
            lineLen += (int)strlen(words[j]);  // 累加字母長度 / accumulate letter count
            j++;                               // 嘗試下一個單字 / try next word
        }
        // 此時 words[i..j-1] 是這行的單字 / words[i..j-1] are the chosen words

        int wordCount   = j - i;                  // 這行有幾個單字 / how many words on this line
        int totalSpaces = maxWidth - lineLen;     // 還需要塞入幾個空格 / total spaces to place

        // ---- 階段 2:把這行字串建出來 / Build the line string ----
        // 配置 maxWidth + 1 個 byte(多 1 個放 '')/ allocate maxWidth + 1 bytes (one for '')
        char* line = (char*)malloc(maxWidth + 1);
        int pos = 0;                              // line 目前的寫入位置 / current write index in line

        // 最後一行 OR 只有 1 個單字 → 左對齊 / Last line or single-word line → left-justify
        if (j == wordsSize || wordCount == 1) {
            for (int k = i; k < j; k++) {
                int len = (int)strlen(words[k]);
                memcpy(line + pos, words[k], len);    // 複製單字到 line / copy word bytes
                pos += len;                            // 移動寫入指標 / advance write pointer
                if (k < j - 1) {                       // 不是最後一個單字 / not the last word
                    line[pos++] = ' ';                 // 單空格分隔 / single space separator
                }
            }
            while (pos < maxWidth) line[pos++] = ' '; // 行尾補空格 / pad trailing spaces
        } else {
            // 一般行:平均分配空格 / Regular line: distribute spaces evenly
            int gaps       = wordCount - 1;            // 空隙數 / number of gaps
            int baseSpaces = totalSpaces / gaps;       // 每隙的基本空格 / base spaces per gap
            int extra      = totalSpaces % gaps;       // 餘下的空格從左邊先發 / leftover spaces, leftmost first

            for (int k = i; k < j; k++) {
                int len = (int)strlen(words[k]);
                memcpy(line + pos, words[k], len);     // 寫入單字 / write the word
                pos += len;
                if (k < j - 1) {                       // 不是最後一個就要放空格 / not last → add gap
                    // 前 extra 個空隙多放 1 個空格 / first `extra` gaps get one extra space
                    int spaces = baseSpaces + ((k - i) < extra ? 1 : 0);
                    for (int s = 0; s < spaces; s++) line[pos++] = ' ';
                }
            }
        }
        line[maxWidth] = '';        // C 字串結尾 / null-terminate the C string
        result[lineCount++] = line;   // 收進結果陣列 / store the line in the result array
        i = j;                        // 下一行從 j 開始 / next line begins at j
    }

    *returnSize = lineCount;          // 透過輸出參數回傳行數 / report total lines back via out-param
    return result;                    // 回傳指向所有行的指標陣列 / return the array of line pointers
}

Solution — C++

// 演算法同 C 版:貪心分組 + 三種情況排版 / Same algorithm as C: greedy grouping + 3-case formatting.
// 用 std::string 自動管理記憶體,append(n, ch) 直接重複空格 / std::string handles memory; append(n,ch) repeats a char.

#include <string>
#include <vector>
using namespace std;

class Solution {
public:
    vector<string> fullJustify(vector<string>& words, int maxWidth) {
        vector<string> result;          // 最終輸出 / final output lines
        int n = (int)words.size();      // 單字總數 / total word count
        int i = 0;                      // 當前行的第一個單字 / first word index of the current line

        while (i < n) {
            // ---- 階段 1:找出能放入這行的單字範圍 [i, j) / Find the word range that fits ----
            int j = i;                  // j 會停在第一個放不下的單字 / j stops at the first word that won't fit
            int lineLen = 0;            // 字母總長(不含空格)/ letter-only length so far
            while (j < n &&
                   lineLen + (int)words[j].size() + (j - i) <= maxWidth) {
                lineLen += (int)words[j].size();   // 加上這個單字的字母數 / add letter count
                ++j;                               // 試下一個單字 / try next
            }

            int wordCount   = j - i;                // 這行單字數量 / number of words this line
            int totalSpaces = maxWidth - lineLen;   // 剩餘要填入的空格 / spaces left to fill

            // ---- 階段 2:根據情況組裝這行 / Build the line by case ----
            string line;
            line.reserve(maxWidth);                 // 預先保留容量避免重新配置 / reserve to avoid reallocs

            if (j == n || wordCount == 1) {
                // 最後一行或單字行 → 左對齊 / Last line or single-word line → left-justify
                for (int k = i; k < j; ++k) {
                    line += words[k];               // 串接單字 / append the word
                    if (k < j - 1) line += ' ';     // 單空格分隔 / single space between words
                }
                line.append(maxWidth - line.size(), ' '); // 右側補空格 / pad the right side
            } else {
                // 一般行:平均分配 / Regular line: distribute spaces evenly
                int gaps       = wordCount - 1;      // 空隙數 / gap count
                int baseSpaces = totalSpaces / gaps; // 每隙基本空格 / base per gap
                int extra      = totalSpaces % gaps; // 左側多分的空格數 / extras going to left gaps

                for (int k = i; k < j; ++k) {
                    line += words[k];                // 接上單字 / append word
                    if (k < j - 1) {                 // 不是最後一個單字 / not the last word on this line
                        int spaces = baseSpaces + ((k - i) < extra ? 1 : 0); // 前 extra 個 gap +1 / first `extra` gaps get +1
                        line.append(spaces, ' ');    // 一次塞 spaces 個空格 / push that many spaces at once
                    }
                }
            }
            result.push_back(std::move(line));       // move 進結果,省一次複製 / move into result to avoid copy
            i = j;                                   // 下一行從 j 開始 / next line begins at j
        }
        return result;                               // 回傳全部行 / return all lines
    }
};

複雜度 / Complexity

  • Time: O(N · L),其中 N 是單字數,L 是平均單字長度(換句話說:O(總輸出字元數))。每個單字被讀取與寫入的次數都是常數次;strlen 在 C 版每次重算字串長度,但因每字長度有上限 20,仍是線性。Each word is examined a constant number of times to decide its line, then written once into the output. The output's total character count is the dominant cost.
  • Space: O(總輸出字元數) = O(N · maxWidth)。除了輸出本身,沒有額外資料結構。Apart from the output strings, we use only a few integer scalars — no extra data structures.

Pitfalls & Edge Cases

  • 空格分配公式搞反 / Distributing spaces the wrong way. 題目明確說「左邊空隙比右邊多」,所以餘數要發給最左邊的 gap。如果改成發給右邊,會在 Example 2 失敗。Code guard: ((k - i) < extra ? 1 : 0) 用「目前是第幾個 gap」當索引,從 0 開始往右。
  • j - i 容易少算空格 / Forgetting the implicit separator count. 判斷能否再塞一個單字時,需要保留「已有單字之間至少 1 個空格」的位置:條件是 lineLen + words[j].size() + (j - i) <= maxWidth,其中 (j - i) 就是「再加一個單字後所需的分隔空格數」。漏掉就會超過 maxWidth
  • 只有一個單字的行 / Single-word lines. gaps = wordCount - 1 = 0,會發生除以零。所以必須先用 wordCount == 1 走左對齊分支。Always branch on the single-word case before dividing.
  • 最後一行 / Last line. 即使它的單字可以「完全對齊」湊滿,也必須左對齊。判斷方式:選完這行後 j == n。Note that "last line" is determined after grouping, not by some flag set in advance.
  • 行尾補空格別漏 / Don't forget trailing padding. 左對齊情況裡,行尾要補到剛好 maxWidth。C 版用 while (pos < maxWidth),C++ 版用 line.append(maxWidth - line.size(), ' ')
  • C 字串結尾 \0 / Null terminator. C 版分配的是 maxWidth + 1 bytes 並在 line[maxWidth]'\0';漏掉會造成 printf("%s", ...) 越界讀。
  • strlen 反覆呼叫 / Repeated strlen calls. 在 C 版中每次計算字串長度都是 O(L)。本題單字長度上限 20,影響不大;若長度可能很大,應先把每個 strlen 預先存起來。