一、题目描述
给定一个表示代码片段的字符串,你需要实现一个验证器来解析这段代码,并返回它是否合法。合法的代码片段需要遵守以下的所有规则:
- 代码必须被合法的闭合标签包围。否则,代码是无效的。
- 闭合标签 (不一定合法)要严格符合格式:
<TAG_NAME>TAG_CONTENT</TAG_NAME>。其中,<TAG_NAME>是起始标签,</TAG_NAME>是结束标签。起始和结束标签中的 TAG_NAME 应当相同。当且仅当 TAG_NAME 和 TAG_CONTENT 都是合法的,闭合标签才是合法的。 - 合法的
TAG_NAME仅含有大写字母 ,长度在范围 [1,9] 之间。否则,该TAG_NAME是不合法的。 - 合法的
TAG_CONTENT可以包含其他合法的闭合标签 ,cdata (请参考规则7)和任意字符(注意参考规则1)除了 不匹配的<、不匹配的起始和结束标签、不匹配的或带有不合法 TAG_NAME 的闭合标签。否则,TAG_CONTENT是不合法的。 - 一个起始标签,如果没有具有相同 TAG_NAME 的结束标签与之匹配,是不合法的。反之亦然。不过,你也需要考虑标签嵌套的问题。
- 一个
<,如果你找不到一个后续的>与之匹配,是不合法的。并且当你找到一个<或</时,所有直到下一个>的前的字符,都应当被解析为 TAG_NAME(不一定合法)。 - cdata 有如下格式:
<![CDATA[CDATA_CONTENT]]>。CDATA_CONTENT的范围被定义成<![CDATA[和后续的第一个]]>之间的字符。 CDATA_CONTENT可以包含任意字符 。cdata 的功能是阻止验证器解析CDATA_CONTENT,所以即使其中有一些字符可以被解析为标签(无论合法还是不合法),也应该将它们视为常规字符。
合法代码的例子:
输入: "<DIV>This is the first line <![CDATA[<div>]]></DIV>"
输出: True
解释:
代码被包含在了闭合的标签内: <DIV> 和 </DIV> 。
TAG_NAME 是合法的,TAG_CONTENT 包含了一些字符和 cdata 。
即使 CDATA_CONTENT 含有不匹配的起始标签和不合法的 TAG_NAME,它应该被视为普通的文本,而不是标签。
所以 TAG_CONTENT 是合法的,因此代码是合法的。最终返回True。
输入: "<DIV>>> ![cdata[]] <![CDATA[<div>]>]]>]]>>]</DIV>"
输出: True
解释:
我们首先将代码分割为: start_tag|tag_content|end_tag 。
start_tag -> "\<DIV\>"
end_tag -> "\</DIV\>"
tag_content 也可被分割为: text1|cdata|text2 。
text1 -> "\>\> !\[cdata\[\]\] "
cdata -> "\<!\[CDATA\[\<div\>\]\>\]\]\>" ,其中 CDATA_CONTENT 为 "\<div\>\]\>"
text2 -> "\]\]\>\>\]"
start_tag 不是 "\<DIV\>\>\>" 的原因参照规则 6 。
cdata 不是 "\<!\[CDATA\[\<div\>\]\>\]\]\>\]\]\>" 的原因参照规则 7 。
不合法代码的例子:
输入: "<A> <B> </A> </B>"
输出: False
解释: 不合法。如果 "<A>" 是闭合的,那么 "<B>" 一定是不匹配的,反之亦然。
输入: "<DIV> div tag is not closed <DIV>"
输出: False
输入: "<DIV> unmatched < </DIV>"
输出: False
输入: "<DIV> closed tags with invalid tag name <b>123</b> </DIV>"
输出: False
输入: "<DIV> unmatched tags with invalid tag name </1234567890> and <CDATA[[]]> </DIV>"
输出: False
输入: "<DIV> unmatched start tag <B> and unmatched end tag </C> </DIV>"
输出: False
注意:
- 为简明起见,你可以假设输入的代码(包括提到的任意字符 )只包含
数字, 字母,'<','>','/','!','[',']'和' '。
二、解题思路
-
首先,我们需要定义一个函数来检查一个字符串是否是合法的标签名。这个函数应该检查字符串是否只包含大写字母,并且长度在1到9之间。
-
我们需要一个辅助函数来解析字符串并找到下一个标签的开始位置。这个函数应该返回标签的类型(起始标签、结束标签或CDATA),标签名,以及下一个标签的开始位置。
-
使用一个栈来管理标签。当我们遇到一个起始标签时,将其推入栈中;当我们遇到一个结束标签时,检查栈顶元素是否与结束标签匹配,如果匹配则弹出栈顶元素。
-
解析字符串时,我们需要处理以下几种情况:
- 如果遇到
<,我们需要判断是起始标签、结束标签还是CDATA。 - 如果是起始标签或结束标签,我们需要检查标签名是否合法,并相应地处理栈。
- 如果是CDATA,我们需要找到CDATA的结束位置,并跳过其中的内容。
- 如果遇到其他字符,我们需要确保它们不是不匹配的
<。
- 如果遇到
-
在解析结束后,如果栈为空,则代码是合法的;否则,它是非法的。
三、具体代码
java
import java.util.*;
public class Solution {
public boolean isValid(String code) {
Stack<String> stack = new Stack<>();
int i = 0;
while (i < code.length()) {
if (code.charAt(i) == '<') {
if (i == code.length() - 1) return false; // '<' is the last character
if (code.charAt(i + 1) == '/') { // End tag
int j = code.indexOf('>', i);
if (j == -1) return false; // No matching '>'
String tagName = code.substring(i + 2, j);
if (!isValidTagName(tagName) || stack.isEmpty() || !stack.pop().equals(tagName)) {
return false;
}
i = j + 1;
} else if (code.charAt(i + 1) == '!') { // CDATA
if (!stack.isEmpty() && code.startsWith("<![CDATA[", i)) {
int j = code.indexOf("]]>", i);
if (j == -1) return false; // No matching ']]>'
i = j + 3;
} else {
return false;
}
} else { // Start tag
int j = code.indexOf('>', i);
if (j == -1) return false; // No matching '>'
String tagName = code.substring(i + 1, j);
if (!isValidTagName(tagName)) return false;
stack.push(tagName);
i = j + 1;
}
} else {
i++;
}
}
return stack.isEmpty();
}
private boolean isValidTagName(String tagName) {
return tagName.matches("[A-Z]{1,9}");
}
}
这段代码定义了一个Solution类,其中包含isValid方法用于检查代码是否合法,以及一个辅助方法isValidTagName用于检查标签名是否合法。代码逻辑按照上述解题思路实现。
四、时间复杂度和空间复杂度
1. 时间复杂度
-
我们定义
code.length()为n,这是输入字符串的长度。 -
我们有一个主循环,它遍历字符串
code的每个字符。这个循环最多执行n次。 -
在每次循环中,我们可能会执行以下操作:
- 检查是否是起始标签、结束标签或CDATA,这涉及到字符串比较,其时间复杂度为O(1)。
- 查找下一个
>字符的位置,使用indexOf方法,其时间复杂度为O(n)。 - 检查标签名是否合法,使用正则表达式匹配,其时间复杂度为O(1),因为标签名长度限制在1到9之间。
- 向栈中推入或弹出元素,其时间复杂度为O(1)。
-
由于
indexOf方法在每次遇到<字符时都会被调用,并且在最坏情况下,它可能需要遍历整个字符串,因此,整个算法的时间复杂度主要取决于indexOf方法的调用次数和每次调用的成本。 -
在最坏情况下,
indexOf方法可能在每次循环迭代中被调用一次,总共有n次迭代,因此时间复杂度为O(n^2)。
2. 空间复杂度
-
我们使用了一个栈
stack来存储标签名。 -
在最坏的情况下,如果所有标签都是嵌套的,那么栈的大小可能达到
n/2(假设每个标签都是成对的,并且每个标签长度为2,这是最紧凑的情况)。 -
因此,空间复杂度为O(n),其中
n是输入字符串的长度。
五、总结知识点
-
Java 类的定义:
public class Solution定义了一个公共类Solution。
-
Java 方法的定义:
public boolean isValid(String code)定义了一个公共方法isValid,它接受一个字符串参数并返回一个布尔值。
-
Java 栈的使用:
Stack<String> stack = new Stack<>();创建了一个字符串类型的栈,用于管理标签。
-
字符串操作:
- 使用
charAt(int index)方法来获取字符串中特定位置的字符。 - 使用
indexOf(char ch)方法来查找字符在字符串中的位置。 - 使用
substring(int beginIndex, int endIndex)方法来获取字符串的子串。
- 使用
-
循环和条件语句:
- 使用
while循环来遍历字符串。 - 使用
if-else语句来处理不同类型的标签(起始标签、结束标签、CDATA)。
- 使用
-
正则表达式:
- 使用
matches(String regex)方法来检查字符串是否符合给定的正则表达式模式。
- 使用
-
异常处理:
- 检查字符串是否以特定的子串开始,例如
startsWith(String prefix)。 - 检查字符串是否在特定位置结束,例如检查是否没有匹配的结束标签
>。
- 检查字符串是否以特定的子串开始,例如
-
栈操作:
- 使用
push(E element)方法将元素推入栈。 - 使用
pop()方法从栈中移除并返回栈顶元素。 - 使用
isEmpty()方法检查栈是否为空。
- 使用
-
字符串与字符的比较:
- 使用
==运算符来比较字符是否相等。
- 使用
-
错误处理:
- 返回
false来处理各种非法情况,例如不匹配的标签、不合法的标签名、字符串结束前没有闭合的标签等。
- 返回
以上就是解决这个问题的详细步骤,希望能够为各位提供启发和帮助。