题目描述

✅ 722. 删除注释

image-20260929000052398

image-20260929000052399

image-20260929000052400

题意分析

输入数组按物理行保存一段源码,需要删除行注释和块注释,并按删除后的行返回其余内容。行注释从 // 开始,持续到当前行末;块注释从 /* 开始,到后面第一个不与开始标记重叠的 */ 结束,可以跨越多行。

块注释内的换行也会被删除,因此它前后的代码可能拼成同一条输出行。删除后真正为空的字符串不输出,但普通空格仍是代码中需要保留的字符,不能自行去除首尾空格。

题目保证块注释最终闭合,输入只包含可打印 ASCII 字符,没有单引号、双引号或会干扰注释规则的其他语法,因此不需要额外解析字符串字面量。

解法:逐字符状态机

核心思路

[!blue]

注释标记是否生效,取决于当前是否已经进入块注释。用 inBlock 记录这个状态,用缓冲区 line 保存当前尚未完成的输出行,从左到右扫描每一行的字符。

处在块注释中时,只寻找结束标记 */:找到就离开块状态并跳过两个字符,否则跳过当前字符。此时遇到 // 不会截断本行,遇到新的 /* 也不会增加嵌套层数,因为它们都只是当前块注释的内容。

处在块注释外时,若遇到 //,当前物理行后面的所有字符都应丢弃,直接结束本行扫描;若遇到 /*,进入块状态并跳过标记;否则把当前字符加入缓冲区。每个标记识别后都前进两格,因此同一个字符不会同时被用作开始和结束标记的一部分。

块状态和输出缓冲区都要保留到下一条物理行。若当前行扫描结束时仍在块内,换行也属于注释,不能输出或清空已经保留的前半段代码;后面闭合块后还要继续把代码接在它后面。只有行末已处在块外时,这条输出行才真正结束,非空就保存并清空缓冲区。

双字符标记只能由同一条物理行中相邻的字符构成,不能拿上一行的最后一字符与下一行的第一字符重新拼成标记。代码每次检查 i + 1 都限定在当前行长度内,正好保留了这个边界。

解题步骤

  1. 创建结果列表、空行缓冲区和 inBlock = false,依次扫描各条物理行。
  2. 若处在块内,识别结束标记后退出块状态,否则继续跳过字符。
  3. 若处在块外,依次处理行注释、块注释开始标记、普通字符;普通字符才写入缓冲区。
  4. 双字符标记处理后移动两格,普通字符或块内无关字符处理后移动一格。
  5. 每条物理行结束时,若已在块外且缓冲区非空,将其转换成独立字符串保存,再清空缓冲区;若仍在块内则保留状态和内容。
  6. 全部输入扫描完后返回结果。

代码实现

class Solution {
    public List<String> removeComments(String[] source) {
        List<String> ans = new ArrayList<>();
        StringBuilder line = new StringBuilder();
        boolean inBlock = false;

        for (String code : source) {
            for (int i = 0; i < code.length(); ) {
                // 块内只识别结束标记,双字符检查限定在当前物理行
                if (inBlock) {
                    if (i + 1 < code.length()
                            && code.charAt(i) == '*'
                            && code.charAt(i + 1) == '/') {
                        inBlock = false;
                        i += 2;
                    } else {
                        i++;
                    }
                } else if (i + 1 < code.length()
                        && code.charAt(i) == '/'
                        && code.charAt(i + 1) == '/') {
                    break;
                } else if (i + 1 < code.length()
                        && code.charAt(i) == '/'
                        && code.charAt(i + 1) == '*') {
                    inBlock = true;
                    i += 2;
                } else {
                    line.append(code.charAt(i++));
                }
            }

            // 离开块注释才完成逻辑行,否则保留缓冲继续跨行拼接
            if (!inBlock && line.length() > 0) {
                ans.add(line.toString());
                line.setLength(0);
            }
        }

        return ans;
    }
}
func removeComments(source []string) []string {
    ans := make([]string, 0)
    line := make([]byte, 0)
    inBlock := false

    for _, code := range source {
        for i := 0; i < len(code); {
            // 块内只识别结束标记,双字符检查限定在当前物理行
            if inBlock {
                if i+1 < len(code) && code[i] == '*' && code[i+1] == '/' {
                    inBlock = false
                    i += 2
                } else {
                    i++
                }
            } else if i+1 < len(code) && code[i] == '/' && code[i+1] == '/' {
                break
            } else if i+1 < len(code) && code[i] == '/' && code[i+1] == '*' {
                inBlock = true
                i += 2
            } else {
                line = append(line, code[i])
                i++
            }
        }

        // 离开块注释才完成逻辑行,否则保留缓冲继续跨行拼接
        if !inBlock && len(line) > 0 {
            ans = append(ans, string(line))
            line = line[:0]
        }
    }
    return ans
}

复杂度分析

  • 时间复杂度:$O(C)$,C 为输入总字符数,每个字符至多扫描一次,保留的字符也只写入缓冲区和结果常数次。
  • 空间复杂度:不计返回结果为 $O(L)$,L 为删除注释后最长逻辑行的长度;跨行块注释可能让它长于单条物理行。计入全部结果为 $O(C)$。

关键点总结

[!green]

  • 先按当前块状态判断规则,不能在注释内部再次启动另一种注释解析。
  • 物理行边界用于识别行注释,输出行边界则取决于块注释是否吞掉了换行。
  • 状态和缓冲区都跨行保存,才能正确拼接跨行块注释前后的代码。
  • 只去掉长度为零的结果,保留没有被注释覆盖的空格。

易错点总结

[!yellow]

  • 每读一条物理行就清空缓冲区,会丢失跨行块注释之前已经保留的代码。
  • 在块内继续识别行注释,会过早停止扫描,可能错过本行的真正结束标记。
  • 在块内遇到开始标记就再次嵌套,改变了本题只以第一个有效结束标记闭合的规则。
  • 识别标记后只移动一格,可能让同一个字符重复参与其他标记。
  • 把相邻物理行的两端拼成一个标记,或对输出做首尾空格清理,都会改动本应保留的内容。

相似题目

题目 难度 关联与区别
补充题 127. 有效代码物理行数统计 中等 原题统计有效代码行时必须保留物理行边界,还要区分字面量内的注释符号;本题实际移除注释并拼接保留内容。
转载与许可
作者
链接 https://hgnulb.github.io/blog/2026/10450688
许可 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议,转载请注明出处!