看完“土猪拱白菜“的张锡峰,我明白计算机有多难了

计算机有多难?

今天无意中,看到一篇关于「"土猪拱白菜"学霸后悔报考浙大计算机」的文章。

或许会有不少和我刚开始一样懵圈的同学:张锡峰是谁?"土猪拱白菜"又是什么梗?

带着疑惑,我打开了简中网著名的搜索引擎,找到这位曾经少年的大三在读生的故事。

百度百科
百度百科

张锡峰曾就读于衡水中学,在 2021 年,作为高三学生的他参加综艺节目《超级演说家》,因演讲内容「我就是一只来自乡下的"土猪",也要立志,去拱了大城市里的白菜」而出圈。

当时这句话被病毒式传播。

这场演讲也被普遍解读为:不屈不挠、勇于奋斗。尽管来自小地方,但有着进入大城市、实现自己梦想的决心。

有流量的地方就有对立,除了得到了大多数人的共鸣认可以外,也有不少质疑张锡峰的声音。

面对这些声音,张锡峰除了强势回应「你以为我们每天天不亮就奔向操场,一边奔跑一边呼喊是为了什么?是假装吗?是作秀吗?我们是为了改命啊!」以外,「还以 674 分的高考成绩考入浙大计算机」

如果故事只是到这里结束,那很好,一个眼里有光的少年,面对质疑打破质疑,成就自我。

但时间很快,张锡峰现在是一名大三学生,也要考虑实习找工作的事儿,也要面临这日趋严峻的就业环境。

遗憾的是,重现在网友面前的张锡峰,从「阳光、活力、干翻一切」变成「平静、冷漠、眼里没光」。

alt
alt

这让我想起来之前在公众号的网友留言,原话我不太记得了,大致意思是:当时报计算机专业,真觉得挺好,薪资也高,但随着找不到工作,以及参与工作之后的 996,也分不清楚自己是不是真的喜欢这行了。

这些话题看多了,聊多了,我开始理解,有些兴趣爱好甚至是热爱,之所以还在,可能真就被生活放了一马罢了。

让我感到绝望的是,代表希望的种子被击落之后,不少人开始反过来攻击种子本身。

最近网上出现了另一种质疑声音:会不会这个张锡峰就是个普通的"小镇做题家",只是那场演讲把他捧到了天上。

但只要继续深究就不难发现,早在那场演讲之前,张锡峰就以「励志、干翻一切」小有名气。

2019 年,他在衡水中学的演讲《这世间,唯有青春与梦想不可辜负》在 B 站就有 2300W+ 的播放量。

alt

所以,是他的励志使他站上了《超级演说家》的舞台,而不是刚好被综艺的风吹起。

校园世界和现实世界,是两个世界,这没错。

但不代表我们不需要张锡峰这样的孩子,不需要这种向往。

张锡峰只是在他那个年纪做了大多数人都想做的事情,他或许失败了,但不该被嘲讽,不该成为乐子。

就像哥谭不该只有蝙蝠侠一样。

...

回归主线。

来一道和「华为」相关的题目。

题目描述

平台: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, falsefalsefalsetruefalsetruefalsefalsefalsefalsefalsetrue]

解释:
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] == 0return 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] == 0return false;
            p = tr[p][u];
        }
        return isEnd[p];
    }
    StreamChecker(const vector<string>& words) {
        memset(tr, 0sizeof(tr));
        memset(isEnd, 0sizeof(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] == 0return 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 + 10);
            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] == 0return false;
            p = tr[p][u];
        }
        return isEnd[p];
    }
};
  • 时间复杂度: StreamChecker 初始化复杂度为 ,其中 words 字符总数; query 操作复杂度为 ,其中 为最大 words[i] 长度
  • 空间复杂度: ,其中 words 字符总数, 为字符集大小

最后

巨划算的 LeetCode 会员优惠通道目前仍可用 ~

使用福利优惠通道 leetcode.cn/premium/?promoChannel=acoier,年度会员 有效期额外增加两个月,季度会员 有效期额外增加两周,更有超大额专属 🧧 和实物 🎁 福利每月发放。

我是宫水三叶,每天都会分享算法知识,并和大家聊聊近期的所见所闻

欢迎关注,明天见。

更多更全更热门的「笔试/面试」相关资料可访问排版精美的 合集新基地 🎉🎉

  • 16
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值