计算机有多难?
今天无意中,看到一篇关于「"土猪拱白菜"学霸后悔报考浙大计算机」的文章。
或许会有不少和我刚开始一样懵圈的同学:张锡峰是谁?"土猪拱白菜"又是什么梗?
带着疑惑,我打开了简中网著名的搜索引擎,找到这位曾经少年的大三在读生的故事。
张锡峰曾就读于衡水中学,在 2021 年,作为高三学生的他参加综艺节目《超级演说家》,因演讲内容「我就是一只来自乡下的"土猪",也要立志,去拱了大城市里的白菜」而出圈。
当时这句话被病毒式传播。
这场演讲也被普遍解读为:不屈不挠、勇于奋斗。尽管来自小地方,但有着进入大城市、实现自己梦想的决心。
有流量的地方就有对立,除了得到了大多数人的共鸣认可以外,也有不少质疑张锡峰的声音。
面对这些声音,张锡峰除了强势回应「你以为我们每天天不亮就奔向操场,一边奔跑一边呼喊是为了什么?是假装吗?是作秀吗?我们是为了改命啊!」以外,「还以 674 分的高考成绩考入浙大计算机」。
如果故事只是到这里结束,那很好,一个眼里有光的少年,面对质疑打破质疑,成就自我。
但时间很快,张锡峰现在是一名大三学生,也要考虑实习找工作的事儿,也要面临这日趋严峻的就业环境。
遗憾的是,重现在网友面前的张锡峰,从「阳光、活力、干翻一切」变成「平静、冷漠、眼里没光」。
这让我想起来之前在公众号的网友留言,原话我不太记得了,大致意思是:当时报计算机专业,真觉得挺好,薪资也高,但随着找不到工作,以及参与工作之后的 996,也分不清楚自己是不是真的喜欢这行了。
这些话题看多了,聊多了,我开始理解,有些兴趣爱好甚至是热爱,之所以还在,可能真就被生活放了一马罢了。
让我感到绝望的是,代表希望的种子被击落之后,不少人开始反过来攻击种子本身。
最近网上出现了另一种质疑声音:会不会这个张锡峰就是个普通的"小镇做题家",只是那场演讲把他捧到了天上。
但只要继续深究就不难发现,早在那场演讲之前,张锡峰就以「励志、干翻一切」小有名气。
2019 年,他在衡水中学的演讲《这世间,唯有青春与梦想不可辜负》在 B 站就有 2300W+ 的播放量。
所以,是他的励志使他站上了《超级演说家》的舞台,而不是刚好被综艺的风吹起。
校园世界和现实世界,是两个世界,这没错。
但不代表我们不需要张锡峰这样的孩子,不需要这种向往。
张锡峰只是在他那个年纪做了大多数人都想做的事情,他或许失败了,但不该被嘲讽,不该成为乐子。
就像哥谭不该只有蝙蝠侠一样。
...
回归主线。
来一道和「华为」相关的题目。
题目描述
平台:LeetCode
题号:1032
设计一个算法:接收一个字符流,并检查这些字符的后缀是否是字符串数组 words
中的一个字符串。
例如,words = ["abc", "xyz"]
且字符流中逐个依次加入 个字符 'a'
、'x'
、'y'
和 'z'
,你所设计的算法应当可以检测到 "axyz"
的后缀 "xyz"
与 words
中的字符串 "xyz"
匹配。
按下述要求实现 StreamChecker
类:
-
StreamChecker(String[] words)
:构造函数,用字符串数组words
初始化数据结构。 -
boolean query(char letter)
:从字符流中接收一个新字符,如果字符流中的任一非空后缀能匹配words
中的某一字符串,返回true
;否则,返回false
。
示例:
输入:
["StreamChecker", "query", "query", "query", "query", "query", "query", "query", "query", "query", "query", "query", "query"]
[[["cd", "f", "kl"]], ["a"], ["b"], ["c"], ["d"], ["e"], ["f"], ["g"], ["h"], ["i"], ["j"], ["k"], ["l"]]
输出:
[null, false, false, false, true, false, true, false, false, false, false, false, true]
解释:
StreamChecker streamChecker = new StreamChecker(["cd", "f", "kl"]);
streamChecker.query("a"); // 返回 False
streamChecker.query("b"); // 返回 False
streamChecker.query("c"); // 返回n False
streamChecker.query("d"); // 返回 True ,因为 'cd' 在 words 中
streamChecker.query("e"); // 返回 False
streamChecker.query("f"); // 返回 True ,因为 'f' 在 words 中
streamChecker.query("g"); // 返回 False
streamChecker.query("h"); // 返回 False
streamChecker.query("i"); // 返回 False
streamChecker.query("j"); // 返回 False
streamChecker.query("k"); // 返回 False
streamChecker.query("l"); // 返回 True ,因为 'kl' 在 words 中
提示:
-
-
-
words[i]
由小写英文字母组成 -
letter
是一个小写英文字母 -
最多调用查询 次
Trie + 枚举
先考虑最为简单的做法:将给定的所有 顺序插入字典树,根据数据范围可知这一步计算量为 ,其中最大的 长度只有 200。
然后利用 长度只有 200 这一条件,直接使用「枚举」的方式来实现 query
。
具体的,我们可以先使用一个字符串 s
来记录 query
操作产生的数据流,然后实现一个 boolean query(int start, int end)
方法,该方法会检查字典树中是否存在 子串。
由于 长度只有 200(假设当前 s
的长度为 n
),因此我们只需要枚举「 作为子串左端点, 作为子串右端点」是否存在字典树中(是否存在 中)即可,最坏情况下,单次 query
操作计算量为 。
❝一些细节:为了避免每个样例都
❞new
大数组,我们可以使用static
优化。
Java 代码:
class StreamChecker {
static int N = 2010 * 200, idx = 0;
static int[][] tr = new int[N][26];
static boolean[] isEnd = new boolean[N * 26];
StringBuilder sb = new StringBuilder();
void add(String s) {
int p = 0;
for (int i = 0; i < s.length(); i++) {
int u = s.charAt(i) - 'a';
if (tr[p][u] == 0) tr[p][u] = ++idx;
p = tr[p][u];
}
isEnd[p] = true;
}
boolean query(int start, int end) {
int p = 0;
for (int i = start; i <= end; i++) {
int u = sb.charAt(i) - 'a';
if (tr[p][u] == 0) return false;
p = tr[p][u];
}
return isEnd[p];
}
public StreamChecker(String[] words) {
for (int i = 0; i <= idx; i++) {
Arrays.fill(tr[i], 0);
isEnd[i] = false;
}
idx = 0;
for (String s : words) add(s);
}
public boolean query(char c) {
sb.append(c);
int n = sb.length(), min = Math.max(0, n - 200);
for (int i = n - 1; i >= min; i--) {
if (query(i, n - 1)) return true;
}
return false;
}
}
C++ 代码:
class StreamChecker {
public:
static const int N = 2010 * 200;
int tr[N][26];
bool isEnd[N];
string sb;
int idx = 0;
void add(const string &s) {
int p = 0;
for (char c : s) {
int u = c - 'a';
if (tr[p][u] == 0) tr[p][u] = ++idx;
p = tr[p][u];
}
isEnd[p] = true;
}
bool query(int start, int end) {
int p = 0;
for (int i = start; i <= end; i++) {
int u = sb[i] - 'a';
if (tr[p][u] == 0) return false;
p = tr[p][u];
}
return isEnd[p];
}
StreamChecker(const vector<string>& words) {
memset(tr, 0, sizeof(tr));
memset(isEnd, 0, sizeof(isEnd));
for (const string &s : words) add(s);
}
bool query(char c) {
sb.push_back(c);
int n = sb.length(), min = max(0, n - 200);
for (int i = n - 1; i >= min; i--) {
if (query(i, n - 1)) return true;
}
return false;
}
};
-
时间复杂度: StreamChecker
初始化复杂度为 ,其中 为words
字符总数;query
操作复杂度为 ,其中 为最大words[i]
长度 -
空间复杂度: ,其中 为 words
字符总数, 为字符集大小
Trie(优化)
初始化将所有的 存入 Trie
是必然的,我们只能考虑如何优化 query
操作。
在解法一中,我们需要对新数据流对应的字符串的每个后缀进行搜索,同时每次搜索是相互独立的,即本次匹配不会对下一次匹配产生贡献。
「实际上,我们可以通过「倒序建 Trie
」的方式,将「枚举检查多个后缀」的操作变为「匹配一次后缀」操作。」
具体的,我们可以在初始化 StreamChecker
时,将每个 翻转(倒序)加入 Trie
中;然后在 query
操作时(假设当前数据流对应的字符串为 s
,长度为 n
),从 s
的尾部开始在 Trie
中进行检索(即从 开始往回找)。
若在某个位置 idx
时匹配成功,意味着 的翻转子串在字典树中,同时我们又是将每个 words[i]
进行倒序插入,即意味着 的正向子串在 words
中,即满足 s
的某个后缀出现在 words
中。
同理,我们可以利用最大的 words[i]
长度为 200 来控制从 开始往回找的最远距离,同时利用当某个短后缀不在 Trie
中,则其余长度更大的后缀必然不在 Trie
中进行剪枝操作。
Java 代码:
class StreamChecker {
static int N = 2010 * 200, idx = 0;
static int[][] tr = new int[N][26];
static boolean[] isEnd = new boolean[N * 26];
StringBuilder sb = new StringBuilder();
void add(String s) {
int p = 0;
for (int i = s.length() - 1; i >= 0; i--) {
int u = s.charAt(i) - 'a';
if (tr[p][u] == 0) tr[p][u] = ++idx;
p = tr[p][u];
}
isEnd[p] = true;
}
public StreamChecker(String[] words) {
for (int i = 0; i <= idx; i++) {
Arrays.fill(tr[i], 0);
isEnd[i] = false;
}
idx = 0;
for (String s : words) add(s);
}
public boolean query(char c) {
sb.append(c);
int n = sb.length(), min = Math.max(0, n - 200), p = 0;
for (int i = n - 1; i >= min; i--) {
if (isEnd[p]) return true;
int u = sb.charAt(i) - 'a';
if (tr[p][u] == 0) return false;
p = tr[p][u];
}
return isEnd[p];
}
}
C++ 代码:
class StreamChecker {
public:
static const int N = 2010 * 200;
static const int ALPHABET_SIZE = 26;
vector<vector<int>> tr;
vector<bool> isEnd;
string sb;
int idx = 0;
void add(const string &s) {
int p = 0;
for (int i = s.length() - 1; i >= 0; i--) {
int u = s[i] - 'a';
if (tr[p].size() <= u) tr[p].resize(u + 1, 0);
if (tr[p][u] == 0) tr[p][u] = ++idx;
p = tr[p][u];
}
isEnd[p] = true;
}
StreamChecker(const vector<string>& words) {
tr.resize(N);
isEnd.resize(N);
fill(isEnd.begin(), isEnd.end(), false);
for (const auto &s : words) {
add(s);
}
}
bool query(char c) {
sb.push_back(c);
int n = sb.length(), min = max(0, n - 200), p = 0;
for (int i = n - 1; i >= min; i--) {
if (isEnd[p]) return true;
int u = sb[i] - 'a';
if (tr[p].size() <= u || tr[p][u] == 0) return false;
p = tr[p][u];
}
return isEnd[p];
}
};
-
时间复杂度: StreamChecker
初始化复杂度为 ,其中 为words
字符总数;query
操作复杂度为 ,其中 为最大words[i]
长度 -
空间复杂度: ,其中 为 words
字符总数, 为字符集大小
最后
巨划算的 LeetCode 会员优惠通道目前仍可用 ~
使用福利优惠通道 leetcode.cn/premium/?promoChannel=acoier,年度会员 有效期额外增加两个月,季度会员 有效期额外增加两周,更有超大额专属 🧧 和实物 🎁 福利每月发放。
我是宫水三叶,每天都会分享算法知识,并和大家聊聊近期的所见所闻。
欢迎关注,明天见。
更多更全更热门的「笔试/面试」相关资料可访问排版精美的 合集新基地 🎉🎉