标签验证器

标签: 字符串

难度: Hard

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

  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. 为简明起见,你可以假设输入的代码(包括提到的任意字符)只包含数字, 字母, '<','>','/','!','[',']'' '

Submission

运行时间: 28 ms

内存: 16.2 MB

class Solution:
    def isValid(self, code: str) -> bool:
        stack = []  # 用于存储标签的栈
        i = 0
        n = len(code)
        
        while i < n:
            if i > 0 and len(stack) == 0:
                return False  # 如果代码不是被合法的闭合标签包围,返回False
            
            if code.startswith("<![CDATA[", i):
                end_idx = code.find("]]>", i)
                if end_idx == -1:
                    return False  # 如果没有找到闭合的CDATA标签,返回False
                i = end_idx + 3
            elif code.startswith("</", i):
                end_idx = code.find(">", i)
                if end_idx == -1:
                    return False  # 如果没有找到闭合的标签,返回False
                tag_name = code[i+2:end_idx]
                if not self.isValidTagName(tag_name):
                    return False  # 如果标签名不合法,返回False
                if len(stack) == 0 or stack[-1] != tag_name:
                    return False  # 如果栈为空或栈顶标签与当前标签不匹配,返回False
                stack.pop()
                i = end_idx + 1
            elif code.startswith("<", i):
                end_idx = code.find(">", i)
                if end_idx == -1:
                    return False  # 如果没有找到闭合的标签,返回False
                tag_name = code[i+1:end_idx]
                if not self.isValidTagName(tag_name):
                    return False  # 如果标签名不合法,返回False
                stack.append(tag_name)
                i = end_idx + 1
            else:
                i += 1
        
        return len(stack) == 0  # 如果栈为空,说明所有标签都闭合了,返回True;否则返回False
    
    def isValidTagName(self, tag_name: str) -> bool:
        if not tag_name.isupper():
            return False
        if len(tag_name) < 1 or len(tag_name) > 9:
            return False
        return tag_name.isalpha()

solution = Solution()
print(solution.isValid("<DIV>This is the first line <![CDATA[<div>]]></DIV>"))  # True
print(solution.isValid("<DIV>>>  ![cdata[]] <![CDATA[<div>]>]]>]]>>]</DIV>"))  # True
print(solution.isValid("<A>  <B> </A>   </B>"))  # False
print(solution.isValid("<DIV>  div tag is not closed  <DIV>"))  # False
print(solution.isValid("<DIV>  unmatched <  </DIV>"))  # False
print(solution.isValid("<DIV> closed tags with invalid tag name  <b>123</b> </DIV>"))  # False
print(solution.isValid("<DIV> unmatched tags with invalid tag name  </1234567890> and <CDATA[[]]>  </DIV>"))  # False
print(solution.isValid("<DIV>  unmatched start tag <B>  and unmatched end tag </C>  </DIV>"))  # False

Explain

这个题解使用栈来验证代码的合法性。遍历整个代码字符串,遇到开始标签时入栈,遇到结束标签时判断是否与栈顶标签匹配并出栈。同时需要处理 CDATA 标签和一些特殊情况。最后判断栈是否为空,为空则代码合法,否则不合法。

时间复杂度: O(n)

空间复杂度: O(n)

class Solution:
    def isValid(self, code: str) -> bool:
        stack = []  # 用于存储标签的栈
        i = 0
        n = len(code)
        
        while i < n:
            if i > 0 and len(stack) == 0:
                return False  # 如果代码不是被合法的闭合标签包围,返回False
            
            if code.startswith("<![CDATA[", i):
                end_idx = code.find("]]>", i)
                if end_idx == -1:
                    return False  # 如果没有找到闭合的CDATA标签,返回False
                i = end_idx + 3
            elif code.startswith("</", i):
                end_idx = code.find(">", i)
                if end_idx == -1:
                    return False  # 如果没有找到闭合的标签,返回False
                tag_name = code[i+2:end_idx]
                if not self.isValidTagName(tag_name):
                    return False  # 如果标签名不合法,返回False
                if len(stack) == 0 or stack[-1] != tag_name:
                    return False  # 如果栈为空或栈顶标签与当前标签不匹配,返回False
                stack.pop()  # 弹出栈顶标签
                i = end_idx + 1
            elif code.startswith("<", i):
                end_idx = code.find(">", i)
                if end_idx == -1:
                    return False  # 如果没有找到闭合的标签,返回False
                tag_name = code[i+1:end_idx]
                if not self.isValidTagName(tag_name):
                    return False  # 如果标签名不合法,返回False
                stack.append(tag_name)  # 将标签名入栈
                i = end_idx + 1
            else:
                i += 1
        
        return len(stack) == 0  # 如果栈为空,说明所有标签都闭合了,返回True;否则返回False
    
    def isValidTagName(self, tag_name: str) -> bool:
        if not tag_name.isupper():
            return False  # 标签名必须全部由大写字母组成
        if len(tag_name) < 1 or len(tag_name) > 9:
            return False  # 标签名长度必须在1到9之间
        return tag_name.isalpha()  # 标签名必须全部由字母组成

Explore

在算法中,当遇到`<![CDATA[`标签后,代码使用`code.find(']]>', i)`来查找CDATA区段的结束点。在这种情况下,即使CDATA内容中包含伪造的`]]>`,`find`方法会返回第一个出现的`]]>`的位置。因此,这里的关键是如何处理CDATA内的数据。理论上,CDATA区块允许包含任何字符,包括`]]>`,但是在实际处理中,一旦检测到第一个`]]>`序列,算法就会认定CDATA区块结束。为了更准确处理,可以通过在CDATA区块中逐字符检查,直到遇到确切无误的闭合标签`]]>`,但这会增加算法的复杂度。当前算法假定输入是合理的,不包含误导性的CDATA结束标签。

函数`isValidTagName`中的标签名验证逻辑首先检查标签名是否全部由大写字母组成,然后检查其长度是否在1到9之间。这种分开处理主要是为了清晰地区分不同的验证规则,并且让错误类型更容易被理解和调试。例如,如果一个标签名由数字组成,那么首先违反的是“必须由字母组成”的规则,而不是长度规则。通过分开检查,可以更精确地提供错误反馈,告诉用户具体是哪部分的验证失败了。这种方法也有利于代码的可维护性和可读性。

在当前的算法实现中,一旦遇到非法标签或其他验证失败的情况,就直接返回`False`,这意味着整个字符串不符合要求。这种设计选择是基于效率和实用性的考虑。虽然在某些应用场景中,继续解析以识别所有可能的错误可以提供更详细的错误信息,但在这种情况下,目标是尽快确认字符串是否完全有效。继续解析可能会导致算法效率降低,特别是在处理很长的字符串时。如果需要详尽的错误报告,可以设计一个更复杂的解析器,不仅检查合法性,还能收集所有的错误详情。