LeetCode 722. 删除注释
题目描述



题意分析
输入数组按物理行保存一段源码,需要删除行注释和块注释,并按删除后的行返回其余内容。行注释从
//开始,持续到当前行末;块注释从/*开始,到后面第一个不与开始标记重叠的*/结束,可以跨越多行。块注释内的换行也会被删除,因此它前后的代码可能拼成同一条输出行。删除后真正为空的字符串不输出,但普通空格仍是代码中需要保留的字符,不能自行去除首尾空格。
题目保证块注释最终闭合,输入只包含可打印 ASCII 字符,没有单引号、双引号或会干扰注释规则的其他语法,因此不需要额外解析字符串字面量。
解法:逐字符状态机
核心思路
[!blue]
注释标记是否生效,取决于当前是否已经进入块注释。用
inBlock记录这个状态,用缓冲区line保存当前尚未完成的输出行,从左到右扫描每一行的字符。处在块注释中时,只寻找结束标记
*/:找到就离开块状态并跳过两个字符,否则跳过当前字符。此时遇到//不会截断本行,遇到新的/*也不会增加嵌套层数,因为它们都只是当前块注释的内容。处在块注释外时,若遇到
//,当前物理行后面的所有字符都应丢弃,直接结束本行扫描;若遇到/*,进入块状态并跳过标记;否则把当前字符加入缓冲区。每个标记识别后都前进两格,因此同一个字符不会同时被用作开始和结束标记的一部分。块状态和输出缓冲区都要保留到下一条物理行。若当前行扫描结束时仍在块内,换行也属于注释,不能输出或清空已经保留的前半段代码;后面闭合块后还要继续把代码接在它后面。只有行末已处在块外时,这条输出行才真正结束,非空就保存并清空缓冲区。
双字符标记只能由同一条物理行中相邻的字符构成,不能拿上一行的最后一字符与下一行的第一字符重新拼成标记。代码每次检查
i + 1都限定在当前行长度内,正好保留了这个边界。
解题步骤
- 创建结果列表、空行缓冲区和
inBlock = false,依次扫描各条物理行。- 若处在块内,识别结束标记后退出块状态,否则继续跳过字符。
- 若处在块外,依次处理行注释、块注释开始标记、普通字符;普通字符才写入缓冲区。
- 双字符标记处理后移动两格,普通字符或块内无关字符处理后移动一格。
- 每条物理行结束时,若已在块外且缓冲区非空,将其转换成独立字符串保存,再清空缓冲区;若仍在块内则保留状态和内容。
- 全部输入扫描完后返回结果。
代码实现
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. 有效代码物理行数统计 | 中等 | 原题统计有效代码行时必须保留物理行边界,还要区分字面量内的注释符号;本题实际移除注释并拼接保留内容。 |
转载与许可
许可
本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议,转载请注明出处!