程序员老鬼

在大厂工作10年左右被裁,现在接近45岁了,找工作真的太难,外包倒是有机会,40W左右,但后面怕是找不到正式的了。

刚看到个贴子,说一位在大厂干了十年的人被裁,如今年近45,只能考虑年薪40W的外包,还在纠结:要不要干半年,再把这段经历当成gap隐掉。

Image

我觉得这事吧,先别纠结“体不体面”,先保住现金流。这个年龄段,最大的风险不是履历有个外包,而是长期没收入、状态掉光。工作经历就像简历上的一串数字,比起“好不好看”,“能不能证明你还在场”更重要。

换个角度想,外包也可以是过渡期:一边维持收入,一边用业余时间补技能、人脉和副业,为下一步做准备。说到底,40+职场,面子值不了几个钱,能养家、能保持竞争力,才是真本事。

面试题:匹配子序列的单词数

昨晚十一点多我还在公司楼下吹风,手机一刷又刷,刷到这道题我整个人都清醒了,名字挺绕口——“匹配子序列的单词数”,但意思其实特别生活化,你可以想象成:一长串弹幕在屏幕上刷过去,你手里有一堆关键词,看这些关键词里有多少个,是可以按顺序从弹幕里“抠”出来的。

题目大概长这样: 给你一个长字符串 s,再给你一个字符串数组 words,问你:words 里面有多少个单词,是 s 的子序列。 子序列这个词别被吓到,就是“只保证顺序不乱,可以中间跳着选”。 比如:s = "abcde"words = ["a","bb","acd","ace"]能匹配的是 "a", "acd", "ace",一共 3 个。

我先说下很多人第一反应的写法哈,就那种“我干脆一个个比”的暴力思路:

  • 对 words 里的每个单词 w,从头扫一遍 s
  • 双指针:i 指向 s,j 指向 w,遇到相同的字符就 j++
  • 最后 j == w.length() 就说明 w 是子序列

伪代码脑补一下:

booleanisSub(String s, String w){
int i = 0, j = 0;
while (i < s.length() && j < w.length()) {
if (s.charAt(i) == w.charAt(j)) {
            j++;
        }
        i++;
    }
return j == w.length();
}

然后外面再套一层循环,对每个 w 调用一下。逻辑特别好懂,对吧?

问题也很明显:如果 s 很长、words 也很多,这玩意儿复杂度就成了O(|s| * |words|),上题库直接超时报警,你人都麻了。

我后来想了个更“懒”的办法:既然每个单词都是在 s 上“顺着往后走”,那我能不能让所有单词一起在 s 上排队走?s 只扫一遍,顺路把所有单词都处理了,这样不就舒服多了。

核心小想法是: “按下一个要匹配的字符把单词分桶装起来”。

你可以脑补成有 26 个桶(a ~ z),每个桶里塞的是一堆“等待这个字符”的单词状态。 我这里不用直接放字符串,而是放一个小对象:它要记录:这个单词是谁,现在已经匹配到哪个位置了。

比如有单词 "ace",刚开始它还没匹配任何东西,就丢到 'a' 这个桶里,表示:“我下一个要 a”。

然后我们开始从左到右扫 s:

  1. 当前字符是 c,那就把 c 这个桶里所有“等 c”的单词拿出来处理一遍

  2. 每拿出一个单词状态,都把它的 index++,表示我们匹配到了一个字符,往前走一步

    • 如果刚好走到单词末尾,说明这个单词整串匹配完了,答案 +1
    • 否则,看它下一个想要的字符是什么,再丢到对应的桶里继续排队

整个过程 s 只扫一遍,每个单词的每个字符,也只被处理一次,就很丝滑。

直接上 Java 代码,按题库那种写法写了一份,你可以直接拿去交:

classSolution{

// 小节点,记录“这是哪个单词,现在匹配到哪里了”
staticclassNode{
        String word;
int index;

        Node(String word, int index) {
this.word = word;
this.index = index;
        }
    }

publicintnumMatchingSubseq(String s, String[] words){
// 26 个桶,每个桶里是一个链表,放的是等这个字母的 Node
@SuppressWarnings("unchecked")
        List<Node>[] buckets = new ArrayList[26];
for (int i = 0; i < 26; i++) {
            buckets[i] = new ArrayList<>();
        }

// 初始化:把每个单词按“第一个字符”丢进对应桶
for (String w : words) {
if (w == null || w.length() == 0) {
// 空串也算子序列的话,这里可以视情况 +1
continue;
            }
char first = w.charAt(0);
            buckets[first - 'a'].add(new Node(w, 0));
        }

int ans = 0;

// 扫一遍 s
for (char ch : s.toCharArray()) {
int idx = ch - 'a';
            List<Node> waitList = buckets[idx];
if (waitList.isEmpty()) {
// 现在没人等这个字符,跳过
continue;
            }

// 关键点:当前桶里的这批,都是“此刻就能吃掉 ch 的”
// 这一轮处理完要把当前桶清空,再把没匹配完的重新分配去其他桶
            buckets[idx] = new ArrayList<>();  // 换个新的空桶
            List<Node> current = waitList;

for (Node node : current) {
                node.index++; // 吃掉一个字符

if (node.index == node.word.length()) {
// 整个单词匹配完了
                    ans++;
                } else {
// 还没完,看下一个想要啥
char next = node.word.charAt(node.index);
                    buckets[next - 'a'].add(node);
                }
            }
        }

return ans;
    }
}

这个写法里,有几个点你注意一下:

  • buckets 这个数组相当于 26 个“等待队列”
  • 扫到字符 ch 的那一刻,只处理当前桶里的那些 Node,别把之后新加进去的也处理了 所以我先把 buckets[idx] 换成新的 ArrayList,老的列表丢到局部变量 current 里慢慢处理
  • 每个单词的每个字符,最多被移动一次,所以整体复杂度差不多就是O(|s| + 所有单词总长度),比暴力那种 |s| * |words| 爽太多

你可以自己手动跑一个例子,比如:

  • s = "abcde"
  • words = ["a", "bb", "acd", "ace"]

脑子里模拟一下桶的变化,多模拟几轮就彻底懂了,背都不用背。

这题本质就是:让所有单词在一条大串里“排队前进”,用桶把“下一步要的字符”管理起来,s 从左扫到右,顺手把所有人都推进度条。 写顺手之后,这种“按下一步需求分桶”的套路,在别的子序列类题目里也能复用,挺香的。

行了我去泡杯茶压压咖啡,还得回去改 bug,有啥不懂你直接把你写的代码贴出来咱一起看。

-END-

我为大家打造了一份RPA教程,完全免费:songshuhezi.com/rpa.html

最后给大家分享一份不错的副业资料,点击下方公众号,回复关键字: 副业 领,也可以链接我微信:hls404