一、题目描述
给定一个表示代码片段的字符串,你需要实现一个验证器来解析这段代码,并返回它是否合法。合法的代码片段需要遵守以下的所有规则:
- 代码必须被合法的闭合标签包围。否则,代码是无效的。
- 闭合标签 (不一定合法)要严格符合格式:
<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
来处理各种非法情况,例如不匹配的标签、不合法的标签名、字符串结束前没有闭合的标签等。
- 返回
以上就是解决这个问题的详细步骤,希望能够为各位提供启发和帮助。