1. 问题背景与核心挑战
文本左右对齐(Text Justification)是LeetCode上经典的字符串处理题目(第68题),要求将一组单词按照指定的最大宽度进行排版,生成左右对齐的文本行。这个问题看似简单,实则暗藏多个需要仔细处理的边界条件,是检验开发者字符串操作能力和细节处理能力的绝佳案例。
在实际开发场景中,类似的文本对齐需求广泛存在于文字编辑器、报表生成、打印排版等应用中。例如:
- 生成PDF文档时需要保证段落两端对齐
- 控制台输出表格时要求各列宽度固定
- 网页排版中实现杂志式的多栏布局
这个问题的核心难点在于:
- 单词分配策略:如何确定每行放置哪些单词
- 空格分配算法:如何在单词间合理分配空格以达到指定宽度
- 最后一行处理:与其他行的对齐方式不同
- 性能考量:最优解应达到O(n)时间复杂度
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 解题思路分析与算法设计
2.1 基础思路分解
解决文本对齐问题可以分解为三个主要步骤:
- 单词分组:确定哪些单词可以放在同一行
- 空格计算:计算需要插入的空格总数及其分配方式
- 行构建:根据前两步结果构造对齐后的文本行
对于单词分组,我们需要:
- 维护当前行已选单词的总长度(包括最小必需空格)
- 遍历单词列表,直到添加下一个单词会导致行超长
- 使用贪心算法确保每行尽可能多地放置单词
2.2 关键算法实现
以下是Python实现的算法框架:
python复制def fullJustify(words, maxWidth):
res = []
line = [] # 当前行的单词
curr_length = 0 # 当前行字符数(不含空格)
for word in words:
# 尝试将单词加入当前行
if curr_length + len(word) + len(line) <= maxWidth:
line.append(word)
curr_length += len(word)
else:
# 处理当前行对齐
res.append(process_line(line, curr_length, maxWidth, False))
# 开始新行
line = [word]
curr_length = len(word)
# 处理最后一行
res.append(process_line(line, curr_length, maxWidth, True))
return res
2.3 空格分配策略
空格分配是本题最复杂的部分,需要考虑以下情况:
-
常规行处理:
- 总空格数 = maxWidth - 当前行所有单词总长度
- 空格间隙数 = 单词数 - 1
- 每个间隙至少分配的空格数 = 总空格数 // 空格间隙数
- 剩余空格数 = 总空格数 % 空格间隙数
- 前N个间隙需要多分配1个空格
-
单单词行处理:
- 单词左对齐,后面补全空格
-
最后一行处理:
- 单词间单空格分隔
- 行末补全空格
处理行的函数实现:
python复制def process_line(line, curr_length, maxWidth, is_last):
if len(line) == 1 or is_last:
# 单单词行或最后一行:左对齐
res = ' '.join(line)
return res.ljust(maxWidth)
total_spaces = maxWidth - curr_length
gaps = len(line) - 1
space_per_gap = total_spaces // gaps
extra_spaces = total_spaces % gaps
res = []
for i in range(len(line)-1):
res.append(line[i])
res.append(' ' * (space_per_gap + (1 if i < extra_spaces else 0)))
res.append(line[-1])
return ''.join(res)
3. 复杂度分析与优化
3.1 时间复杂度
该算法的时间复杂度为O(n),其中n是单词总数。这是因为:
- 每个单词只被处理一次(主循环)
- 每行的处理时间是O(m),m是该行单词数,所有行的m总和为n
- 字符串拼接操作的总时间也与n成正比
3.2 空间复杂度
空间复杂度也是O(n),主要用于:
- 存储结果列表
- 临时存储当前行的单词
3.3 性能优化技巧
在实际编码面试中,可以应用以下优化技巧:
- 预计算单词长度,避免重复计算
- 使用字符串生成器(如Python中的列表)代替直接字符串拼接
- 提前处理边界条件(如空输入、单个单词等)
4. 边界条件与测试用例
4.1 必须考虑的边界情况
- 单个单词的行
- 刚好填满整行的单词组合
- 包含长单词(长度等于maxWidth)
- 最后一行处理
- 输入为空列表
- maxWidth为0(虽然题目通常保证>0)
4.2 典型测试用例
python复制test_cases = [
(["This", "is", "an", "example", "of", "text", "justification."], 16,
[
"This is an",
"example of text",
"justification. "
]),
(["What","must","be","acknowledgment","shall","be"], 16,
[
"What must be",
"acknowledgment ",
"shall be "
]),
(["Science","is","what","we","understand","well","enough","to","explain",
"to","a","computer.","Art","is","everything","else","we","do"], 20,
[
"Science is what we",
"understand well",
"enough to explain to",
"a computer. Art is",
"everything else we",
"do "
]),
(["Listen","to","many,","speak","to","a","few."], 6,
[
"Listen",
"to ",
"many, ",
"speak ",
"to a",
"few. "
])
]
5. 常见错误与调试技巧
5.1 新手常犯错误
-
空格计算错误:
- 忘记单词间至少需要1个空格
- 错误计算剩余空格的分配
-
最后一行处理不当:
- 与其他行使用相同的对齐方式
- 忘记在行末补足空格
-
索引越界:
- 处理单单词行时访问line[1]
- 计算空格间隙时未考虑单词数为1的情况
5.2 调试建议
-
打印中间变量:
- 每行的单词列表
- 计算出的空格分配方案
- 构建中的字符串长度
-
使用可视化调试:
- 用'·'代替空格便于观察
- 检查每行实际长度是否等于maxWidth
-
单元测试:
- 先测试简单情况(如单单词)
- 再测试常规情况
- 最后测试复杂边界情况
6. 扩展思考与实际应用
6.1 算法变种
-
两端对齐与居中对齐:
- 修改空格分配策略即可实现不同对齐方式
-
多语言支持:
- 处理全角字符(如中文)需要考虑字符宽度
-
单词换行策略:
- 支持连字符分割长单词
- 考虑音节边界进行优雅换行
6.2 实际工程应用
-
文本编辑器实现:
- 需要实时计算换行位置
- 考虑性能优化(增量计算)
-
报表生成:
- 表格列的对齐
- 多语言文本处理
-
响应式网页设计:
- 根据视口宽度动态调整文本布局
- 结合CSS的text-justify属性
7. 不同语言实现对比
7.1 Python实现特点
优势:
- 字符串操作简便
- 列表切片和join方法高效
- 内置ljust/rjust方法
注意点:
- 字符串不可变,频繁拼接性能差
- 使用列表作为缓冲区更高效
7.2 Java实现要点
java复制public List<String> fullJustify(String[] words, int maxWidth) {
List<String> res = new ArrayList<>();
int index = 0;
while (index < words.length) {
int count = words[index].length();
int last = index + 1;
while (last < words.length) {
if (words[last].length() + count + 1 > maxWidth) break;
count += words[last].length() + 1;
last++;
}
StringBuilder builder = new StringBuilder();
int diff = last - index - 1;
// 处理最后一行或单单词行
if (last == words.length || diff == 0) {
for (int i = index; i < last; i++) {
builder.append(words[i]).append(" ");
}
builder.deleteCharAt(builder.length() - 1);
while (builder.length() < maxWidth) {
builder.append(" ");
}
} else {
// 常规行处理
int spaces = (maxWidth - count) / diff;
int r = (maxWidth - count) % diff;
for (int i = index; i < last; i++) {
builder.append(words[i]);
if (i < last - 1) {
for (int j = 0; j <= (spaces + ((i - index) < r ? 1 : 0)); j++) {
builder.append(" ");
}
}
}
}
res.add(builder.toString());
index = last;
}
return res;
}
7.3 C++实现注意事项
- 注意字符串拼接性能
- 使用ostringstream处理复杂字符串构建
- 提前预留字符串空间避免多次分配
8. 面试技巧与评分标准
8.1 面试官考察重点
-
问题分解能力:
- 能否将大问题拆解为单词分组、空格计算等子问题
-
边界条件处理:
- 是否考虑最后一行、单单词行等特殊情况
-
代码整洁度:
- 变量命名合理
- 函数拆分适当
- 注释清晰
-
沟通表达:
- 能否清晰解释算法思路
- 能否合理回应面试官提问
8.2 评分标准参考
| 评分维度 | 优秀标准 | 合格标准 | 不合格表现 |
|---|---|---|---|
| 正确性 | 处理所有边界条件 | 主要功能正确 | 基本逻辑错误 |
| 复杂度 | O(n)时间/空间 | 次优解但正确 | 高复杂度解法 |
| 代码质量 | 模块化、可读性好 | 基本可读 | 混乱难懂 |
| 沟通表达 | 思路清晰流畅 | 能解释主要思路 | 表达不清 |
8.3 面试应答策略
-
先确认问题:
- 询问maxWidth是否总是大于单词长度
- 确认最后一行是否总是左对齐
-
从简单案例入手:
- 先处理常规情况
- 再逐步添加边界处理
-
主动解释思考过程:
- "这里我需要考虑..."
- "这个边界条件需要特殊处理..."
-
写完代码后:
- 用测试用例逐步验证
- 解释可能的优化方向
