|
| 1 | +### 题目描述 |
| 2 | + |
| 3 | +这是 LeetCode 上的 **[591. 标签验证器](https://leetcode-cn.com/problems/tag-validator/solution/by-ac_oier-9l8z/)** ,难度为 **困难**。 |
| 4 | + |
| 5 | +Tag : 「模拟」、「栈」 |
| 6 | + |
| 7 | +m |
| 8 | + |
| 9 | +给定一个表示代码片段的字符串,你需要实现一个验证器来解析这段代码,并返回它是否合法。合法的代码片段需要遵守以下的所有规则: |
| 10 | + |
| 11 | +1. 代码必须被合法的闭合标签包围。否则,代码是无效的。 |
| 12 | +2. 闭合标签(不一定合法)要严格符合格式:`<TAG_NAME>TAG_CONTENT</TAG_NAME>`。其中,`<TAG_NAME>` 是起始标签,`</TAG_NAME>` 是结束标签。起始和结束标签中的 `TAG_NAME` 应当相同。当且仅当 `TAG_NAME` 和 `TAG_CONTENT` 都是合法的,闭合标签才是合法的。 |
| 13 | +3. 合法的 `TAG_NAME` 仅含有大写字母,长度在范围 $[1,9]$ 之间。否则,该 `TAG_NAME` 是不合法的。 |
| 14 | +4. 合法的 `TAG_CONTENT` 可以包含其他合法的闭合标签,`cdata` ( 请参考规则 $7$ )和任意字符( 注意参考规则 $1$ )除了不匹配的 `<`、不匹配的起始和结束标签、不匹配的或带有不合法 `TAG_NAME` 的闭合标签。否则,`TAG_CONTENT` 是不合法的。 |
| 15 | +5. 一个起始标签,如果没有具有相同 `TAG_NAME` 的结束标签与之匹配,是不合法的。反之亦然。不过,你也需要考虑标签嵌套的问题。 |
| 16 | +6. 一个 `<`,如果你找不到一个后续的 `>` 与之匹配,是不合法的。并且当你找到一个 `<` 或`</` 时,所有直到下一个 `>` 的前的字符,都应当被解析为 `TAG_NAME`(不一定合法)。 |
| 17 | +7. `cdata` 有如下格式:`<![CDATA[CDATA_CONTENT]]>`。`CDATA_CONTENT` 的范围被定义成 `<![CDATA[` 和后续的第一个 `]]>`之间的字符。 |
| 18 | +8. `CDATA_CONTENT` 可以包含任意字符。`cdata` 的功能是阻止验证器解析 `CDATA_CONTENT`,所以即使其中有一些字符可以被解析为标签(无论合法还是不合法),也应该将它们视为常规字符。 |
| 19 | + |
| 20 | +合法代码的例子: |
| 21 | +``` |
| 22 | +输入: "<DIV>This is the first line <![CDATA[<div>]]></DIV>" |
| 23 | +
|
| 24 | +输出: True |
| 25 | +
|
| 26 | +解释: |
| 27 | +
|
| 28 | +代码被包含在了闭合的标签内: <DIV> 和 </DIV> 。 |
| 29 | +
|
| 30 | +TAG_NAME 是合法的,TAG_CONTENT 包含了一些字符和 cdata 。 |
| 31 | +
|
| 32 | +即使 CDATA_CONTENT 含有不匹配的起始标签和不合法的 TAG_NAME,它应该被视为普通的文本,而不是标签。 |
| 33 | +
|
| 34 | +所以 TAG_CONTENT 是合法的,因此代码是合法的。最终返回True。 |
| 35 | +
|
| 36 | +
|
| 37 | +输入: "<DIV>>> ![cdata[]] <![CDATA[<div>]>]]>]]>>]</DIV>" |
| 38 | +
|
| 39 | +输出: True |
| 40 | +
|
| 41 | +解释: |
| 42 | +
|
| 43 | +我们首先将代码分割为: start_tag|tag_content|end_tag 。 |
| 44 | +
|
| 45 | +start_tag -> "<DIV>" |
| 46 | +
|
| 47 | +end_tag -> "</DIV>" |
| 48 | +
|
| 49 | +tag_content 也可被分割为: text1|cdata|text2 。 |
| 50 | +
|
| 51 | +text1 -> ">> ![cdata[]] " |
| 52 | +
|
| 53 | +cdata -> "<![CDATA[<div>]>]]>" ,其中 CDATA_CONTENT 为 "<div>]>" |
| 54 | +
|
| 55 | +text2 -> "]]>>]" |
| 56 | +
|
| 57 | +
|
| 58 | +start_tag 不是 "<DIV>>>" 的原因参照规则 6 。 |
| 59 | +cdata 不是 "<![CDATA[<div>]>]]>]]>" 的原因参照规则 7 。 |
| 60 | +``` |
| 61 | +不合法代码的例子: |
| 62 | +``` |
| 63 | +输入: "<A> <B> </A> </B>" |
| 64 | +输出: False |
| 65 | +解释: 不合法。如果 "<A>" 是闭合的,那么 "<B>" 一定是不匹配的,反之亦然。 |
| 66 | +
|
| 67 | +输入: "<DIV> div tag is not closed <DIV>" |
| 68 | +输出: False |
| 69 | +
|
| 70 | +输入: "<DIV> unmatched < </DIV>" |
| 71 | +输出: False |
| 72 | +
|
| 73 | +输入: "<DIV> closed tags with invalid tag name <b>123</b> </DIV>" |
| 74 | +输出: False |
| 75 | +
|
| 76 | +输入: "<DIV> unmatched tags with invalid tag name </1234567890> and <CDATA[[]]> </DIV>" |
| 77 | +输出: False |
| 78 | +
|
| 79 | +输入: "<DIV> unmatched start tag <B> and unmatched end tag </C> </DIV>" |
| 80 | +输出: False |
| 81 | +``` |
| 82 | +注意: |
| 83 | +* 为简明起见,你可以假设输入的代码(包括提到的任意字符)只包含`数字`, `字母`, `'<'`,`'>'`,`'/'`,`'!'`,`'['`,`']'`和`' '`。 |
| 84 | + |
| 85 | +--- |
| 86 | + |
| 87 | +### 模拟(栈) |
| 88 | + |
| 89 | +字符串大模拟,假设字符串 `s` 长度为 $n$,当前处理到的位置为 $i$,根据以下优先级进行检查: |
| 90 | + |
| 91 | +1. 优先尝试检查以 $i$ 为开始的连续段是否为 `CDATA`,若能匹配到开头,则尝试匹配到 `CDATA` 的结尾处,并更新 $i$,若无法找到结尾,返回 `False`; |
| 92 | +2. 尝试匹配 $s[i]$ 是否为 `<`,若满足,则根据 $s[i + 1]$ 是否为 `/` 来判断当前 `TAG_NAME` 是处于右边还是左边,然后将 `TAG_NAME` 取出,记为 $tag$,判断 $tag$ 长度是否合法,不合法返回 `False`,合法则根据是左边还是右边的 `TAG_NAME` 分情况讨论: |
| 93 | + * 位于左边的 `TAG_NAME`:将其加入栈中,等待右边的 `TAG_NAME` 与其匹配; |
| 94 | + * 位于右边的 `TAG_NAME`:将其与当前栈顶的元素进行匹配,若栈为空或匹配不上,返回 `False`(注意:由于整个 `s` 应当被一对 `TAG_NAME` 所包裹,因此如果取出后栈顶元素匹配后,栈为空,同时又还没处理完整个 `s`,此时 `s` 也不合法,返回 `Fasle`); |
| 95 | +3. 其余情况则为普通字符。 |
| 96 | + |
| 97 | +最后由于整个 `s` 应当被一对 `TAG_NAME` 所包裹,因此当 $i = 0$ 时,不能是情况 $1$ 和情况 $3$,需要特判一下。 |
| 98 | + |
| 99 | +代码: |
| 100 | +```Java |
| 101 | +class Solution { |
| 102 | + String CDATA1 = "<![CDATA[", CDATA2 = "]]>"; |
| 103 | + public boolean isValid(String s) { |
| 104 | + Deque<String> d = new ArrayDeque<>(); |
| 105 | + int n = s.length(), i = 0; |
| 106 | + while (i < n) { |
| 107 | + if (i + 8 < n && s.substring(i, i + 9).equals(CDATA1)) { |
| 108 | + if (i == 0) return false; |
| 109 | + int j = i + 9; |
| 110 | + boolean ok = false; |
| 111 | + while (j < n && !ok) { |
| 112 | + if (j + 2 < n && s.substring(j, j + 3).equals(CDATA2)) { |
| 113 | + j = j + 3; ok = true; |
| 114 | + } else { |
| 115 | + j++; |
| 116 | + } |
| 117 | + } |
| 118 | + if (!ok) return false; |
| 119 | + i = j; |
| 120 | + } else if (s.charAt(i) == '<') { |
| 121 | + if (i == n - 1) return false; |
| 122 | + boolean isEnd = s.charAt(i + 1) == '/'; |
| 123 | + int p = isEnd ? i + 2 : i + 1, j = p; |
| 124 | + while (j < n && s.charAt(j) != '>') { |
| 125 | + if (!Character.isUpperCase(s.charAt(j))) return false; |
| 126 | + j++; |
| 127 | + } |
| 128 | + if (j == n) return false; |
| 129 | + int len = j - p; |
| 130 | + if (len < 1 || len > 9) return false; |
| 131 | + String tag = s.substring(p, j); |
| 132 | + i = j + 1; |
| 133 | + if (!isEnd) { |
| 134 | + d.addLast(tag); |
| 135 | + } else { |
| 136 | + if (d.isEmpty() || !d.pollLast().equals(tag)) return false; |
| 137 | + if (d.isEmpty() && i < n) return false; |
| 138 | + } |
| 139 | + |
| 140 | + } else { |
| 141 | + if (i == 0) return false; |
| 142 | + i++; |
| 143 | + } |
| 144 | + } |
| 145 | + return d.isEmpty(); |
| 146 | + } |
| 147 | +} |
| 148 | +``` |
| 149 | +* 时间复杂度:$O(n)$ |
| 150 | +* 空间复杂度:$O(n)$ |
| 151 | + |
| 152 | +--- |
| 153 | + |
| 154 | +### 最后 |
| 155 | + |
| 156 | +这是我们「刷穿 LeetCode」系列文章的第 `No.591` 篇,系列开始于 2021/01/01,截止于起始日 LeetCode 上共有 1916 道题目,部分是有锁题,我们将先把所有不带锁的题目刷完。 |
| 157 | + |
| 158 | +在这个系列文章里面,除了讲解解题思路以外,还会尽可能给出最为简洁的代码。如果涉及通解还会相应的代码模板。 |
| 159 | + |
| 160 | +为了方便各位同学能够电脑上进行调试和提交代码,我建立了相关的仓库:https://github.com/SharingSource/LogicStack-LeetCode 。 |
| 161 | + |
| 162 | +在仓库地址里,你可以看到系列文章的题解链接、系列文章的相应代码、LeetCode 原题链接和其他优选题解。 |
| 163 | + |
0 commit comments