0591-标签验证器

Raphael Liu Lv10

给定一个表示代码片段的字符串,你需要实现一个验证器来解析这段代码,并返回它是否合法。合法的代码片段需要遵守以下的所有规则:

  1. 代码必须被 合法的闭合标签 包围。否则,代码是无效的。
  2. 闭合标签 (不一定合法)要严格符合格式:<TAG_NAME>TAG_CONTENT</TAG_NAME>。其中,<TAG_NAME>是起始标签,</TAG_NAME>是结束标签。起始和结束标签中的 TAG_NAME 应当相同。当且仅当 TAG_NAME 和 TAG_CONTENT 都是合法的,闭合标签才是 合法的
  3. 合法的 TAG_NAME 仅含有 大写字母 ,长度在范围 [1,9] 之间。否则,该 TAG_NAME不合法的
  4. 合法的 TAG_CONTENT 可以包含其他 合法的闭合标签cdata (请参考规则7)和任意字符(注意参考规则1) 除了 不匹配的<、不匹配的起始和结束标签、不匹配的或带有不合法 TAG_NAME 的闭合标签。否则,TAG_CONTENT不合法的
  5. 一个起始标签,如果没有具有相同 TAG_NAME 的结束标签与之匹配,是不合法的。反之亦然。不过,你也需要考虑标签嵌套的问题。
  6. 一个<,如果你找不到一个后续的>与之匹配,是不合法的。并且当你找到一个<</时,所有直到下一个>的前的字符,都应当被解析为 TAG_NAME(不一定合法)。
  7. cdata 有如下格式:<![CDATA[CDATA_CONTENT]]>CDATA_CONTENT 的范围被定义成 <![CDATA[后续的第一个 ]]>之间的字符。
  8. 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. 为简明起见,你可以假设输入的代码(包括提到的 任意字符 )只包含数字, 字母, '<','>','/','!','[',']'' '

方法一:栈 + 字符串遍历

思路与算法

本题是一道解析字符串的题目,涉及到标签的闭合。由于标签具有「最先开始的标签最后结束」的特性,因此我们可以考虑使用一个栈存储当前开放的标签。除此之外,我们还需要考虑 cdata 以及一般的字符,二者都可以使用遍历 + 判断的方法直接进行验证。

我们可以对字符串 code 进行一次遍历。在遍历的过程中,根据遍历到位置 i 的当前字符,采取对应的判断:

  • 如果当前的字符为 <,那么需要考虑下面的四种情况:

    • 如果下一个字符为 /,那么说明我们遇到了一个结束标签。我们需要定位下一个 > 的位置 j,此时 code}[i+2..j-1] 就是该结束标签的名称。我们需要判断该名称与当前栈顶的名称是否匹配,如果匹配,说明名称的标签已经闭合,我们需要将当前栈顶的名称弹出。同时根据规则 1,我们需要保证整个 code 被闭合标签包围,因此如果栈中已经没有标签,但是 j 并不是 code 的末尾,那么说明后续还会有字符,它们不被闭合标签包围。

    • 如果下一个字符为 !,那么说明我们遇到了一个 cdata,我们需要继续往后读 7 个字符,判断其是否为 [CDATA[。在这之后,我们定位下一个 ]]> 的位置 j,此时 code}[i+9..j-1] 就是 cdata 中的内容,它不需要被解析,所以我们也不必进行任何验证。需要注意的是,根据规则 1,栈中需要存在至少一个开放的标签。

    • 如果下一个字符为大写字母,那么说明我们遇到了一个开始标签。我们需要定位下一个 > 的位置 j,此时 code}[i+2..j-1] 就是该开始标签的名称。我们需要判断该名称是否恰好由 1 至 9 个大写字母组成,如果是,说明该标签合法,我们需要将该名称放入栈顶。

    • 除此之外,如果不存在下一个字符,或者下一个字符不属于上述三种情况,那么 code 是不合法的。

  • 如果当前的字符为其它字符,那么根据规则 1,栈中需要存在至少一个开放的标签。

在遍历完成后,我们还需要保证此时栈中没有任何(还没有结束的)标签。

代码

[sol1-C++]
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
class Solution {
public:
bool isValid(string code) {
int n = code.size();
stack<string> tags;

int i = 0;
while (i < n) {
if (code[i] == '<') {
if (i == n - 1) {
return false;
}
if (code[i + 1] == '/') {
int j = code.find('>', i);
if (j == string::npos) {
return false;
}
string tagname = code.substr(i + 2, j - (i + 2));
if (tags.empty() || tags.top() != tagname) {
return false;
}
tags.pop();
i = j + 1;
if (tags.empty() && i != n) {
return false;
}
}
else if (code[i + 1] == '!') {
if (tags.empty()) {
return false;
}
string cdata = code.substr(i + 2, 7);
if (cdata != "[CDATA[") {
return false;
}
int j = code.find("]]>", i);
if (j == string::npos) {
return false;
}
i = j + 3;
}
else {
int j = code.find('>', i);
if (j == string::npos) {
return false;
}
string tagname = code.substr(i + 1, j - (i + 1));
if (tagname.size() < 1 || tagname.size() > 9) {
return false;
}
if (!all_of(tagname.begin(), tagname.end(), [](unsigned char c) { return isupper(c); })) {
return false;
}
tags.push(move(tagname));
i = j + 1;
}
}
else {
if (tags.empty()) {
return false;
}
++i;
}
}

return tags.empty();
}
};
[sol1-Java]
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
class Solution {
public boolean isValid(String code) {
int n = code.length();
Deque<String> tags = new ArrayDeque<String>();

int i = 0;
while (i < n) {
if (code.charAt(i) == '<') {
if (i == n - 1) {
return false;
}
if (code.charAt(i + 1) == '/') {
int j = code.indexOf('>', i);
if (j < 0) {
return false;
}
String tagname = code.substring(i + 2, j);
if (tags.isEmpty() || !tags.peek().equals(tagname)) {
return false;
}
tags.pop();
i = j + 1;
if (tags.isEmpty() && i != n) {
return false;
}
} else if (code.charAt(i + 1) == '!') {
if (tags.isEmpty()) {
return false;
}
if (i + 9 > n) {
return false;
}
String cdata = code.substring(i + 2, i + 9);
if (!"[CDATA[".equals(cdata)) {
return false;
}
int j = code.indexOf("]]>", i);
if (j < 0) {
return false;
}
i = j + 3;
} else {
int j = code.indexOf('>', i);
if (j < 0) {
return false;
}
String tagname = code.substring(i + 1, j);
if (tagname.length() < 1 || tagname.length() > 9) {
return false;
}
for (int k = 0; k < tagname.length(); ++k) {
if (!Character.isUpperCase(tagname.charAt(k))) {
return false;
}
}
tags.push(tagname);
i = j + 1;
}
} else {
if (tags.isEmpty()) {
return false;
}
++i;
}
}

return tags.isEmpty();
}
}
[sol1-C#]
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
public class Solution {
public bool IsValid(string code) {
int n = code.Length;
Stack<string> tags = new Stack<string>();

int i = 0;
while (i < n) {
if (code[i] == '<') {
if (i == n - 1) {
return false;
}
if (code[i + 1] == '/') {
int j = code.IndexOf('>', i);
if (j < 0) {
return false;
}
string tagname = code.Substring(i + 2, j - (i + 2));
if (tags.Count == 0 || !tags.Peek().Equals(tagname)) {
return false;
}
tags.Pop();
i = j + 1;
if (tags.Count == 0 && i != n) {
return false;
}
} else if (code[i + 1] == '!') {
if (tags.Count == 0) {
return false;
}
if (i + 9 > n) {
return false;
}
string cdata = code.Substring(i + 2, 7);
if (!"[CDATA[".Equals(cdata)) {
return false;
}
int j = code.IndexOf("]]>", i);
if (j < 0) {
return false;
}
i = j + 3;
} else {
int j = code.IndexOf('>', i);
if (j < 0) {
return false;
}
string tagname = code.Substring(i + 1, j - (i + 1));
if (tagname.Length < 1 || tagname.Length > 9) {
return false;
}
foreach (char c in tagname) {
if (!char.IsUpper(c)) {
return false;
}
}
tags.Push(tagname);
i = j + 1;
}
} else {
if (tags.Count == 0) {
return false;
}
++i;
}
}

return tags.Count == 0;
}
}
[sol1-Python3]
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
class Solution:
def isValid(self, code: str) -> bool:
n = len(code)
tags = list()

i = 0
while i < n:
if code[i] == "<":
if i == n - 1:
return False
if code[i + 1] == "/":
j = code.find(">", i)
if j == -1:
return False
tagname = code[i+2:j]
if not tags or tags[-1] != tagname:
return False
tags.pop()
i = j + 1
if not tags and i != n:
return False
elif code[i + 1] == "!":
if not tags:
return False
cdata = code[i+2:i+9]
if cdata != "[CDATA[":
return False
j = code.find("]]>", i)
if j == -1:
return False
i = j + 3
else:
j = code.find(">", i)
if j == -1:
return False
tagname = code[i+1:j]
if not 1 <= len(tagname) <= 9 or not all(ch.isupper() for ch in tagname):
return False
tags.append(tagname)
i = j + 1
else:
if not tags:
return False
i += 1

return not tags
[sol1-C]
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
#define MIN(a, b) ((a) < (b) ? (a) : (b))

bool isValid(char * code){
int n = strlen(code);
char ** tags = (char **)malloc(sizeof(char *) * n);

int i = 0;
int top = 0;
while (i < n) {
if (code[i] == '<') {
if (i == n - 1) {
return false;
}
if (code[i + 1] == '/') {
char *p = strchr(code + i, '>');
if (NULL == p) {
return false;
}
int j = p - code;
if (top == 0 || strncmp(tags[top - 1], code + i + 2, j - (i + 2)) != 0) {
return false;
}
free(tags[top - 1]);
top--;
i = j + 1;
if (top == 0 && i != n) {
return false;
}
} else if (code[i + 1] == '!') {
if (top == 0) {
return false;
}
if (strncmp(code + i + 2, "[CDATA[", 7) != 0) {
return false;
}
char *p = strstr(code + i, "]]>");
if (NULL == p) {
return false;
}
int j = p - code;
i = j + 3;
} else {
char *p = strchr(code + i, '>');
if (NULL == p) {
return false;
}
int j = p - code;
int len = MIN(n - i - 1, j - (i + 1));
if (len < 1 || len > 9) {
return false;
}
for (int k = 0; k < len; k++) {
if (!isupper(code[i + 1 + k])) {
return false;
}
}
char *tagname = (char *)malloc(sizeof(char) * (len + 1));
strncpy(tagname, code + i + 1, len);
tagname[len] = 0;
tags[top++] = tagname;
i = j + 1;
}
} else {
if (top == 0) {
return false;
}
++i;
}
}
return top == 0;
}
[sol1-JavaScript]
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
var isValid = function(code) {
const n = code.length;
const tags = [];

let i = 0;
while (i < n) {
if (code[i] === '<') {
if (i === n - 1) {
return false;
}
if (code[i + 1] === '/') {
const j = code.indexOf('>', i);
if (j < 0) {
return false;
}
const tagname = code.slice(i + 2, j);
if (tags.length === 0 || tags[tags.length - 1] !== tagname) {
return false;
}
tags.pop();
i = j + 1;
if (tags.length === 0 && i !== n) {
return false;
}
} else if (code[i + 1] === '!') {
if (tags.length === 0) {
return false;
}
if (i + 9 > n) {
return false;
}
const cdata = code.slice(i + 2, i + 9);
if ("[CDATA[" !== cdata) {
return false;
}
const j = code.indexOf("]]>", i);
if (j < 0) {
return false;
}
i = j + 3;
} else {
const j = code.indexOf('>', i);
if (j < 0) {
return false;
}
const tagname = code.slice(i + 1, j);
if (tagname.length < 1 || tagname.length > 9) {
return false;
}
for (let k = 0; k < tagname.length; ++k) {
if (!(tagname[k] >= 'A' && tagname[k] <= 'Z')) {
return false;
}
}
tags.push(tagname);
i = j + 1;
}
} else {
if (tags.length === 0) {
return false;
}
++i;
}
}

return tags.length === 0;
};
[sol1-Golang]
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
func isValid(code string) bool {
tags := []string{}
for code != "" {
if code[0] != '<' {
if len(tags) == 0 {
return false
}
code = code[1:]
continue
}
if len(code) == 1 {
return false
}
if code[1] == '/' {
j := strings.IndexByte(code, '>')
if j == -1 {
return false
}
if len(tags) == 0 || tags[len(tags)-1] != code[2:j] {
return false
}
tags = tags[:len(tags)-1]
code = code[j+1:]
if len(tags) == 0 && code != "" {
return false
}
} else if code[1] == '!' {
if len(tags) == 0 || len(code) < 9 || code[2:9] != "[CDATA[" {
return false
}
j := strings.Index(code, "]]>")
if j == -1 {
return false
}
code = code[j+3:]
} else {
j := strings.IndexByte(code, '>')
if j == -1 {
return false
}
tagName := code[1:j]
if tagName == "" || len(tagName) > 9 {
return false
}
for _, ch := range tagName {
if !unicode.IsUpper(ch) {
return false
}
}
tags = append(tags, tagName)
code = code[j+1:]
}
}
return len(tags) == 0
}

复杂度分析

  • 时间复杂度:O(n),其中 n 是字符串 code 的长度。我们只需要对 code 进行一次遍历。

  • 空间复杂度:O(n),即为栈存储标签名称需要使用的空间。

 Comments
On this page
0591-标签验证器