双指针判断子序列

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

先别急着看代码,我们先搞清楚它要回答的那个问题。假设你手里有两串字符,短的那串叫 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)。这种"时间线性、空间常数"的解法,正是双指针这类技巧的价值所在------用最朴素、最省内存的方式,高效地解决了一类按顺序匹配的问题。

相关推荐
毕竟是shy哥2 小时前
计算YOLO数据集中每个类的目标数
算法·yolo·机器学习
M78佐菲2 小时前
Linux学习笔记:TCP协议
linux·笔记·学习·tcp/ip·算法
晊晌_h4 小时前
嵌入式从0到精通——数据结构总结[特殊字符]
数据结构·算法·排序算法
我找到地球的支点啦4 小时前
Matlab系列(009) 一CRC循环冗余校验详解
开发语言·数据结构·算法·matlab·信息与通信
罗西的思考5 小时前
【OpenClaw具身硬件】ZeroClaw 源码阅读笔记(3)--- RAG
人工智能·算法·机器学习
浪里镖客5 小时前
位姿转换矩阵写法-个人习惯(计算机理解其实是相反的)
线性代数·算法·矩阵
小白羊丨8 小时前
如何诊断 Prompt 模板导致的效果下降?
人工智能·算法·prompt
OPEN-F9 小时前
C++11/14新特性精讲:移动语义与智能指针实战
开发语言·c++·算法
lisin-lee-cooper10 小时前
【leetcode658】有序数组找出k个最接近x的数
java·数据结构·算法
sunburn-10 小时前
Java堆(Heap)详解与实战教学
java·开发语言·数据结构·ide·算法