1. 题目
题目描述
给定字符串 s 和 t ,判断 s 是否为 t 的子序列。
字符串的一个子序列是原始字符串删除一些(也可以不删除)字符而不改变剩余字符相对位置形成的新字符串。(例如,"ace"是"abcde"的一个子序列,而"aec"不是)。
示例
输入:s = "abc", t = "ahbgdc"
输出:true
输入:s = "axc", t = "ahbgdc"
输出:false
约束
-
(0 <= s.length <= 100)
-
(0 <= t.length <= 10^4)
-
两个字符串都只由小写英文字母组成
进阶:如果有大量输入的 S,称作 S1, S2, ... , Sk 其中 k >= 10 亿,你需要依次检查它们是否为 T 的子序列。在这种情况下,你会怎样改变代码?
2. 最佳解题思路描述(双指针贪心,单次遍历最优)
-
定义指针
p指向s当前待匹配字符,初始为 0; -
遍历长字符串 t每一个字符:
-
若当前
t[i] == s[p],说明匹配成功,p向后移动一位; -
若
p已经走完s全部字符,可提前跳出循环;
-
-
遍历结束后,若
p == s.size()说明s全部匹配完成,返回 true;否则 false。
优势
-
时间复杂度 (O(len(t))),仅遍历一次 t;
-
空间复杂度 (O(1)),仅一个指针变量;
-
逻辑极简,贪心思想:匹配到字符立刻消耗 s 的一位,保证相对顺序。
3. 我的代码(逻辑完全正确,存在冗余变量、细节优化点)
class Solution {
public:
bool isSubsequence(string s, string t) {
bool flag = false;
int len = s.size();
int n = 0;
for(int i = 0;i<t.size();i++){
if(s[n] == t[i]){
n++;
}
}
if(n == len){
return true;
}
return false;
}
};
代码说明
-
正确性:无逻辑 bug,所有测试用例均可通过,是基础标准写法;
-
冗余 / 可优化点:
-
定义了
flag变量但全程未使用,无意义,直接删除; -
缺少提前终止:当
n == len时 s 已全部匹配,无需继续遍历 t 剩余字符; -
变量命名语义差:
n改为p/ptr更直观,len可直接使用s.size()无需单独存储; -
边界场景兼容:s 为空字符串时
s.size()=0,直接返回 true,代码天然支持。
-
4. 规范优化版代码(增加提前跳出,精简变量)
class Solution {
public:
bool isSubsequence(string s, string t) {
int ptr = 0;
int sLen = s.size();
for (char c : t) {
if (ptr == sLen) break; // 匹配完成直接退出
if (c == s[ptr]) ptr++;
}
return ptr == sLen;
}
};
5. 总结
-
你的双指针思路完全正确,能 AC;仅存在无用变量、无提前剪枝的小瑕疵;
-
核心判定条件:匹配计数等于 s 总长度;
-
空字符串 s 一定是任意 t 的子序列,代码天然兼容;
-
基础场景直接用一次遍历双指针,简洁高效。
6. 相关知识拓展
拓展 1:进阶海量 s 查询场景(预处理 t + 二分查找)
当 t 固定、有百万 / 十亿个 s 需要判断时,单次遍历 t 会超时;预处理 t 记录每个字母出现的下标数组,对 s 每个字符二分查找下一个匹配位置:
#include <vector>
#include <algorithm>
class Solution {
public:
bool isSubsequence(string s, string t) {
vector<vector<int>> pos(26);
// 预处理每个字母在t中出现的所有下标
for (int i = 0; i < t.size(); i++) {
pos[t[i] - 'a'].push_back(i);
}
int curIdx = -1;
for (char c : s) {
int idx = c - 'a';
// 二分找大于curIdx的最小下标
auto& vec = pos[idx];
auto it = upper_bound(vec.begin(), vec.end(), curIdx);
if (it == vec.end()) return false;
curIdx = *it;
}
return true;
}
};
预处理(O(len(t))),单次查询(O(len(s) \log len(t))),适合多次查询。
拓展 2:递归写法(不推荐,存在栈开销)
bool isSubsequence(string s, string t) {
if (s.empty()) return true;
if (t.empty()) return false;
if (s[0] == t[0])
return isSubsequence(s.substr(1), t.substr(1));
else
return isSubsequence(s, t.substr(1));
}
拓展 3:复杂度对比
-
基础双指针(优化后):单次查询 (O(len(t))),(O(1)) 空间;适合少量查询;
-
预处理二分法:预处理(O(len(t))),单次(O(len(s)\log len(t))),(O(26 \times len(t)))空间;适合海量 s 查询。
拓展 4:易错点复盘
-
忘记提前终止循环,长 t 会做多余遍历;
-
新增无使用的冗余变量,代码可读性下降;
-
混淆子序列与子串:子串要求连续,子序列只要求相对顺序不变。