程序员老鬼

我烟草,我老婆电网,一年加起来大概50个,看病啥的基本不花钱~

烟草配电网,这配置一甩出来,评论区都安静了两秒。俩人一年加起来50个,医药费基本不用操心,假期还稳定,顺手再搞点小投资,这日子放职场里,真算“闷声过舒服”的那种。

Image

有网友一看就酸了,说这已经不是双职工,这是“双系统叠buff”。

这俩行业单拎一个都够家里稳了,凑一块儿,HR看了都得默默关掉招聘软件。

我倒觉得,大家不是眼红50个,主要是眼红那种确定感。很多人现在工资未必差太多,怕就怕今天还行,明天项目黄了,后天公司开始谈优化。

说白了,班还是那个班,可人家下班是真下班,旅游也是真旅游。

面试题:最小窗口子序列

这题一上来,很多人会往“最小覆盖子串”上靠,结果写着写着就歪了。

最小窗口子序列麻烦的地方就在这儿:它不是要求字符出现次数够,而是要求 t 必须按顺序出现在 s 的子序列里。顺序一变,双指针那套熟练手法就不太够用了,硬怼 usually 会把自己绕进去。

我第一次看这种题,第一反应就不是先抠优化,而是先把“怎么找到一个合法窗口”这件事写稳。这个题其实很适合用两段式扫描:先正向找一个能覆盖 t 的右边界,再反向收缩左边界。这套路不花,但好使,代码也不拧巴,比较像线上真会交的写法。整体判断和落笔方式,会更偏实战一点,而不是那种模板味很重的题解。

先说过程。

给你两个字符串 s 和 t,在 s 里找一个最短连续子串,使得这个子串里能按顺序挑出 t。注意,是连续窗口里包含一个不连续但有序的子序列。

比如:

s = "abcdebdde"
t = "bde"

答案是 "bcde"。

因为在 "bcde" 里,可以按顺序拿到 b -> d -> e。后面的 "bdde" 也是合法的,但长度一样时一般取更靠左的。

这题我一般这么写。

先用指针 i 扫 s,再用指针 j 匹配 t。如果一路往前能把 t 全部匹配完,说明找到了一个合法窗口的右边界。别急着记结果,这时候再反着走一遍,把多余字符剔掉,尽量把左边界往右收。这样就能拿到一个以当前右边界结尾的最短窗口。

代码如下:

publicclassMinWindowSubsequence{

public String minWindow(String s, String t){
int m = s.length();
int n = t.length();

int bestStart = -1;
int bestLen = Integer.MAX_VALUE;

int i = 0;
while (i < m) {
int j = 0;
int k = i;

// 第一步:从 i 开始,正向找到一个能覆盖 t 的右边界
while (k < m && j < n) {
if (s.charAt(k) == t.charAt(j)) {
                    j++;
                }
                k++;
            }

// 没匹配完整,后面也不用看了
if (j < n) {
break;
            }

// 第二步:反向收缩,找到最左边界
int end = k - 1;
            j = n - 1;
            k = end;

while (j >= 0) {
if (s.charAt(k) == t.charAt(j)) {
                    j--;
                }
                k--;
            }

int start = k + 1;
int len = end - start + 1;

if (len < bestLen) {
                bestLen = len;
                bestStart = start;
            }

// 下次从更靠后一点的位置继续找
            i = start + 1;
        }

return bestStart == -1 ? "" : s.substring(bestStart, bestStart + bestLen);
    }

publicstaticvoidmain(String[] args){
        MinWindowSubsequence solver = new MinWindowSubsequence();
        System.out.println(solver.minWindow("abcdebdde", "bde")); // bcde
        System.out.println(solver.minWindow("fgrqsqsnodwmxzkzxwqegkndaa", "fnok")); // fgrqsqsnodwmxzk
    }
}

这段代码的关键,不在“扫描两次”这件事,而在两个边界的意义要拎清。

正向扫描时,你拿到的是“第一个可行的右边界”。 反向扫描时,你收出来的是“在这个右边界下最紧的左边界”。

这就避免了很多人常犯的错:窗口明明已经合法了,还在那儿乱缩,最后把顺序缩没了。

复杂度上,这个写法最坏情况下不是最优,接近 O(m * n) 到 O(m * m) 之间,看字符串分布。但面试里大多数场景是够用的,尤其是你先把正确性打稳,比上来就写一坨 DP 更重要。

当然,这题也能用 DP 做。比如 dp[i][j] 表示 s 前 i 个字符里,匹配到 t 前 j 个字符时,对应窗口的起点。那套更规整,但代码会更重,状态定义一旦没想顺,很容易把自己写晕。真在面试现场,时间一紧,我反而更信这种前扫后缩的写法,短,直,出错点少。

这题说到底,坑就一个:子串是连续的,子序列是有序但可以不连续的。这两个词一混,十分钟就没了。

把这个点想明白,代码其实不难。难的是别套错题。