为了能让博客更灵动、更有趣,特在此设立了一个广告位~

算法题-全0子数组的数目

373次阅读
没有评论

共计 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]

  1. 累加 (数列求和) 定义 数量为 total, total = k*(k+1)/2 (1+2+3+4+5+6….)
  2. 利用对象等数据结构进行计数

方法一:滑动窗口(双指针)

我们建立一个只观察 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 的数目;
  • 求最长连续递增子数组长度等。

这类问题的共性解法思路:

  1. 关注“连续区间”的特性,避免暴力枚举;
  2. 用指针(双指针、滑动窗口)或变量(记录当前区间状态)标记区间;
  3. 寻找区间长度与目标结果的数学关系(如求和公式、单调性等)。
正文完
文章二维码
post-qrcode
 0
xyblog
版权声明:本站原创文章,由 xyblog 于2025-08-19发表,共计2662字。
转载说明:除特殊说明外本站文章皆由CC-4.0协议发布,转载请注明出处。
评论(没有评论)
这是全站底部广告位