题目简介
LeetCode 68「Text Justification」要求我们实现一个简单的排版器:给定单词数组 words 和行宽 maxWidth,将单词按行排版,使每一行刚好有 maxWidth 个字符,并且做到左右两端对齐(两端对齐)。 leetcode
题目中给出了明确的规则: leetcode
- 采用贪心策略:每一行尽可能多塞单词。
- 每一行长度必须恰好为
maxWidth,不足的部分用空格' '填充。 - 一行中单词之间的多余空格要尽量平均分配,如果无法整除,左边的间隔要比右边多一个空格。
- 最后一行必须左对齐:单词之间只有一个空格,多余空格全部补在行末。
- 如果某一行只有一个单词,该行也视作左对齐,多余空格补在右侧。
本题的难点不在算法,而在于各种边界情况的实现细节。
思路概览:从左到右的贪心切行
题目描述里就明确提示了使用贪心(greedy): leetcode
You should pack your words in a greedy approach; that is, pack as many words as you can in each line.
整体思路可以概括为两层:
- 外层:按行切分单词
- 从左到右扫
words,对当前行找到能容纳的最大区间[start, end]。 - 判断当前行是不是最后一行(
end == wordsSize - 1)。
- 从左到右扫
- 内层:根据当前行的类型生成一行字符串
- 普通中间行:两端对齐,需要平均分配空格。
- 最后一行:左对齐,单词之间一个空格,右边补空格。
- 只有一个单词的行:左对齐,右边补空格。
下面分别展开。
如何用贪心划分每一行?
设当前行从 start 开始,我们用 end 向右扩展,尝试加入更多的单词。
sum_words_width:记录当前行中单词长度之和。end - start:当前行单词间的间隔数(至少需要这么多个空格)。
当我们尝试把 words[end] 放入当前行时,如果:
sum_words_width + \\text{len(words\[end\])} + (end - start) \> maxWidth
说明再加这个单词就超过行宽了,此时应该停止,并回退这个单词。 leetcode
对应的 C 代码片段如下:
c
start = i;
end = i;
sum_words_width = 0;
while (end < wordsSize) {
sum_words_width += strlen(words[end]);
// sum_words_width + (end - start) 是单词长度 + 至少一个空格的长度
if (sum_words_width + end - start > maxWidth) {
sum_words_width -= strlen(words[end]); // 回退
break;
}
end++;
}
end--; // 最后一次多加了一个
然后我们判断当前行是否为最后一行:
c
bool isLastLine = (end == wordsSize - 1);
行内空格如何计算?
对于每一行 [start, end],先统计:
num_words = end - start + 1:当前行单词个数。sum_words_width:单词总长度(上一步已算出)。
接下来分三种情况:
情况 1:最后一行 或 只有一个单词
这两种都按照「左对齐」处理:
- 单词之间用一个空格分隔。
- 剩余的空格全部补在行尾,使总长度为
maxWidth。 leetcode
伪代码:
text
line = words[start]
for k = start+1 .. end:
line += " " + words[k]
在行尾补空格,直到长度 == maxWidth
情况 2:中间行(多于一个单词,两端对齐)
此时我们要让单词两端对齐,并且尽可能平均地分配空格:
- 间隔数:
slotCount = num_words - 1。 - 总空格数:
spaceWidth = maxWidth - sum_words_width。 - 每个间隔至少的空格:
avgSpace = spaceWidth / slotCount。 - 多出来的空格:
extra = spaceWidth % slotCount,分配给最左边的extra个间隔,每个多 1 个空格。 leetcode
填充顺序是:
- 从
start到end,依次写入单词。 - 每写完一个单词(除了最后一个),在其后添加:
avgSpace个空格;- 如果当前间隔序号
< extra,再多加 1 个空格。
用伪代码表示:
text
for i = start .. end:
写入 words[i]
if i < end:
写入 avgSpace 个空格
if (i - start) < extra:
再写 1 个空格
一种实现小技巧:先把整行填满空格,再写单词
在 C 语言实现中,一个常见技巧是:
- 对结果数组
result[line],先把这一行全部初始化为' '。 - 然后用一个指针
pos从左向右写入单词。 - 需要「加空格」的地方,只移动
pos(因为这些位置本来就是空格),而不必显式写' '。
这样可以简化很多边界处理。
例如下面这段代码,给最后一行或只有一个单词的行填充内容:
c
if (num_words == 1 || isLastLine) {
for (i = start; i <= end; i++) {
memcpy(pos, words[i], strlen(words[i]));
pos += strlen(words[i]);
pos += 1; // 跳过一个空格位置,不用写,因为初始化时就是 ' '
}
}
中间行的实现类似,只不过「跳过空格位置」的步长变成了 space_per_slot 和 space_per_slot + 1:
c
slot = num_words - 1;
space_per_slot = (maxWidth - sum_words_width) / slot;
extra_spaces = (maxWidth - sum_words_width) % slot;
for (i = start; i <= end; i++) {
memcpy(pos, words[i], strlen(words[i]));
pos += strlen(words[i]);
pos += space_per_slot; // 平均空格
if ((i - start) < extra_spaces) {
pos += 1; // 左侧多的空格
}
}
由于整行一开始就已经被填充为空格,这里的 pos += ... 等价于「跳过这些空格」,逻辑上就是我们「写了这么多空格」。
完整 C 代码示例
下面是一个基于上述思路的完整 C 实现,使用了预填空格 + 贪心切分 + 行内空格平均分配的方式:
c
#include <stdlib.h>
#include <string.h>
#include <stdbool.h>
void fillResult(char* result_line, char** words,
int start, int end, int maxWidth, int sum_words_width,
bool isLastLine) {
int i;
int num_words = end - start + 1;
char *pos = result_line;
if (num_words == 1 || isLastLine) {
// 左对齐:单词之间一个空格,剩余空格在尾部(由于整行已填空格,这里只需写单词并跳一个位置)
for (i = start; i <= end; i++) {
int len = (int)strlen(words[i]);
memcpy(pos, words[i], len);
pos += len;
if (pos - result_line < maxWidth) {
pos += 1; // 跳过一个空格位置
}
}
} else {
// 中间行:两端对齐,平均分配空格,左边多右边少
int slot = num_words - 1;
int spaceWidth = maxWidth - sum_words_width;
int space_per_slot = spaceWidth / slot;
int extra_spaces = spaceWidth % slot;
for (i = start; i <= end; i++) {
int len = (int)strlen(words[i]);
memcpy(pos, words[i], len);
pos += len;
if (i < end) {
int spaces = space_per_slot;
if ((i - start) < extra_spaces) {
spaces += 1;
}
pos += spaces; // 跳过这么多空格
}
}
}
}
/**
* Note: The returned array must be malloced, assume caller calls free().
*/
char** fullJustify(char** words, int wordsSize, int maxWidth, int* returnSize) {
if (words == NULL || wordsSize <= 0 || maxWidth <= 0) {
*returnSize = 0;
return NULL;
}
// 最多不会超过 wordsSize 行,先分配足够的空间
char** result = (char**)malloc(wordsSize * sizeof(char*));
if (!result) {
*returnSize = 0;
return NULL;
}
// 每一行预填空格并加 '\0'
for (int i = 0; i < wordsSize; i++) {
result[i] = (char*)malloc((maxWidth + 1) * sizeof(char));
for (int j = 0; j < maxWidth; j++) {
result[i][j] = ' ';
}
result[i][maxWidth] = '\0';
}
int line_num = 0;
for (int i = 0; i < wordsSize; i++) {
int start = i;
int end = i;
int sum_words_width = 0;
// 贪心地找到当前行可以容纳的 [start, end]
while (end < wordsSize) {
int len = (int)strlen(words[end]);
sum_words_width += len;
if (sum_words_width + (end - start) > maxWidth) {
sum_words_width -= len;
break;
}
end++;
}
end--; // 回退到最后一个合法单词
bool isLastLine = (end == wordsSize - 1);
fillResult(result[line_num], words, start, end, maxWidth, sum_words_width, isLastLine);
line_num++;
i = end;
}
*returnSize = line_num;
return result;
}
这份代码的核心要点是:
- 按行贪心切分单词 ,保证每行尽量多放单词且不超过
maxWidth。 - 根据行类型(中间行 / 最后一行 / 单词数为 1)决定对齐方式。
- 中间行将总空格数平均分到各个间隔,多出的空格分配给左边的若干间隔。
- 通过「整行预填空格 + 移动指针」简化了显式写空格的逻辑。
小结
这道题本质上是一个「实现题 + 贪心」,难点不在算法,而在于:
- 切行时长度如何计算精确(单词长度 + 间隔数量)。
- 区分三种行类型:
- 最后一行。
- 只有一个单词的行。
- 中间普通行。
- 中间行如何正确且优雅地平均分配空格,并做到「左多右少」。
一旦理解了上述逻辑,代码实现就只是细致的字符串拼接工作,非常适合作为 C 语言 / 字符串操作的练手题。