一天一大 leet(判断子序列)难度:简单-Day20200727

时间:2022-07-25
本文章向大家介绍一天一大 leet(判断子序列)难度:简单-Day20200727,主要内容包括其使用实例、应用技巧、基本知识点总结和需要注意事项,具有一定的参考价值,需要的朋友可以参考一下。

题目:

给定字符串 s 和 t ,判断 s 是否为 t 的子序列。

你可以认为 s 和 t 中仅包含英文小写字母。字符串 t 可能会很长(长度 ~= 500,000),而 s 是个短字符串(长度 <=100)。

字符串的一个子序列是原始字符串删除一些(也可以不删除)字符而不改变剩余字符相对位置形成的新字符串。(例如,"ace"是"abcde"的一个子序列,而"aec"不是)。

示例:

  1. 示例 1
s = "abc", t = "ahbgdc"
返回 true.
  1. 示例 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
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 中是否包含满足该顺序字符
var isSubsequence = function (s, t) {
  return new RegExp(s.split('').join('[a-z]*')).test(t)
}