LeetCode 剑指 Offer 57 - II. 和为s的连续正数序列
题目描述

题意分析
给定正整数
target,找出所有和等于它的连续正整数序列。每个序列至少包含两个数,数值逐个加一,不能跳过中间的数,也不能包含零或负数。要返回每个序列的完整内容,并按序列起点从小到大排列。只返回方案数量不够;将
target自身作为一个单元素序列也不符合要求。连续性意味着只要确定左右端点,这个序列及其和就完全确定。
解法:滑动窗口
核心思路
[!blue]
用闭区间
[left, right]表示当前连续序列,用sum保存其中所有整数的和。初始选择最小的两个正整数,令left = 1、right = 2、sum = 3。窗口至少有两个数时,才可能成为答案。由于所有数都是正数,右端加入一个新数会使和增大,左端移走一个旧数会使和减小。若当前和小于目标,保持左端再缩短右侧只会更小,必须向右扩张。若当前和大于目标,保持左端再扩张只会更大,当前左端已经没有可行机会,应移除左端并继续找。
右端无需回退:此前因为和不足而放弃的更短右端,在左端向右移动后,窗口和只会进一步减小,不可能重新成为答案。
相等时先记录当前序列,再收缩左端。对于同一个左端,右端继续增大后总和会严格增加,不会再次等于目标,因此记录后直接寻找更大的左端不会漏解。两个端点都只向右移动,每个候选区间最多记录一次,答案的起点顺序也自然递增。
每次移动还要同步维护
sum:收缩时先减掉旧的left,再增加左端;扩张时先增加right,再加上新进入的数。这样sum始终精确对应当前闭区间。当窗口缩到只剩一个数时即可结束。缩小前最后一个双元素窗口的和已经不小于目标,而后面从更大起点开始、至少包含两个数的窗口只会更大,不可能再产生答案。
left < right因此既排除了单元素结果,也给出了搜索终点。
解题步骤
- 初始化
left = 1、right = 2、sum = 3,结果列表为空。- 在
left < right时比较当前窗口和与目标。- 若和恰好相等,创建长度为
right - left + 1的结果数组,依次写入左右端点之间的整数。- 若
sum >= target,先减去旧左端,再令left++;命中答案后也走这一分支。- 若
sum < target,先令right++,再把新右端加到窗口和中。- 循环结束后返回已按起点递增排列的全部序列。
代码实现
class Solution {
public int[][] findContinuousSequence(int target) {
List<int[]> result = new ArrayList<>();
int left = 1;
int right = 2;
int sum = 3;
while (left < right) {
if (sum == target) {
int[] sequence = new int[right - left + 1];
for (int i = 0; i < sequence.length; i++) {
sequence[i] = left + i;
}
result.add(sequence);
}
if (sum >= target) {
// 先减去旧左端,再移动窗口,命中答案后也必须继续收缩。
sum -= left;
left++;
} else {
right++;
// 右端已先移动,这里加入的是新进入窗口的数。
sum += right;
}
}
return result.toArray(new int[0][]);
}
}
func findContinuousSequence(target int) [][]int {
result := make([][]int, 0)
left, right, sum := 1, 2, 3
for left < right {
if sum == target {
sequence := make([]int, right-left+1)
for i := range sequence {
sequence[i] = left + i
}
result = append(result, sequence)
}
if sum >= target {
// 先减去旧左端,再移动窗口,命中答案后也必须继续收缩。
sum -= left
left++
} else {
right++
// 右端已先移动,这里加入的是新进入窗口的数。
sum += right
}
}
return result
}
复杂度分析
- 时间复杂度:
O(target + S),其中S为所有返回序列的元素总数。两个端点都只向右移动,搜索次数为O(target);输出完整序列还需要逐个写入S个元素。- 空间复杂度:结果占
O(S)。搜索本身只保存常数个变量;若返回r个序列,Java 在转换结果数组前还保留O(r + 1)个临时列表引用,Go 不计返回结果时为O(1)。
关键点总结
[!green]
- 正数保证两种指针移动对窗口和的影响方向确定,才能根据大小关系排除不可能的区间。
- 窗口采用闭区间,和的增减与端点更新必须对应同一批元素。
- 命中后继续收缩即可找全答案,起点递增使结果无需额外排序。
- 返回的是具体序列,复杂度中需要计入生成所有结果元素的开销。
易错点总结
[!yellow]
- 命中后只记录不移动:下一轮会停留在同一个窗口,导致重复记录或死循环。
- 先移动左端再减它的值:减掉的会是新左端而非移出的旧元素,使
sum与窗口脱节。- 扩张时先加旧右端:新进入窗口的应是
right + 1,先移动后累加才与实现一致。- 允许
left == right作为结果:题目至少要求两个数,单独一个目标值不合法。- 只把端点存入答案:需要输出整个连续序列,而不是返回一个二元区间。
- 把这一移动规则直接用于含负数的数组:加入或删除负数会破坏和的单调变化,本题的正确性依赖所有数为正。
相似题目
| 题目 | 难度 | 关联与区别 |
|---|---|---|
| 829. 连续整数求和 | 困难 | 原题允许单项target并只统计数量,本题每段至少两个数且输出具体序列。 |
| 209. 长度最小的子数组 | 中等 | 正整数让窗口和随两端移动单调,本题找全部精确和区间,原题找达到下界的最短长度。 |
转载与许可
许可
本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议,转载请注明出处!