392. 判断子序列-Easy
给定字符串 s 和 t ,判断 s 是否为 t 的子序列。
字符串的一个子序列是原始字符串删除一些(也可以不删除)字符而不改变剩余字符相对位置形成的新字符串。(例如,"ace"是"abcde"的一个子序列,而"aec"不是)。
进阶:
如果有大量输入的 S,称作 S1, S2, … , Sk 其中 k >= 10亿,你需要依次检查它们是否为 T 的子序列。在这种情况下,你会怎样改变代码?
题解1: 执行用时击败100% (indexOf() 用法复习)
1 | /** |
题解2:简洁,通用
1 | var isSubsequence = function(s, t) { |
进阶:针对海量输入的 S 检查是否为 T 的子序列的优化方案
当需要处理 10亿级 的字符串 S1, S2, ..., Sk 并判断它们是否为 T 的子序列时,直接对每个 S 使用双指针法(O(n) 时间复杂度)会导致总时间复杂度达到 **O(k·n)**,这在 k=10^9 时显然不可行。以下是分阶段的优化策略:
1. 预处理阶段:对 T 建立索引
目标
将 T 转换为一种支持 快速查询字符位置 的数据结构,避免每次处理 S 时都重新扫描 T。
方法:建立字符位置映射表
- 使用哈希表记录
T中每个字符的 所有出现位置(按顺序存储)。 - 例如
T = "ahbgdc"的映射表:1
2
3
4
5
6
7
8{
'a': [0],
'h': [1],
'b': [2],
'g': [3],
'd': [4],
'c': [5]
}
代码实现
1 | function buildIndex(T) { |
复杂度分析
- 时间复杂度:O(n)(仅需一次)
- 空间复杂度:O(n)
2. 查询阶段:对每个 S 进行高效匹配
目标
利用预处理好的 indexT,快速判断 S 是否为 T 的子序列。
方法:二分查找下一个匹配位置
- 初始化
currentPos = -1(表示从T的开头开始匹配)。 - 对于
S中的每个字符c:- 如果
c不在indexT中,直接返回false。 - 在
indexT[c]中 二分查找 第一个大于currentPos的位置。 - 如果找到,更新
currentPos;否则返回false。
- 如果
复杂度分析(单次查询)
- 时间复杂度:O(m log L)
m是S的长度,L是字符在T中的平均出现次数。- 例如
T="aaaabbbb",L=4。
- 空间复杂度:O(1)
实现示例(完整代码)
1 | // 预处理阶段 |
3. 总复杂度对比
| 方法 | 预处理时间 | 单次查询时间 | 总时间(k=10^9) |
|---|---|---|---|
| 原始双指针法 | 无 | O(n) | O(10^9 · n) → 不可行 |
| 预处理+二分查找 | O(n) | O(m log L) | O(n + 10^9 · m log L) |
4. 进一步优化(针对特定场景)
(1) 如果 T 很短(如 n < 1000)
- 直接使用双指针法,避免预处理开销。
(2) 如果 S 的平均长度远小于 T(如 m << n)
- 预处理
T的收益可能不明显,需测试权衡。
(3) 并行化处理
并行化处理海量字符串检查
1. 预处理阶段(单机)
- 构建字符位置索引:对
T建立哈希表,记录每个字符的所有出现位置(有序数组)。
2. 查询阶段(分布式并行)
方案一:多线程(Node.js Worker Threads)
1 | const { Worker, isMainThread, parentPort, workerData } = require('worker_threads'); |
方案二:MapReduce(大规模分布式, 离线计算)/ Spark(离线/实时计算/流处理/图计算/机器学习…)
- Map 阶段:
- 将海量
S分片到多台机器。 - 每台机器读取
T的索引(可共享存储如 Redis)。
- 将海量
- Reduce 阶段:
- 汇总各机器的结果。
3. 优化点
- 索引共享:将
T的索引存储在分布式缓存(如 Redis)中,避免重复构建。 - 动态负载均衡:根据机器性能动态分配
S的分片大小。
三、复杂度对比
| 方法 | 预处理时间 | 单次查询时间 | 总时间(k=10^9, p=1000台机器) |
|---|---|---|---|
| 单机双指针 | 无 | O(n) | O(10^9 · n) → 不可行 |
| 预处理+并行查询 | O(n) | O(m log L) | O(n + (10^9 · m log L)/p) |
四、关键问题解答
并行化如何保证正确性?
- 只读操作:各线程/机器独立处理不同的
S,无需共享可变状态。 - 索引一致性:预处理后的
T索引是只读的,可安全共享。
如何处理超长 T 或 S?
- 压缩索引:对
T的字符位置使用差值编码(如存储相邻位置的差值)。 - 分批加载:将
S分批次处理,避免内存溢出。
***总结***
- 单机优化:预处理
T为字符位置索引,将单次查询时间从 O(n) 降至 O(m log L)。 - 并行化:通过多线程或分布式框架(如 MapReduce)横向扩展,处理海量
S。 - 适用场景:
T较长且稳定,S数量极大(如日志分析、DNA序列匹配)。
6. 边界情况处理
| 情况 | 处理方式 |
|---|---|
T 为空字符串 |
所有 S 若非空则返回 false |
S 为空字符串 |
直接返回 true |
T 或 S 含 Unicode 字符 |
需确保哈希表支持 Unicode |
总结
- **预处理
T**:建立字符位置索引表(O(n) 时间)。 - 高效查询:对每个
S使用二分查找(O(m log L) 时间)。 - 适用场景:
k极大(如 10^9)、T较长且S长度适中。 - 权衡点:若
T极短或S极长,可能需要调整策略。