1. 题目解析与核心思路
LeetCode 228题"汇总区间"是一个典型的数组处理问题,要求我们将有序且无重复的整数数组转换为最小区间表示。这个问题看似简单,但考察了对数组遍历、边界条件处理以及字符串操作的综合能力。
1.1 题目要求详解
给定一个无重复元素的有序整数数组nums,我们需要返回一个区间列表,满足:
- 每个区间恰好覆盖数组中连续的数字
- 区间表示采用最小化原则
- 输出格式为:
- 单个数字:"a"(当a==b时)
- 区间范围:"a->b"(当a<b时)
例如:
- 输入[0,1,2,4,5,7] → 输出["0->2","4->5","7"]
- 输入[0,2,3,4,6,8,9] → 输出["0","2->4","6","8->9"]
1.2 解题思路拆解
解决这个问题的核心在于识别连续的数字序列。我们可以采用"滑动窗口"的思想:
- 初始化一个起点指针(start)指向当前区间的开始
- 遍历数组,检查当前元素是否与前一个元素连续(即nums[i] == nums[i-1]+1)
- 当遇到不连续的元素时:
- 将当前区间[start, i-1]转换为字符串格式
- 更新start指针到当前位置i
- 遍历结束后,处理最后一个区间
这种方法的优势在于只需一次遍历(O(n)时间复杂度),且空间复杂度仅为O(1)(不考虑输出结果的空间)。
2. 代码实现与详细解析
2.1 C++实现版本
cpp复制class Solution {
public:
vector<string> summaryRanges(vector<int>& nums) {
vector<string> result;
int n = nums.size();
if (n == 0) return result;
int start = 0;
for (int i = 1; i < n; ++i) {
if (nums[i] != nums[i-1] + 1) {
if (start == i-1) {
result.push_back(to_string(nums[start]));
} else {
result.push_back(to_string(nums[start]) + "->" + to_string(nums[i-1]));
}
start = i;
}
}
// 处理最后一个区间
if (start == n-1) {
result.push_back(to_string(nums[start]));
} else {
result.push_back(to_string(nums[start]) + "->" + to_string(nums[n-1]));
}
return result;
}
};
关键点解析:
- 使用start指针标记当前区间的起始位置
- 遍历时比较当前元素与前一个元素的连续性
- 当发现不连续时,根据区间长度决定输出格式
- 循环结束后必须单独处理最后一个区间
2.2 JavaScript实现版本
javascript复制var summaryRanges = function(nums) {
const res = [];
const n = nums.length;
if (n === 0) return res;
let start = 0;
for (let i = 1; i < n; i++) {
if (nums[i] !== nums[i-1] + 1) {
if (start === i-1) {
res.push(nums[start].toString());
} else {
res.push(`${nums[start]}->${nums[i-1]}`);
}
start = i;
}
}
// 处理最后一个区间
if (start === n-1) {
res.push(nums[start].toString());
} else {
res.push(`${nums[start]}->${nums[n-1]}`);
}
return res;
};
JavaScript实现注意事项:
- 使用严格相等运算符(===)进行比较
- 字符串拼接推荐使用模板字符串(``)
- 数组操作用push方法添加元素
- toString()方法需要加括号调用
2.3 常见错误分析
在实际编码中,容易犯的错误包括:
- 忘记处理空数组的特殊情况
- 循环结束后漏掉最后一个区间的处理
- 区间结束位置计算错误(应该是i-1而非i)
- JavaScript中使用单引号而非反引号包裹模板字符串
- 忘记调用toString()方法(直接使用函数引用)
3. 算法优化与扩展思考
3.1 代码重构与优化
我们可以将格式化区间的逻辑提取为独立函数,提高代码复用性:
cpp复制class Solution {
public:
vector<string> summaryRanges(vector<int>& nums) {
vector<string> result;
int n = nums.size();
if (n == 0) return result;
int start = 0;
for (int i = 1; i < n; ++i) {
if (nums[i] != nums[i-1] + 1) {
result.push_back(formatRange(nums[start], nums[i-1]));
start = i;
}
}
result.push_back(formatRange(nums[start], nums[n-1]));
return result;
}
private:
string formatRange(int a, int b) {
return a == b ? to_string(a) : to_string(a) + "->" + to_string(b);
}
};
这种重构使得:
- 主逻辑更加清晰
- 格式化逻辑可以单独修改
- 代码更易于维护和扩展
3.2 相似问题扩展
这个解题模式可以应用于多种区间相关问题:
- 合并区间(LeetCode 56)
- 插入区间(LeetCode 57)
- 区间列表的交集(LeetCode 986)
- 划分字母区间(LeetCode 763)
通用解题框架:
- 排序(如果输入无序)
- 初始化起始指针
- 遍历比较,确定区间边界
- 处理最后一个区间
3.3 复杂度分析
时间复杂度:O(n)
- 只需要一次遍历整个数组
- 每个元素只被访问一次
空间复杂度:O(1)(不考虑输出结果)
- 只使用了固定数量的额外空间(start指针等)
4. 实际应用与注意事项
4.1 实际应用场景
这种区间汇总算法在实际开发中有广泛应用:
- 日志分析:合并连续的时间戳
- 数据库查询:优化范围查询条件
- 版本控制:合并连续的版本号
- 日程管理:合并连续的时间段
4.2 调试技巧与测试用例
推荐测试用例:
| 输入 | 预期输出 | 测试目的 |
|---|---|---|
| [] | [] | 空数组处理 |
| [1] | ["1"] | 单元素数组 |
| [1,2,3,5,6,8] | ["1->3","5->6","8"] | 常规情况 |
| [0,2,3,4,6,8,9] | ["0","2->4","6","8->9"] | 混合单元素和区间 |
| [-2147483648,2147483647] | ["-2147483648","2147483647"] | 边界值测试 |
调试技巧:
- 在循环开始和结束时打印start和i的值
- 在添加每个区间前打印即将添加的内容
- 特别注意数组边界条件(第一个和最后一个元素)
4.3 性能优化思考
虽然这个算法已经是O(n)时间复杂度,但在某些情况下还可以优化:
- 对于极大数组,可以考虑并行处理(分段后合并)
- 如果数组特别大而区间很少,可以尝试跳跃式检查
- 在特定硬件环境下,可以考虑SIMD指令优化连续检查
5. 学习建议与总结
5.1 学习路径建议
- 先掌握基础的双指针技巧
- 理解滑动窗口的概念
- 练习类似的区间合并问题
- 尝试用不同语言实现同一算法
- 分析算法的时间/空间复杂度
5.2 常见问题解答
Q: 如何处理无序数组?
A: 需要先排序,但要注意题目通常会给有序数组
Q: 如果数组中存在重复元素怎么办?
A: 本题假设无重复,如果有重复需要先去重
Q: 如何扩展到二维区间?
A: 需要同时考虑两个维度的连续性,复杂度会提高
5.3 个人心得
在实际刷题过程中,我发现这类区间问题的关键在于:
- 明确区间的开始和结束条件
- 处理好边界情况(特别是第一个和最后一个元素)
- 保持代码简洁清晰,便于调试
- 提取通用逻辑作为辅助函数
建议初学者可以先用纸笔模拟算法运行过程,画出指针移动和区间变化,这样能更直观地理解算法逻辑。