双指针判断子序列

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

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

相关推荐
倒头就睡的小比特1 天前
算法竞赛C++常用的STL
c++·算法
小羊没烦恼!1 天前
初探性能优化——2个月到4小时的性能提升
java·开发语言·windows·算法·c#
猎头南楼1 天前
知识社区推荐系统实践:新用户冷启动与长短期兴趣建模的挑战 资深推荐算法工程师
人工智能·深度学习·算法·机器学习
旖旎夜光1 天前
力控面试题 01.01: 判定字符是否唯一(位运算) —— 题解
c++·学习·算法·leetcode·力控
wzdark1 天前
大规模并行计算中的负载均衡算法研究4
算法
Because_of_Her11 天前
并查集-听课笔记
笔记·算法·并查集
码流子1 天前
高速公路安全监测实践:碰撞监测预警+物联网底座,从感知到处置的闭环
大数据·人工智能·物联网·算法·架构
another heaven1 天前
【算法/C++ MD5算法能否逆解码?原理、C++实现与同类哈希算法对比】
c++·算法·哈希算法
wzdark1 天前
从算法设计模式看编程思维的抽象能力4
算法
2601_962218611 天前
万象生鲜系统称重自动多退少补算法解决生鲜非标品痛点
大数据·数据库·人工智能·python·算法