目录

题目描述

722. 删除注释

题意分析

输入是一份按行切开的源码,要求删掉其中的注释并返回剩余内容;删空的行不出现在结果里,未删空的行按原顺序输出。

注释有两种。行注释以两个连续斜杠开头,作用范围到本行末尾;块注释以斜杠加星号开头,到之后最近一次出现的星号加斜杠为止,这段范围可以横跨若干行。题目明确说明块注释不会嵌套,也没有字符串字面量、转义字符这类会干扰判断的语法,这大大简化了识别逻辑。

输入规模很小(行数与每行长度都在百量级),说明本题考的不是效率而是分类讨论的完备性:真正的难点在于「当前读到的两个字符算不算注释标记」要看上下文,而上下文可能来自上一行。

由此浮现出题目最容易被忽视的一点:一段跨行的块注释被删除后,它前面的残留代码和它后面的残留代码属于同一条输出行。例如上一行剩下 a、下一行剩下 d,输出应当是一条 ad,而不是两条。

边界情形包括:整行都被注释掉、块注释在同一行内开始并结束、块注释结束后同一行还有有效代码、块注释内部出现两个斜杠或再次出现斜杠星号(都只是普通字符)、以及一行的最后一个字符是斜杠而下一行以星号开头(跨行不构成标记,因为标记必须是同一行内相邻的两个字符)。

解法:逐字符状态机

核心思路

注释标记是否生效取决于当前是否位于块注释中,因此只需维护一个布尔状态 inBlock。在块注释外,// 丢弃本行剩余内容,/* 进入块注释;在块注释内,只有 */ 能退出,其余字符全部忽略。

逐字符扫描时必须优先识别由两个字符组成的标记,命中后一次前进两格;否则把普通字符写入当前输出缓冲区。

缓冲区不能在每个物理行末都清空。若块注释跨行,注释前后的代码应拼成同一输出行,例如 a/*......*/b 最终得到 ab。因此只有在行末处于块注释外时,才输出并清空非空缓冲区。

状态机覆盖了所有合法输入:每个字符要么作为普通字符保留,要么属于行注释被整段跳过,要么在块注释中被忽略;三种情况互斥且没有遗漏。

解题步骤

  • 初始化 inBlock = false,并准备跨行复用的缓冲区 line
  • 逐行、逐字符扫描;判断双字符标记前先确认 i + 1 未越界。
  • 位于块注释内:遇到 */ 就退出状态并前进两格,否则忽略当前字符。
  • 位于块注释外:遇到 // 就结束本行;遇到 /* 就进入块注释;否则保留当前字符。
  • 扫描完一行后,若不在块注释中且缓冲区非空,就加入答案并清空。

a/*xy*/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. 反转字符串中的单词 中等 同样是单趟扫描重建字符串,但切分依据只是空白而无跨行状态