LeetCode 68. Text Justification 题解:贪心与实现细节

题目简介

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.

整体思路可以概括为两层:

  1. 外层:按行切分单词
    • 从左到右扫 words,对当前行找到能容纳的最大区间 [start, end]
    • 判断当前行是不是最后一行(end == wordsSize - 1)。
  2. 内层:根据当前行的类型生成一行字符串
    • 普通中间行:两端对齐,需要平均分配空格。
    • 最后一行:左对齐,单词之间一个空格,右边补空格。
    • 只有一个单词的行:左对齐,右边补空格。

下面分别展开。


如何用贪心划分每一行?

设当前行从 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:最后一行 或 只有一个单词

这两种都按照「左对齐」处理:

  • 单词之间用一个空格分隔。
  • 剩余的空格全部补在行尾,使总长度为 maxWidthleetcode

伪代码:

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

填充顺序是:

  • startend,依次写入单词。
  • 每写完一个单词(除了最后一个),在其后添加:
    • 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_slotspace_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 语言 / 字符串操作的练手题。

相关推荐
WL_Aurora4 小时前
【每日一题】前缀和
python·算法
汉克老师4 小时前
GESP2025年3月认证C++五级( 第二部分判断题(1-10))
c++·算法·分治算法·线性筛法·gesp5级·gesp五级
洛水水4 小时前
【力扣100题】17.K 个一组翻转链表
算法·leetcode·链表
洛水水5 小时前
【力扣100题】16.两两交换链表中的节点
算法·leetcode·链表
wuweijianlove5 小时前
算法教学中的抽象建模与动态可视化设计的技术7
算法
2zcode5 小时前
基于改进YOLO11算法的芯片微缺陷检测系统(UI界面+数据集+分析界面+处置建议+训练代码)
算法·芯片缺陷
leoufung5 小时前
LeetCode 30:Substring with Concatenation of All Words 题解(含 C 语言 uthash 实现)
c语言·leetcode·c#
王老师青少年编程5 小时前
csp信奥赛C++高频考点专项训练之贪心算法 --【哈夫曼贪心】:荷马史诗
c++·算法·贪心·csp·信奥赛·哈夫曼贪心·荷马史诗
样例过了就是过了5 小时前
LeetCode热题100 最小路径和
c++·算法·leetcode·动态规划