题目描述

✅ 68. 文本左右对齐

image-20260928222323294

image-20260928222323295

image-20260928222323296

题意分析

保持单词顺序,每行放入尽可能多的单词,再补空格使长度恰好为 maxWidth。普通行要左右对齐,词间空格尽量均匀,多出的空格优先放在左侧间隔;最后一行以及只有一个单词的行左对齐,剩余空格补在末尾。

解法:逐行贪心分组并分配空格

核心思路

[!blue]

先确定本行包含哪些单词,再决定空格放在哪里。left 是本行起点,idx 是尚未加入的第一个单词,wordsLen 只统计已选单词的字符数。若加入 words[idx],本行会有 idx-left+1 个单词,至少需要 idx-left 个间隔空格,因此加入条件为 wordsLen + words[idx].length() + idx-left <= maxWidth。

一旦下一个单词放不下,继续加入只会使长度更大,当前的 [left, idx) 就是按顺序能放入的最多单词。题目保证每个单词都不超过行宽,所以每轮至少放入一个单词,idx 一定前进。

普通行有 gaps = idx-left-1 个间隔,要分配的空格总数为 spaces = maxWidth-wordsLen。令 base = spaces/gaps、extra = spaces%gaps,每个间隔先分 base 个,前 extra 个间隔再多分一个。这样恰好用完全部空格,任意两个间隔最多相差一个,也满足左侧优先。分组时已预留最少间隔,因此 base 至少为 1。

最后一行要求词间只放一个空格;只有一个单词时没有可均分的间隔。这两种情况统一用单空格连接单词,再把剩余空格补到行尾。

解题步骤

  1. 从 idx 指向的单词开始,累加单词长度;把必要的最少间隔也算入,直到下一个单词放不下。
  2. 记录本行区间 [left, idx)、单词总长度 wordsLen,判断它是否为最后一行。
  3. 若是最后一行或只有一个单词,使用单空格连接,再在末尾补空格。
  4. 否则计算 spaces、gaps、平均空格数 base 和余数 extra。
  5. 从左到右拼接:前 extra 个间隔放 base + 1 个空格,其余放 base 个。
  6. 重复处理下一行,直到所有单词用完。

代码实现

class Solution {
    public List<String> fullJustify(String[] words, int maxWidth) {
        List<String> ans = new ArrayList<>();
        int idx = 0;

        while (idx < words.length) {
            int left = idx;
            // 加入新词后,最少间隔数等于它与本行起点的下标差。
            int wordsLen = 0;

            while (idx < words.length && wordsLen + words[idx].length() + idx - left <= maxWidth) {
                wordsLen += words[idx].length();
                idx++;
            }

            ans.add(buildLine(words, left, idx, wordsLen, maxWidth, idx == words.length));
        }

        return ans;
    }

    private String buildLine(
            String[] words, int left, int right, int wordsLen, int maxWidth, boolean lastLine) {
        StringBuilder line = new StringBuilder(maxWidth);
        int gaps = right - left - 1;

        // 最后一行和单词行左对齐,避免零间隔时做除法。
        if (lastLine || gaps == 0) {
            for (int i = left; i < right; i++) {
                if (i > left) {
                    line.append(' ');
                }

                line.append(words[i]);
            }

            appendSpaces(line, maxWidth - line.length());

            return line.toString();
        }

        int spaces = maxWidth - wordsLen;
        int base = spaces / gaps;
        // 余数个靠左间隔各多分一个空格。
        int extra = spaces % gaps;

        for (int i = left; i < right; i++) {
            line.append(words[i]);

            if (i < right - 1) {
                appendSpaces(line, base + (i - left < extra ? 1 : 0));
            }
        }

        return line.toString();
    }

    private void appendSpaces(StringBuilder line, int count) {
        while (count-- > 0) {
            line.append(' ');
        }
    }
}
func fullJustify(words []string, maxWidth int) []string {
    ans := make([]string, 0)
    idx := 0

    for idx < len(words) {
        left := idx
        wordsLen := 0
        // 加入新词后,最少间隔数等于它与本行起点的下标差。
        for idx < len(words) &&
            wordsLen+len(words[idx])+idx-left <= maxWidth {
            wordsLen += len(words[idx])
            idx++
        }
        ans = append(ans, buildJustifiedLine(
            words, left, idx, wordsLen, maxWidth, idx == len(words),
        ))
    }
    return ans
}

func buildJustifiedLine(words []string, left, right, wordsLen, maxWidth int, lastLine bool) string {
    line := make([]byte, 0, maxWidth)
    gaps := right - left - 1

    // 最后一行和单词行左对齐,避免零间隔时做除法。
    if lastLine || gaps == 0 {
        for i := left; i < right; i++ {
            if i > left {
                line = append(line, ' ')
            }
            line = append(line, words[i]...)
        }
        return string(appendSpaces(line, maxWidth-len(line)))
    }

    spaces := maxWidth - wordsLen
    // 先均分全部空格,余数个靠左间隔各多分一个。
    base, extra := spaces/gaps, spaces%gaps
    for i := left; i < right; i++ {
        line = append(line, words[i]...)
        if i < right-1 {
            count := base
            if i-left < extra {
                count++
            }
            line = appendSpaces(line, count)
        }
    }
    return string(line)
}

func appendSpaces(line []byte, count int) []byte {
    for ; count > 0; count-- {
        line = append(line, ' ')
    }
    return line
}

复杂度分析

设所有输入单词的字符总数为 C,最终产生 R 行。

  • 时间复杂度:$O(C + R \times \text{maxWidth})$。每个单词只参与一次分组,构造答案时每个输出字符只写一次。
  • 空间复杂度:$O(\text{maxWidth})$,不计返回结果。构造单行时的缓冲区最多保存 maxWidth 个字符。

关键点总结

[!green]

  • 分组条件必须同时计入单词字符和「每两个单词至少一个空格」。
  • 普通行均分的是全部剩余空格;除不尽时,额外空格从左往右分配。
  • 最后一行和只有一个单词的行统一按左对齐处理,避免间隔数为 0 时除零。
  • [left, right) 使用左闭右开区间,单词数是 right - left,间隔数少一个。

易错点总结

[!yellow]

  • wordsLen 不包含空格;选词时加入的是最少间隔,排版时分配的是 maxWidth-wordsLen 个全部空格,不能再额外补一轮分隔空格。
  • 普通行的分母是间隔数 wordCount-1;只有一个单词时必须先走左对齐分支,不能除以 0。
  • 判断最后一行要看选词后的 idx 是否等于单词总数,即使它还能均分空格,也必须单空格连接、行尾补齐。

相似题目

题目 难度 关联与区别
1592. 重新排列单词间的空格 简单 同样按单词间隙重新分配空格,原题处理整段文本,本题还需按宽度贪心分行并区分末行。
转载与许可
作者
链接 https://hgnulb.github.io/blog/2026/37423583
许可 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议,转载请注明出处!