双指针判断子序列

一、这段代码在解决什么问题

先别急着看代码,我们先搞清楚它要回答的那个问题。假设你手里有两串字符,短的那串叫 s,长的那串叫 t。现在要你回答:s 能不能不改变字符顺序、只是"跳过" t 里的一些字符,从而"拼"出来?

用个生活中的例子:s = "abc"t = "ahbgdc"。我们在 t 里找 a,找到了;接着往后找 b,也找到了;再往后找 c,又找到了。所以答案是"能",st 的子序列。但如果 s = "axc",在 t 里找到 a 之后往后找 x,翻遍了 t 也找不到 x,答案就是"不能"。

注意"子序列"和"子串"是两回事:子串要求字符在 t 里是连续挨着 的;子序列只要求字符按顺序出现 ,中间可以隔着别的字符。"abc""ahbgdc" 的子序列,但它并不是 "ahbgdc" 的子串,因为 ab 之间隔了个 h

二、核心思想:两个"指针"像两只手一样扫过去

代码里的两个变量 ij,就是所谓的"双指针"。你可以把它们想象成两只手,各自指着一个位置:

  • i 这只手,指向短串 s 的某个字符,代表"我现在要找的下一个字符是谁"。
  • j 这只手,指向长串 t 的某个字符,代表"我现在正在 t 里检查到哪了"。

整个过程就一句话:jt 里一路向右扫,凡是碰到的字符恰好是 s[i] 需要的,就把 i 也往右挪一格,表示"这个字符找到了,继续找下一个"。 扫完整个 t 之后,如果 i 已经走到了 s 的末尾,说明 s 的每个字符都在 t 里按顺序找到了。

为什么这个"碰到就匹配、匹配了就前进"的思路一定对?关键在于一个直觉:对于 s 里的每一个字符,我们当然希望它在 t 里出现得越靠前越好。因为越靠前,留给后面字符的"余量"就越大。这正是一种"贪心"策略------每一步都做当下看起来最有利的选择,最终就能得到全局正确的结果。后面我们会用一个例子验证这个直觉。

三、逐行拆解:这 7 行代码到底在干嘛

javascript 复制代码
let i = 0, j = 0;

初始化两只手,都从各自字符串的**开头(下标 0)**出发。下标是编程里"第几个字符"的编号方式,从 0 开始数。

javascript 复制代码
while(i < s.length && j < t.length){

循环继续的条件有两个,必须同时 满足:i 还没走出 s 的末尾,j 也还没走出 t 的末尾。只要有一只手走到了头,循环就该停了------要么 s 已经被全部匹配完(成功),要么 t 被扫完了还没找到(失败)。

javascript 复制代码
    if(s[i] === t[j]){
        i++;
    }

这是循环里唯一的判断:如果当前 t[j] 这个字符,正好等于我们正在寻找的 s[i],说明"找到了一个",于是 i++,让 i 指向 s 的下一个字符,准备找下一个。

javascript 复制代码
    j++;
}

这一行无论是否匹配都会执行j 的手永远向右移动,表示我们在 t 里一个字符一个字符地往下扫。也就是说:i 是"条件性地前进"(只有匹配时才动),j 是"无条件地前进"(每个回合都动)。这正是双指针的精髓------一个跑,一个在需要时跟。

javascript 复制代码
return i === s.length;

最后,判断 i 是否已经走到了 s 的末尾(s.length 就是 s 的长度)。如果 i 等于 s.length,意味着 s 的每一个字符都被成功匹配过,返回 true;否则说明还差几个字符没找到,返回 false

四、跑一个例子,看代码怎么一步步动

s = "abc"t = "ahbgdc" 来模拟一下:

回合 i j s[i] t[j] 结果
初始 0 0 a a 相等,i 变 1
1 1 1 b h 不等,i 不动
2 1 2 b b 相等,i 变 2
3 2 3 c g 不等
4 2 4 c d 不等
5 2 5 c c 相等,i 变 3
结束 3 --- --- --- j 走到头,循环退出

循环结束时 i = 3,而 s.length = 3,所以 i === s.length 成立,返回 true。整个过程 j 从 0 一路扫到 5,而 i 只在三个匹配点上各前进一次------完美对应了"找到 a、b、c"这三个动作。

五、为什么"贪心"在这里是对的

前面我们提到,让每个字符都匹配到 t 里"最早出现的位置"是最优的。直观地想:假设 s 里的字符 xt 里出现了两次,一次在第 3 位、一次在第 10 位。如果我们贪心地匹配了第 3 位那个,那么第 4 位往后的字符就可以在第 3 位之后随便找;而如果我们"故意"跳过第 3 位、去匹配第 10 位那个,反而白白缩小了后面字符的搜索空间,没有任何好处。所以越早匹配越安全,贪心策略不会错过任何可能。

这里也有一个重要的边界情况值得一提:如果 s 是空字符串s.length === 0),那么它一定是任何字符串的子序列。代码里 i 初始为 0,而空串的 s.length 也是 0,循环条件一开始 i < s.length 就是假的,循环根本不执行,最后 return i === s.length 直接返回 0 === 0,即 true。逻辑自洽,不需要额外特判。

六、效率有多高

衡量算法快慢,通常看它的时间复杂度 ------也就是"最坏情况下要操作多少次"。这段代码里,ij 都只会单调递增、永不回头,两个 while 循环合起来,j 最多扫过整个 ti 最多扫过整个 s。所以总操作次数大约是 s.length + t.length 这个量级。如果两个字符串长度分别是 nm,时间复杂度就是 O(n + m)

更重要的是,它只用了两个变量 ij,不管字符串有多长,占用的额外空间都恒定不变 ,空间复杂度是 O(1)。这种"时间线性、空间常数"的解法,正是双指针这类技巧的价值所在------用最朴素、最省内存的方式,高效地解决了一类按顺序匹配的问题。

相关推荐
贵慜_Derek1 小时前
vLLM-07|MegaMoE 与 FusedMoE:路由相同,算 expert 完全不同
人工智能·算法·llm
机器学习之心1 小时前
基于改进鲸鱼优化算法的CNN-BiLSTM-MATT短期电力负荷预测模型
人工智能·算法·cnn·cnn-bilstm-matt·短期电力负荷预测
windliang1 小时前
Claude Code 源码分析(十):MCP 外部工具如何进入下一轮 Agent 调用
前端·算法·面试
代码地平线2 小时前
C++类与对象(中):默认成员函数与日期类实战
c++·笔记·算法
hansang_IR2 小时前
【题解】可持久化区间仿射区间和(Persistent Range Affine Range Sum)
c++·算法·线段树
XWalnut2 小时前
LeetCode刷题 day37
java·数据结构·算法·leetcode
Smilecoc2 小时前
方向导数与梯度
算法
瑞码空间2 小时前
01背包:动态规划的Hello World
c++·算法·0/1背包问题
LCG元2 小时前
STM32F103 ADC 多通道 DMA 采集与滤波算法对比:从滑动平均到卡尔曼滤波
stm32·嵌入式硬件·算法