LeetCode 100天挑战 Day 3:判断子序列与汇总区间
目录
前言
在算法学习的过程中,字符串处理和数组操作是两个非常重要的领域。今天的两道题目分别代表了这两个领域的经典问题:判断子序列问题考察我们对字符串匹配的理解,而汇总区间问题则训练我们对有序数组的处理能力。这两个问题虽然难度不同,但都蕴含着重要的算法思想。
今日题目统计信息
| 题目 | 难度 | 知识点 | 通过率 | 核心思想 |
|---|---|---|---|---|
| 判断子序列 | 简单 | 双指针、字符串匹配 | 52.1% | 贪心算法 |
| 汇总区间 | 中等 | 数组遍历、区间合并 | 58.3% | 一次遍历 |
题目一:判断子序列问题
题目描述
给定字符串 s 和 t,判断 s 是否为 t 的子序列。
字符串的一个子序列是原始字符串删除一些(也可以不删除)字符而不改变剩余字符相对位置形成的新字符串。例如,"ace"是"abcde"的一个子序列,而"aec"不是。
进阶挑战: 如果有大量输入的 S,称作 S1, S2, ..., Sk 其中 k >= 10亿,你需要依次检查它们是否为 T 的子序列。在这种情况下,你会怎样改变代码?
示例 1:
输入:s = "abc", t = "ahbgdc"
输出:true
示例 2:
输入:s = "axc", t = "ahbgdc"
输出:false
解题思路
这个问题的核心思想是贪心算法 结合双指针技巧。我们需要在t中按顺序寻找s的每个字符:
-
基础思路:
- 使用两个指针,一个指向s的当前位置,一个指向t的当前位置
- 在t中顺序查找s的当前字符
- 找到后,s的指针向前移动,t的指针从找到位置的下一个位置继续查找
- 如果s的所有字符都能在t中按顺序找到,则s是t的子序列
-
优化考虑:
- 如果s的长度大于t的长度,直接返回false
- 如果s为空字符串,直接返回true
- 如果s和t长度相等且内容相同,返回true
代码实现分析
java
class Solution {
public boolean isSubsequence(String s, String t) {
int len1 = s.length();
int len2 = t.length();
// 边界情况处理
if(len1 == 0){
return true;
}
if(len2 == 0){
return false;
}
if(len1 == len2){
return s == t;
}
// 主逻辑:双指针查找
int index = 0; // t中的查找起始位置
for(int i = 0; i < len1; i++){
if(index >= len2) {
return false; // t已经遍历完但s还有字符未匹配
}
// 从index开始在t中查找s.charAt(i)
for(int j = index; j < len2; j++){
if(s.charAt(i) == t.charAt(j)){
index = j + 1; // 更新下次查找的起始位置
break;
}
// 如果遍历到最后一个字符都没找到匹配
if(j == len2 - 1){
return false;
}
}
}
return true;
}
}
代码逐行解析:
- 长度获取与边界处理:
java
int len1 = s.length();
int len2 = t.length();
if(len1 == 0){
return true; // 空字符串是任何字符串的子序列
}
if(len2 == 0){
return false; // 非空字符串不是空字符串的子序列
}
if(len1 == len2){
return s == t; // 长度相同时只有完全相等才是子序列
}
- 核心查找逻辑:
java
int index = 0; // 记录在t中查找的起始位置
for(int i = 0; i < len1; i++){
// 遍历s的每个字符
for(int j = index; j < len2; j++){
// 在t中从index位置开始查找
if(s.charAt(i) == t.charAt(j)){
index = j + 1; // 找到后更新下次查找位置
break;
}
}
}
复杂度分析
| 指标 | 分析过程 | 结果 |
|---|---|---|
| 时间复杂度 | 最坏情况下需要遍历整个t字符串 | O(m×n),其中m=len(s), n=len(t) |
| 空间复杂度 | 只使用了常数个额外变量 | O(1) |
注意:虽然时间复杂度看起来是O(m×n),但在实际运行中,由于t的指针不会回退,实际时间复杂度更接近O(m+n)。
进阶优化:批量处理方案
针对进阶挑战中提到的大量输入情况,我们需要对t进行预处理:
java
class Solution {
// 预处理t,构建字符位置映射
private Map<Character, List<Integer>> preprocess(String t) {
Map<Character, List<Integer>> charPositions = new HashMap<>();
for (int i = 0; i < t.length(); i++) {
char c = t.charAt(i);
charPositions.computeIfAbsent(c, k -> new ArrayList<>()).add(i);
}
return charPositions;
}
// 使用二分查找优化子序列检查
public boolean isSubsequenceOptimized(String s, String t) {
Map<Character, List<Integer>> charPositions = preprocess(t);
int currentIndex = -1;
for (char c : s.toCharArray()) {
if (!charPositions.containsKey(c)) {
return false;
}
List<Integer> positions = charPositions.get(c);
// 找到第一个大于currentIndex的位置
int foundIndex = Collections.binarySearch(positions, currentIndex + 1);
if (foundIndex < 0) {
foundIndex = -foundIndex - 1;
}
if (foundIndex == positions.size()) {
return false;
}
currentIndex = positions.get(foundIndex);
}
return true;
}
}
优化方案分析:
| 方法 | 预处理时间 | 单次查询时间 | 适用场景 |
|---|---|---|---|
| 原始方法 | O(1) | O(m+n) | 单次查询 |
| 优化方法 | O(n) | O(m×logk) | 大量查询 |
题目二:汇总区间问题
题目描述
给定一个无重复元素的有序整数数组nums。
区间[a,b]是从a到b(包含)的所有整数的集合。
返回恰好覆盖数组中所有数字的最小有序区间范围列表。也就是说,nums的每个元素都恰好被某个区间范围所覆盖,并且不存在属于某个区间但不属于nums的数字x。
输出格式要求:
- "a->b",如果a != b
- "a",如果a == b
示例 1:
输入:nums = [0,1,2,4,5,7]
输出:["0->2","4->5","7"]
示例 2:
输入:nums = [0,2,3,4,6,8,9]
输出:["0","2->4","6","8->9"]
解题思路
这个问题可以通过一次遍历解决,核心思想是识别连续的数字序列:
-
主要思路:
- 遍历数组,记录当前区间的起始位置
- 检查当前数字与下一个数字是否连续
- 如果不连续,结束当前区间并开始新的区间
- 处理最后一个区间的特殊情况
-
关键判断条件:
nums[i] + 1 != nums[i + 1]:当前数字与下一个数字不连续i + 1 == nums.length:已到达数组末尾
代码实现分析
java
class Solution {
public List<String> summaryRanges(int[] nums) {
List<String> result = new ArrayList<>();
// 边界情况:空数组
if (nums.length == 0) {
return result;
}
int start = 0; // 当前区间的起始位置
for (int i = 0; i < nums.length; i++) {
// 判断是否需要结束当前区间
if (i + 1 == nums.length || nums[i] + 1 != nums[i + 1]) {
if (start == i) {
// 单个数字的区间
result.add(String.valueOf(nums[start]));
} else {
// 多个数字的连续区间
result.add(nums[start] + "->" + nums[i]);
}
start = i + 1; // 开始新区间
}
}
return result;
}
}
代码详细解析:
- 初始化与边界处理:
java
List<String> result = new ArrayList<>();
if (nums.length == 0) {
return result; // 空数组直接返回空列表
}
- 核心遍历逻辑:
java
int start = 0; // 记录当前区间的起始索引
for (int i = 0; i < nums.length; i++) {
// 检查区间结束条件
if (i + 1 == nums.length || nums[i] + 1 != nums[i + 1]) {
// 处理区间结束
}
}
- 区间格式化逻辑:
java
if (start == i) {
// 单个元素区间
result.add(String.valueOf(nums[start]));
} else {
// 多个元素的连续区间
result.add(nums[start] + "->" + nums[i]);
}
start = i + 1; // 更新下一个区间的起始位置
复杂度分析
| 指标 | 分析 | 结果 |
|---|---|---|
| 时间复杂度 | 只需要一次遍历 | O(n) |
| 空间复杂度 | 存储结果列表,最坏情况n个区间 | O(n) |
边界情况处理
让我们分析各种可能的输入情况:
边界情况详细分析
| 输入情况 | 处理方式 | 预期输出 |
|---|---|---|
空数组 [] |
直接返回空列表 | [] |
单元素 [5] |
单个元素的区间 | ["5"] |
全连续 [1,2,3,4] |
整个数组作为一个区间 | ["1->4"] |
全不连续 [1,3,5,7] |
每个元素单独成区间 | ["1","3","5","7"] |
负数 [-3,-2,-1,0] |
包含负数的连续区间 | ["-3->0"] |
算法思想对比与总结
共同特点分析
虽然这两道题目看似不同,但它们都体现了几个重要的算法思想:
-
一次遍历原则:
- 子序列问题:顺序查找,不回退
- 区间汇总问题:线性扫描,动态分组
-
边界条件处理:
- 都需要仔细处理空输入等特殊情况
- 都涉及循环结束条件的精确判断
-
状态维护:
- 子序列问题:维护当前查找位置
- 区间汇总问题:维护当前区间起始位置
解题技巧总结
| 技巧 | 子序列问题应用 | 区间汇总问题应用 |
|---|---|---|
| 双指针 | ✅ 在t中顺序查找s的字符 | ✅ start和i形成区间边界 |
| 贪心算法 | ✅ 每次找到最早匹配的位置 | ✅ 尽可能扩展连续区间 |
| 状态机 | ✅ 维护查找状态 | ✅ 维护区间开始/结束状态 |
| 边界处理 | ✅ 多种特殊情况处理 | ✅ 数组边界和连续性判断 |
实际应用场景
判断子序列的应用
-
文本搜索与匹配:
- 搜索引擎的关键词匹配
- 文本编辑器中的模式查找
- 生物信息学中的DNA序列匹配
-
数据验证:
- 输入格式验证
- 协议解析中的字段顺序检查
- 版本兼容性检查
汇总区间的应用
-
数据压缩:
- 连续数字范围的压缩存储
- 日志文件的压缩记录
- 网络地址范围表示
-
数据分析:
- 时间序列数据的区间分析
- 传感器数据的连续区间识别
- 金融数据的连续涨跌区间统计
性能优化建议
java
// 子序列问题性能测试
public void performanceTest() {
String t = "a".repeat(1000000) + "b".repeat(1000000);
String s = "ab";
long startTime = System.currentTimeMillis();
boolean result = isSubsequence(s, t);
long endTime = System.currentTimeMillis();
System.out.println("结果: " + result + ", 耗时: " + (endTime - startTime) + "ms");
}
// 区间汇总问题扩展测试
public void rangeSummaryExtended() {
// 测试大数据量
int[] largeArray = new int[1000000];
for (int i = 0; i < largeArray.length; i++) {
largeArray[i] = i * 2; // 创建间隔为2的序列
}
List<String> result = summaryRanges(largeArray);
System.out.println("结果数量: " + result.size());
}
总结与思考
通过今天对这两个算法题的深入分析,我们可以得到以下几点重要启示:
算法思维的提升
-
问题抽象能力:
- 将具体问题抽象为算法模型
- 识别问题的核心本质
- 选择合适的算法策略
-
代码实现技巧:
- 边界条件的重要性
- 状态维护的方法
- 代码可读性与效率的平衡
-
优化思考方式:
- 从暴力解法到最优解的思考路径
- 时间复杂度与空间复杂度的权衡
- 实际应用场景对算法选择的影响
学习建议
-
循序渐进:
- 先理解基本思路
- 再考虑优化方案
- 最后思考扩展应用
-
举一反三:
- 总结通用的解题模式
- 建立知识间的联系
- 培养算法直觉
-
实践验证:
- 编写代码验证思路
- 测试各种边界情况
- 分析性能瓶颈
编程感悟:算法学习不仅是掌握解题技巧,更重要的是培养分析问题、解决问题的思维能力。每一道题目都是一个思维训练的机会,通过深入思考和优化,我们能够不断提升自己的编程水平。
参考资源
如果你觉得这篇文章对你有所帮助,欢迎点赞、收藏和关注!如有疑问或不同见解,欢迎在评论区交流讨论。
标签: #LeetCode #算法 #Java #字符串 #数组 #双指针 #贪心算法 #数据结构