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 & modulo:
a / 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 pointers:
char**代表「指向多個字串的指標」,每個元素自己是一條已配置的char*。An array where each entry points to a separately allocated C-string. std::string::append(n, ch):把字元ch重複n次接到字串尾巴。Appendsncopies ofch— 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 個放 '