题目:
给定字符串 s 和 t ,判断 s 是否为 t 的子序列。
你可以认为 s 和 t 中仅包含英文小写字母。字符串 t 可能会很长(长度 ~= 500,000),而 s 是个短字符串(长度 <=100)。
字符串的一个子序列是原始字符串删除一些(也可以不删除)字符而不改变剩余字符相对位置形成的新字符串。
(例如,"ace"是"abcde"的一个子序列,而"aec"不是)。
示例:
- 示例 1
s = "abc", t = "ahbgdc"
返回 true.
- 示例 2
s = "axc", t = "ahbgdc"
返回 false.
后续挑战 :
如果有大量输入的 S,称作 S1, S2, … , Sk 其中 k >= 10 亿,你需要依次检查它们是否为 T 的子序列。在这种情况下,你会怎样改变代码?
抛砖引玉
思路
- 遍历 s,按索引取出 s 中的单个字符
- 在 t 中查询这个字符的位置,然后删除这个字符及其之前的字符
- 如果删除后 s 未遍历的字符比 t 上则不满足
- 如果变量完成都匹配则返回 true
/**
* @param {string} s
* @param {string} t
* @return {boolean}
*/
var isSubsequence = function (s, t) {
let slen = s.length,
tlen = t.length
if (slen > tlen) return false
for (let i = 0; i < slen; i++) {
let index = t.indexOf(s[i])
if (index === -1) return false
t = t.substring(index + 1)
if (slen - i - 1 > t.length) return false
}
return true
}
- 上面每取出一个字符都需要在 t 中 indexof 查询,
- indexOf 的边界是通过 substring 截取字符串完成,
换种思路不具体操作字符串 s,而是通过索引来限制字符串查找范围
- index 默认从 0 开始
- s[i],不等于 t[index],则接着查询 index+1 位置,其中 index 小于 t.length,i>=index
- s[i]无论匹配结果如果,i 向后移动式查找范围缩小 index+1
- 如果 index === t.length 则说明 s===t,
- 如果 index>t.length 则 index 再查询时有字符未匹配到超出限制范围
/**
* @param {string} s
* @param {string} t
* @return {boolean}
*/
var isSubsequence = function (s, t) {
let index = 0
for (let i = 0; i < s.length; i++) {
while (index < t.length && s[i] !== t[index]) {
index++
}
index++
}
return index <= t.length
}
双指针
- 上面是通过一个索引限制 t 的查询范围
- 更直观些,可以声明两个变量 s->i,t->j,分别表示两个字符串指针
- 匹配成功 i 递增,匹配下一个字符
- 当前位未匹配 j 递增,继续尝试匹配
- 边界:
- i 小于 s.length
- j 小于 t.length
- 触发边界条件终止时,t 变量完则说明 t 中字符全匹配了,不然返回 false
/**
* @param {string} s
* @param {string} t
* @return {boolean}
*/
var isSubsequence = function (s, t) {
let slen = s.length,
tlen = t.length,
i = 0,
j = 0
while (i < slen && j < tlen) {
if (s.charAt(i) == t.charAt(j)) {
i++
}
j++
}
return i == slen
}
动态规划
- 设 t 长 tlen
- 声明一个 tlen*26 的矩阵 dp
- 矩阵中记录 t 中每个字符第一次出现所在的坐标点
- | a | h | b | g | d | c | - |
---|---|---|---|---|---|---|---|
0 | 0 | 6 | … | … | … | … | 6 |
1 | 2 | 2 | 2 | 6 | … | … | 6 |
3 | 4 | 4 | 4 | 4 | 4 | … | 6 |
4 | 5 | 5 | 5 | 5 | 5 | 5 | 6 |
5 | … | … | … | … | … | … | 6 |
6 | 3 | 3 | 3 | 3 | 6 | … | 6 |
7 | 1 | 1 | 6 | … | 6 | ||
… | … | … | … | … | … | … | 6 |
26 | … | … | … | … | … | … | 6 |
- 在生成 s 的矩阵时,因为无法预期第一次出现 t[i]的位置,则倒序查询默认填充 tlen(表示不存在):
- dp[i][j],在 a-z 中,等于的字符,则将 t 中索引存放到 dp[i][j]中
- dp[i][j],不等于的字符,则该位置不是 t[i]出现位置,其值沿用本行已计算的值 dp[i+1][j]
- 遍历 s,每一个字符对应 dp 一行
- 如果该行存放的位置为边界则说明匹配,t 中未查询到该字符
- 位置未越界则,继续查询后一个 s 中的字符,s 的其实位置+1
/**
* @param {string} s
* @param {string} t
* @return {boolean}
*/
var isSubsequence = function (s, t) {
let slen = s.length,
tlen = t.length,
dp = Array.from({ length: tlen + 1 }, () => Array(26))
// 填充边界值
for (let i = 0; i < 26; i++) {
dp[tlen][i] = tlen
}
for (let i = tlen - 1; i >= 0; i--) {
// 生成dp记录每个字符第一次出现位置
for (let j = 0; j < 26; j++) {
if (t.charAt(i) === String.fromCharCode(j + 97)) {
dp[i][j] = i
} else {
dp[i][j] = dp[i + 1][j]
}
}
}
// 查询字符s[i],的t字符起始位置
let index = 0
for (let i = 0; i < slen; i++) {
// 遇到边界说明未匹配到
if (dp[index][s.charAt(i).charCodeAt() - 97] === tlen) {
return false
}
// 满足条件更新t起始位置
index = dp[index][s.charAt(i).charCodeAt() - 97] + 1
}
return true
}
正则
- s = “abc”
- t = “ahbgdc”
- 转换成正则表达式:a[a-Z]*b[a-Z]*c[a-Z]*
- 用时去匹配 t,查询 t 中是否包含满足该顺序字符
/**
* @param {string} s
* @param {string} t
* @return {boolean}
*/
var isSubsequence = function (s, t) {
return new RegExp(s.split('').join('[a-z]*')).test(t)
}
博客: 小书童博客
每天的每日一题,写的题解会同步更新到公众号一天一大 lee 栏目
欢迎关注留言
公号: 坑人的小书童