文本左右对齐如何模拟?(LeetCode 68)
简化版
文本左右对齐按行贪心装单词:每行尽量放入更多单词,保证单词长度加最少间隔不超过 maxWidth。非最后一行要把空格尽量均匀分到单词间,左边间隔比右边多拿余数;最后一行左对齐,单词间一个空格,末尾补空格。重点是分清单词总长度、间隔数量、空格总数。
详细版
List<String> fullJustify(String[] words, int maxWidth) {
List<String> ans = new ArrayList<>();
int i = 0;
while (i < words.length) {
int j = i, len = 0;
while (j < words.length && len + words[j].length() + (j - i) <= maxWidth) {
len += words[j].length();
j++;
}
int gaps = j - i - 1;
StringBuilder line = new StringBuilder();
if (j == words.length || gaps == 0) {
for (int k = i; k < j; k++) {
if (k > i) line.append(' ');
line.append(words[k]);
}
while (line.length() < maxWidth) line.append(' ');
} else {
int spaces = maxWidth - len;
int each = spaces / gaps, extra = spaces % gaps;
for (int k = i; k < j; k++) {
line.append(words[k]);
if (k < j - 1) {
int cnt = each + (k - i < extra ? 1 : 0);
line.append(" ".repeat(cnt));
}
}
}
ans.add(line.toString());
i = j;
}
return ans;
}
完整版教学
一、题目分成“分行”和“排空格”两步
文本对齐看起来细节多,但可以拆成两层:先决定一行放哪些单词,再决定这些单词之间放多少空格。分行规则是贪心:从当前单词开始,尽量往本行塞更多单词,只要放得下就继续。
例如 maxWidth=16,单词 ["This","is","an","example"],第一行可以放 This is an,因为单词长度 4+2+2=8,最少两个间隔,共 10,不超过 16;再放 example 就超过了。
二、分行条件为什么要加最少间隔
如果一行放从 i 到 j 的单词,单词总长度是 len,最少需要 j-i 个空格把它们分开。因此尝试加入 words[j] 时,条件是:
当前单词总长 len + 新单词长度 + 已有间隔数量 (j-i) <= maxWidth
这里 (j-i) 表示加入新单词后,本行单词数会变成 j-i+1,间隔数是 j-i。这个细节经常导致 off-by-one 错误。
记忆钩子:分行时先按“每个间隔至少 1 个空格”估算,排版时再把剩余空格补厚。
三、非最后一行如何均匀分配空格
非最后一行需要左右对齐:行长度必须等于 maxWidth,单词之间空格尽量均匀,不能放到行尾。设单词总长为 len,间隔数为 gaps:
spaces = maxWidth - len
each = spaces / gaps
extra = spaces % gaps
前 extra 个间隔多放 1 个空格,因为题目要求左侧空格不少于右侧。例子:["This","is","an"],maxWidth=16,单词总长 8,间隔 2,总空格 8,所以每个间隔 4 个,结果 "This is an"。
四、最后一行和单词行特殊处理
最后一行要求左对齐:单词之间只放一个空格,剩余空格全部补到行尾。只有一个单词的非最后行也类似,因为没有间隔可以均匀分配,只能把空格补到末尾。
最后一行: "justification. "
单词行 : "Longword "
这两个分支要提前处理,否则 gaps=0 时会出现除以 0。
五、完整例子手推
words=["This","is","an","example","of","text","justification."],maxWidth=16:
第 1 行: This,is,an -> len=8,gaps=2,spaces=8 -> 4/4
"This is an"
第 2 行: example,of,text -> len=13,gaps=2,spaces=3 -> 2/1
"example of text"
第 3 行: justification. -> 最后一行左对齐
"justification. "
每行长度都必须恰好为 16,这是最重要的自检标准。
六、实现方式对比
| 子问题 | 推荐做法 | 易错点 |
|---|---|---|
| 分行 | 贪心尽量放更多单词 | 忘记最少间隔 |
| 非最后行排空格 | each + extra 分配 | 余数要给左边 |
| 最后一行 | 单空格连接,末尾补齐 | 错按两端对齐处理 |
| 单词行 | 末尾补齐 | 除以 0 |
把这四类情况拆开写,代码会比强行合并更可靠。
七、复杂度和字符串构造细节
设所有单词总字符数为 T,输出行数为 L。分行阶段每个单词只会被放入某一行一次,排版阶段每个单词也只会被追加一次;补空格的总数量等于所有行宽之和减去单词字符数。因此总体时间可以看成 O(T + L*maxWidth),如果按输出规模计量,就是线性的。
实现时要避免在循环里反复使用不可变字符串拼接。Java 中 line += word 会不断创建新字符串,可能把单行构造拖成平方级;使用 StringBuilder 逐段 append 更稳定。空格可以用循环追加,也可以在 Java 11 以后使用 " ".repeat(cnt),但要确保 cnt 不为负。
一行输出 = word1 + spaces1 + word2 + spaces2 + ... + wordK
所有 spaces 的总数 = maxWidth - 单词总长度
非最后行:spaces 分布在 gaps 个间隔里
最后一行:单词间固定 1 个空格,剩余全部在行尾
这段构造逻辑还有一个好处:你可以在每一行生成后断言 line.length() == maxWidth。面试时如果能主动说“我会用长度断言检查每行”,通常能说明你对这道模拟题的边界有控制感。
八、常见误区与追问
- 误区:分行时只看单词长度和。 单词之间至少要有空格,必须把最少间隔算进去。
- 误区:余数空格放到右边。 题目要求左边间隔获得更多空格。
- 误区:最后一行也两端对齐。 最后一行必须左对齐。
- 追问:单词长度等于 maxWidth 怎么办? 该行只能放这个单词,直接加入并无需额外补空格。
- 追问:为什么用贪心分行? 题目要求每行尽可能多放单词,当前行放满不会影响后续行合法性。
- 追问:如何验证输出? 每行长度必须等于
maxWidth,非最后行没有尾部额外空格,最后一行左对齐。
九、加强记忆
文本左右对齐分两步:先贪心分行,再排空格。非最后行把 maxWidth - 单词总长 均分到间隔,余数给左边;最后一行和单词行左对齐,末尾补空格。核心变量是 len、gaps、spaces、each、extra,写清它们就不会乱。