共计 2662 个字符,预计需要花费 7 分钟才能阅读完成。
题目叙述
给你一个整数数组 nums ,返回全部为 0 的 子数组 数目。
子数组 是一个数组中一段连续非空元素组成的序列。
示例 1:
输入:nums = [1,3,0,0,2,0,0,4]
输出:6
解释:
子数组 [0] 出现了 4 次。子数组 [0,0] 出现了 2 次。不存在长度大于 2 的全 0 子数组,所以我们返回 6。
我的分析
就像集合一样,求子集的数量。
结果计算
注意 连续 0 的规律,比如 [0,0,0,0] 计算 0 子数组的数量 ? [0] [0,0] [0,0,0] [0,0,0,0]
- 累加 (数列求和) 定义 数量为 total, total = k*(k+1)/2 (1+2+3+4+5+6….)
- 利用对象等数据结构进行计数
方法一:滑动窗口(双指针)
我们建立一个只观察 0 的一个窗口,包含左右指针
首先移动左指针,当遇到 0 时,固定左指针为当前元素,然后移动右指针 直到 遇到不再是 0 的元素时
此时可以计算这个范围内 0 子数组的数量
详细实现如下
/**
* @param {number[]} nums
* @return {number}
*/
var zeroFilledSubarray = function (nums) {
let len = nums.length
let total = 0
let left = 0
// 终止条件
while (left < len) {
// 找到连续 0 的起始位置
if (nums[left] !== 0) {
left++
continue // 直接跳过当前循环 进入下一次循环
}
// 找到第一个 0 那么开始移动右指针
let right = left
while (right < len && nums[right] === 0) {right++}
const length = right - left
// 利用(累加)--> 数列求和
total += length * (length + 1) / 2
left = right
}
return total
};
这个方法的时间复杂度 O(n^2)
方法二 一次遍历 增量法
观察到连续 0 的规律 [0,0,0,0] ==> [0] [0,0] [0,0,0] [0,0,0,0] 在一个连续 0 块内可以这样计算
维护一个计数器,每当遇到 0 就自增 1,并且 total 加上这个计数器,每当遇到的元素不再是 0,计数器重置
具体实现如下
var zeroFilledSubarray = function (nums) {
// 分析连续 0 的规律 1+2+3+4
let total = 0
let countZero = 0
for(let num of nums) {if(num ===0) {
countZero++
total+=countZero
} else {countZero = 0}
}
return total
};
该方法的时间复杂度 O(n) 空间复杂度 O(1)
反思
在做这个题的时候,看到题目要求计算 0 子数组的数量,我首先想到使用滑动窗口(双指针)。
var zeroFilledSubarray = function (nums) {let count = {}
let res = 0
// 双指针
// 先移动左指针
for (let i = 0; i < nums.length; i++) {if (nums[i] !== 0) continue
// 遍历数组元素,遇到 0 就固定左端点然后移动右端点 并且计数器 +1
let x = 1; // 当前连续 0 的长度(以 i 为起点)// 初始化长度为 1 的子数组数量
count[x] = (count[x] || 0) + 1;
// 移动右指针
for (let j = i + 1; j < nums.length; j++) {if (nums[j] !== 0) break;
x++
count[x] = (count[x] || 0) + 1;
}
}
for (let i in count) {res += count[i]
}
return res;
};
这段代码 运行后会超出时间限制。需要进行好好分析,因为这是我初始的想法代表思维逻辑的结果
这段代码,看起来思路是没什么问题的,但是
一开始忘记对 let count = {} 在计数时进行初始化,count[x] ++ 由于未初始化,count[x] 为 undefined,再进行自增运算后结果变为 NaN 这直接导致数据错误,后来改成 count[x] = (count[x] || 0) + 1; 在一定程度上,无论如何 count[x] 都会有一个正确的值类型
其次,在内层循环,当右指针遇到非 0 元素时,没有进行跳出循环,导致计入了非 0 数据 导致数据无效,一定要记住,后续不符合条件的迭代 一定要及时退出 break;
for of 循环使用错误,我错误的 使用 for of 来对一个对象数据 count = {} 进行迭代,然而,for of 循环 只对可迭代的数据类型才可以使用。比如 连续的数组,而如果有稀松数组,也无法使用。由于上面代码在运行时会对某个下标计数,而会导致有些下标直接跳过,导致数组不是连续下标 这会产生稀松数组。
及时清理多余变量。应用程序运行时 只与内存有关,与硬盘无关,声明的每一个变量都会申请一部分内存,若未使用则产生浪费。
以上是对错误回答的总结
学到什么?
理解连续子数组的本质是什么
从暴力解法 到优化
最初可能会想到:枚举所有可能的子数组,检查是否全为 0,然后计数。
具体操作:用两层循环,外层固定子数组起点 i,内层遍历终点 j >= i,判断 nums[i..j] 是否全为 0。
但这种方法的时间复杂度是 O(n²)(n 为数组长度),当 n 很大时(比如 10⁵)会超时。
暴力解法的问题在于 重复检查,本质是计算一串连续 0 的组合数,使用嵌套的循环确实导致运行的复杂度很高
反思点: 抓住“连续区间”的特性,用数学公式替代枚举,是优化的关键 。
一次遍历解法的逻辑
- 作用:用
currentZeroLength实时记录当前连续 0 的长度,每遇到一个 0 就更新长度,并累加“新增的子数组数量”。 - 步骤:
① 遇到 0 时,currentZeroLength += 1,此时新增的子数组数量就是currentZeroLength(以当前 0 为结尾的所有子数组);
② 遇到非 0 时,重置currentZeroLength为 0。 - 反思点:这种方法更简洁,本质是将“区间计算”分散到每个元素的遍历中,用“增量累加”替代“区间一次性计算”,但核心还是依赖
k*(k+1)/2的规律(因为1+2+...+k = k*(k+1)/2)。
同类问题的迁移:总结“连续子数组”问题的共性
这个问题属于“连续子数组计数 / 最值”类问题,同类问题还包括:
- 求最大连续 1 的个数;
- 求子数组和为 k 的数目;
- 求最长连续递增子数组长度等。
这类问题的共性解法思路:
- 关注“连续区间”的特性,避免暴力枚举;
- 用指针(双指针、滑动窗口)或变量(记录当前区间状态)标记区间;
- 寻找区间长度与目标结果的数学关系(如求和公式、单调性等)。