程序员老鬼

因为失误给公司造成了巨大的损失被辞退,公司还给了n+1,hr帮忙申请的,要不要请她吃顿饭

刚刷到这个,给我整不会了。

网友说自己工作失误,直接给公司捅了个大窟窿,最后被辞退。本来都做好空手走人的准备了,结果公司居然还给了N+1,而且还是HR帮忙申请下来的。

Image

这顿饭肯定得请啊,人家明显是在规则里尽量帮你兜了一把。别觉得HR只是动动嘴,很多时候这种补偿能不能批下来,真得有人愿意替你说话。

不过也别搞得太隆重,正常请顿饭,认真道个谢就行。更重要的是以后长点心,这次有人帮你收尾,下次可不一定还有这么好的HR。

今日面试题

这道“标签验证器”,真正容易写错的是扫描顺序

输入下面这段内容:

<A></A><B></B>

两个标签都闭合了,栈最后也是空的,但结果必须是 false。

这地方我第一眼就不会只做普通的括号匹配。题目验证的不是“每个标签能不能闭合”,而是整段代码必须被一个根标签完整包住。根标签一旦结束,后面哪怕再跟一个合法标签,也算非法。

标签名的限制倒不复杂:只能包含大写字母,长度在 1 到 9 之间。麻烦的是内容里还混着 CDATA:

<![CDATA[这里的 <A> 不算标签]]>

CDATA 中间出现什么都不用解析,找到最近的 ]]>,整段跳过去就行。但它只能出现在已打开的标签内部,直接把 CDATA 放在最外层,同样不合法。

这题我会用一个栈保存尚未闭合的标签,再拿一个游标从左往右扫。判断顺序不能乱:先识别 CDATA,再看结束标签,然后才是开始标签,剩下的字符按普通文本处理。

因为 CDATA 也是以 < 开头。先按普通标签解析,后面的判断基本都会被带偏。

import java.util.ArrayDeque;
import java.util.Deque;

classSolution{

publicbooleanisValid(String code){
if (code == null || code.isEmpty()) {
returnfalse;
        }

        Deque<String> activeTags = new ArrayDeque<>();
int cursor = 0;

while (cursor < code.length()) {
// 根标签已经结束,后面不允许再出现任何内容
if (cursor > 0 && activeTags.isEmpty()) {
returnfalse;
            }

if (code.startsWith("<![CDATA[", cursor)) {
if (activeTags.isEmpty()) {
returnfalse;
                }

int cdataEnd = code.indexOf("]]>", cursor + 9);
if (cdataEnd < 0) {
returnfalse;
                }

                cursor = cdataEnd + 3;
continue;
            }

if (code.startsWith("</", cursor)) {
int rightBracket = code.indexOf('>', cursor + 2);
if (rightBracket < 0) {
returnfalse;
                }

                String closingTag = code.substring(cursor + 2, rightBracket);
if (!legalName(closingTag)
                        || activeTags.isEmpty()
                        || !activeTags.peek().equals(closingTag)) {
returnfalse;
                }

                activeTags.pop();
                cursor = rightBracket + 1;
continue;
            }

if (code.charAt(cursor) == '<') {
int rightBracket = code.indexOf('>', cursor + 1);
if (rightBracket < 0) {
returnfalse;
                }

                String openingTag = code.substring(cursor + 1, rightBracket);
if (!legalName(openingTag)) {
returnfalse;
                }

                activeTags.push(openingTag);
                cursor = rightBracket + 1;
continue;
            }

            cursor++;
        }

return activeTags.isEmpty();
    }

privatebooleanlegalName(String tagName){
if (tagName.isEmpty() || tagName.length() > 9) {
returnfalse;
        }

for (int i = 0; i < tagName.length(); i++) {
char current = tagName.charAt(i);
if (current < 'A' || current > 'Z') {
returnfalse;
            }
        }
returntrue;
    }
}

代码里最值得留意的不是 push 和 pop,而是这一段:

if (cursor > 0 && activeTags.isEmpty()) {
returnfalse;
}

它专门拦截根标签结束后还有剩余内容的情况。没有这道判断,<A></A><B></B> 很容易被误判成合法。

整套扫描只会向前走。查找标签结束位置和 CDATA 结束位置虽然用了 indexOf,但游标不会回退,整体可以按线性时间理解,栈空间取决于标签嵌套深度。

这题看着像栈,实际考的是解析器的基本功:先分清当前读到的是什么,再决定怎么处理。扫描顺序一乱,栈写得再漂亮也没用。