LeetCode 722. 删除注释
题目描述
题意分析
输入是一份按行切开的源码,要求删掉其中的注释并返回剩余内容;删空的行不出现在结果里,未删空的行按原顺序输出。
注释有两种。行注释以两个连续斜杠开头,作用范围到本行末尾;块注释以斜杠加星号开头,到之后最近一次出现的星号加斜杠为止,这段范围可以横跨若干行。题目明确说明块注释不会嵌套,也没有字符串字面量、转义字符这类会干扰判断的语法,这大大简化了识别逻辑。
输入规模很小(行数与每行长度都在百量级),说明本题考的不是效率而是分类讨论的完备性:真正的难点在于「当前读到的两个字符算不算注释标记」要看上下文,而上下文可能来自上一行。
由此浮现出题目最容易被忽视的一点:一段跨行的块注释被删除后,它前面的残留代码和它后面的残留代码属于同一条输出行。例如上一行剩下
a、下一行剩下d,输出应当是一条ad,而不是两条。边界情形包括:整行都被注释掉、块注释在同一行内开始并结束、块注释结束后同一行还有有效代码、块注释内部出现两个斜杠或再次出现斜杠星号(都只是普通字符)、以及一行的最后一个字符是斜杠而下一行以星号开头(跨行不构成标记,因为标记必须是同一行内相邻的两个字符)。
解法:逐字符状态机
核心思路
注释标记是否生效取决于当前是否位于块注释中,因此只需维护一个布尔状态
inBlock。在块注释外,//丢弃本行剩余内容,/*进入块注释;在块注释内,只有*/能退出,其余字符全部忽略。逐字符扫描时必须优先识别由两个字符组成的标记,命中后一次前进两格;否则把普通字符写入当前输出缓冲区。
缓冲区不能在每个物理行末都清空。若块注释跨行,注释前后的代码应拼成同一输出行,例如
a/*...和...*/b最终得到ab。因此只有在行末处于块注释外时,才输出并清空非空缓冲区。状态机覆盖了所有合法输入:每个字符要么作为普通字符保留,要么属于行注释被整段跳过,要么在块注释中被忽略;三种情况互斥且没有遗漏。
解题步骤
- 初始化
inBlock = false,并准备跨行复用的缓冲区line。- 逐行、逐字符扫描;判断双字符标记前先确认
i + 1未越界。- 位于块注释内:遇到
*/就退出状态并前进两格,否则忽略当前字符。- 位于块注释外:遇到
//就结束本行;遇到/*就进入块注释;否则保留当前字符。- 扫描完一行后,若不在块注释中且缓冲区非空,就加入答案并清空。
对
a/*x、y*/b//z:第一行保留a后进入块注释,缓冲区不输出;第二行遇到*/后恢复普通状态,追加b,再遇到//停止,最终输出ab。
代码实现
import java.util.*;
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)$。
关键点总结
- 这是一台只有“块注释外、块注释内”两个状态的确定状态机。
- 块注释状态必须跨物理行保留,输出缓冲区也要随跨行注释继续累积。
- 标记是双字符 token,命中后前进两格;普通字符才前进一格并写入。
- 题目排除了字符串字面量和嵌套块注释,不需要实现完整编译器词法分析。
易错点总结
- 每行都重置缓冲区,会把跨行块注释前后的代码错误拆成两行。
- 在块注释内部仍识别
//或新的/*,会错误改变状态;此时只关心*/。- 发现
//后只跳过两个字符而非结束本行,会把注释正文保留下来。- 访问
code[i + 1]前不检查边界,行末单独一个斜杠或星号会越界。- 注释删除后得到空字符串时仍加入答案,不符合题目“空行不输出”的要求。
- 把相邻两行末尾和开头的字符拼成注释标记是错误的;标记只能在同一物理行内连续出现。
相似题目
| 题目 | 难度 | 考察点 |
|---|---|---|
| 65. 有效数字 | 困难 | 同为逐字符状态机,但状态多达七八个且只需判定合法性,不产出新文本 |
| 71. 简化路径 | 中等 | 记号切分后要用栈处理回退语义,重点在 .. 的抵消而非上下文状态 |
| 394. 字符串解码 | 中等 | 语法允许嵌套,必须用栈保存外层上下文,单个布尔状态不够用 |
| 726. 原子的数量 | 困难 | 词法扫描之外还要做括号嵌套下的计数乘法与结果排序 |
| 1106. 解析布尔表达式 | 困难 | 记号识别只是前置步骤,核心是递归求值嵌套的逻辑运算 |
| 151. 反转字符串中的单词 | 中等 | 同样是单趟扫描重建字符串,但切分依据只是空白而无跨行状态 |