力扣【2348. 全0子数组的数目】——从暴力到最优的思考过程

前端算法实战:用JS解决力扣【2348. 全0子数组的数目】------从暴力到最优的思考过程

引言

今天我们来聊一道在前端面试中可能让你眼前一亮的力扣题目------2348. 全0子数组的数目。这道题看似简单,实则蕴含着动态规划和数学归纳的巧妙结合,掌握它不仅能帮你应对面试,更能让你对数组问题有更深层次的理解。今天,我将带大家从前端视角出发,一步步拆解这道题,从最直观的暴力解法,到最终的数学优化,让你彻底掌握这类问题的解题精髓。

题目分析

  • 题目大意

    给定一个整数数组 nums,我们需要找出其中所有由连续的 0 组成的子数组的数目。子数组必须是非空的。

  • 输入输出

    • 输入 :一个整数数组 nums
    • 输出 :一个整数,表示全部为 0 的子数组的数目。
  • 约束条件

    • 1 <= nums.length <= 10^5
    • -10^9 <= nums[i] <= 10^9
  • 示例演示

    示例 1: 输入: nums = [1,3,0,0,2,0,0,4] 输出: 6 解释:

    • 子数组 [0] 出现了 4 次。
    • 子数组 [0,0] 出现了 2 次。
    • 不存在长度大于 2 的全 0 子数组,所以我们返回 6

    示例 2: 输入: nums = [0,0,0,2,0,0] 输出: 9 解释:

    • 子数组 [0] 出现了 5 次。
    • 子数组 [0,0] 出现了 3 次。
    • 子数组 [0,0,0] 出现了 1 次。
    • 不存在长度大于 3 的全 0 子数组,所以我们返回 9

    示例 3: 输入: nums = [2,10,2019] 输出: 0 解释: 没有全 0 子数组,所以我们返回 0

    分步计算示例2: 对于 nums = [0,0,0,2,0,0]

    • 连续的 0 序列有两段:[0,0,0][0,0]

    • 对于 [0,0,0] 这段:

      • 长度为 1 的 [0] 子数组有 3 个。
      • 长度为 2 的 [0,0] 子数组有 2 个。
      • 长度为 3 的 [0,0,0] 子数组有 1 个。
      • 这一段总共有 3 + 2 + 1 = 6 个全 0 子数组。
    • 对于 [0,0] 这段:

      • 长度为 1 的 [0] 子数组有 2 个。
      • 长度为 2 的 [0,0] 子数组有 1 个。
      • 这一段总共有 2 + 1 = 3 个全 0 子数组。
    • 总计:6 + 3 = 9 个全 0 子数组。

思路推导

这道题的核心在于如何高效地统计连续的 0 序列所能形成的全 0 子数组的数目。我们来一步步推导。

笨方法尝试:暴力枚举(不可行)

最直观的想法是暴力枚举所有的子数组,然后判断每个子数组是否全部由 0 组成。如果一个数组的长度为 n,那么它的子数组数量大约是 n^2 个。对于每个子数组,我们还需要遍历一遍来判断是否全为 0,这又是一个 O(n) 的操作。因此,总的时间复杂度将达到 O(n^3)。考虑到题目中 n 的最大值是 10^510^5 的三次方是 10^15,这在力扣上是绝对会超时的。所以,暴力枚举是不可行的。

优化方向:减少重复计算

既然暴力枚举不行,我们就需要寻找更高效的方法。仔细观察题目,我们发现全 0 子数组的形成,只与连续的 0 有关。当遇到非 0 元素时,连续的 0 序列就会中断。这提示我们可以将问题分解为处理一段段连续的 0

最优思路选择:数学归纳法

假设我们发现了一段连续的 k0。这段 0 可以形成多少个全 0 子数组呢?

  • 长度为 1 的全 0 子数组有 k 个(每个 0 自身)。
  • 长度为 2 的全 0 子数组有 k-1 个(例如 [0,0])。
  • 长度为 3 的全 0 子数组有 k-2 个(例如 [0,0,0])。
  • ...
  • 长度为 k 的全 0 子数组有 1 个(整个 k0 组成的子数组)。

将这些数目加起来,就是一个等差数列的和:1 + 2 + ... + k = k * (k + 1) / 2

所以,我们的思路就清晰了:

  1. 遍历数组 nums
  2. 维护一个计数器 count,记录当前连续 0 的个数。
  3. 当遇到 0 时,count 加 1。
  4. 当遇到非 0 元素时,或者遍历到数组末尾时,说明一段连续的 0 结束了。此时,我们将 count * (count + 1) / 2 加到总结果中,并将 count 重置为 0

思路具象化:小例子辅助推演

我们以 nums = [0,0,0,2,0,0] 为例进行推演:

  • 初始化 total_subarrays = 0count = 0

  • 遍历 nums[0] = 0count 变为 1。

  • 遍历 nums[1] = 0count 变为 2。

  • 遍历 nums[2] = 0count 变为 3。

  • 遍历 nums[3] = 2(非 0):

    • 连续 0 序列结束,count = 3
    • total_subarrays += 3 * (3 + 1) / 2 = 3 * 4 / 2 = 6
    • count 重置为 0。
  • 遍历 nums[4] = 0count 变为 1。

  • 遍历 nums[5] = 0count 变为 2。

  • 遍历结束(数组末尾):

    • 连续 0 序列结束,count = 2
    • total_subarrays += 2 * (2 + 1) / 2 = 2 * 3 / 2 = 3
    • count 重置为 0。
  • 最终 total_subarrays = 6 + 3 = 9

这与示例 2 的结果完全一致。这种方法只需要一次遍历,时间复杂度为 O(n),空间复杂度为 O(1),非常高效。

代码实现

根据上述思路,我们可以用 JavaScript 实现如下代码。为了符合前端最佳实践,我们使用 let/const 和箭头函数,并附上逐行注释。

ini 复制代码
/**
 * @param {number[]} nums
 * @return {number}
 */
var zeroFilledSubarray = function(nums) {
    let totalSubarrays = 0; // 用于存储最终的全0子数组总数
    let currentZeroCount = 0; // 用于记录当前连续0的个数
​
    // 遍历整个数组
    for (let i = 0; i < nums.length; i++) {
        if (nums[i] === 0) {
            // 如果当前元素是0,连续0的计数器加1
            currentZeroCount++;
        } else {
            // 如果当前元素不是0,说明连续0的序列中断了
            // 根据数学归纳法,将当前连续0的个数所能形成的全0子数组数量累加到总数中
            // 公式:k * (k + 1) / 2
            totalSubarrays += (currentZeroCount * (currentZeroCount + 1)) / 2;
            // 重置连续0的计数器
            currentZeroCount = 0;
        }
    }
​
    // 循环结束后,如果数组是以0结尾,或者整个数组都是0,需要将最后一段连续0的子数组数量累加
    totalSubarrays += (currentZeroCount * (currentZeroCount + 1)) / 2;
​
    return totalSubarrays;
};

代码说明:

  • 初始化totalSubarrays 用于累加所有全 0 子数组的数量,currentZeroCount 用于统计当前连续 0 的个数,两者都初始化为 0

  • 遍历逻辑 :我们遍历 nums 数组的每一个元素。

    • 如果 nums[i]0,说明连续 0 的序列还在继续,currentZeroCount 简单地加 1
    • 如果 nums[i] 不是 0,这意味着当前的连续 0 序列已经结束了。此时,我们利用前面推导出的公式 k * (k + 1) / 2(其中 k 就是 currentZeroCount)计算出这段连续 0 所能形成的全 0 子数组数量,并将其累加到 totalSubarrays 中。然后,将 currentZeroCount 重置为 0,为下一段连续 0 的统计做准备。
  • 边界处理 :循环结束后,需要特别注意。如果数组的最后一个元素是 0,或者整个数组都是 0,那么在循环结束时,currentZeroCount 可能不为 0。这意味着还有一段连续的 0 序列没有被计算。因此,在循环结束后,我们还需要再执行一次累加操作,确保所有连续 0 序列都被正确计算。

优化提升

原代码分析:

  • 时间复杂度 :我们只对数组进行了一次遍历,每次操作都是常数时间。因此,时间复杂度为 O(n),其中 n 是数组 nums 的长度。这对于 n 达到 10^5 的情况来说,是非常高效的。
  • 空间复杂度 :我们只使用了 totalSubarrayscurrentZeroCount 两个变量来存储中间结果,没有使用额外的数组或复杂的数据结构。因此,空间复杂度为 O(1)。这也是最优的空间复杂度。

优化点:

这道题的解法已经非常高效,在时间和空间上都达到了最优。如果题目要求「返回全 0 子数组的具体内容」(而不是数量),那么我们需要在遍历过程中记录每个连续 0 序列的起始和结束索引,并根据公式生成对应的子数组。但这会增加空间复杂度,因为需要存储这些子数组。

拓展:

这类问题可以有很多变种,例如:

  • 如果要求「全 X 子数组的数目」怎么办? 思路是完全一样的,只需要将判断条件 nums[i] === 0 改为 nums[i] === X 即可。
  • 如果要求「全 0 子数组的最大长度」怎么办? 只需要在遍历过程中,维护一个最大连续 0 的长度即可。
  • 如果数组是二维的(比如「最大全 0 子矩阵」) :这类问题通常会复杂很多,可能需要将二维问题转化为一维问题来解决,或者使用更复杂的动态规划。

面试总结

考点提炼:

这道题的核心考点在于对连续子数组问题 的理解和数学归纳法的应用。面试官可能希望看到你:

  1. 问题分解能力 :能否将复杂问题分解为处理独立的连续 0 序列。
  2. 数学思维 :能否发现连续 k0 形成 k * (k + 1) / 2 个全 0 子数组的规律。
  3. 代码实现能力:能否将思路清晰地转化为高效、简洁的 JavaScript 代码。
  4. 边界条件处理 :能否考虑到数组末尾的连续 0 序列的计算。

技巧总结:

  • 遇到涉及连续子数组 的问题,尤其是需要统计或计算其属性时,可以优先考虑滑动窗口动态规划数学归纳法 。本题通过数学归纳法将问题简化为对连续 0 序列的计数,避免了复杂的动态规划状态转移。
  • 在面试中,如果能清晰地阐述从暴力解法到最优解法的思考过程 ,并用小例子进行推演,会给面试官留下深刻印象。

类似题目:

  • 力扣 413. 等差数列划分:这道题也是统计连续子数组(等差数列)的数目,思路与本题有异曲同工之妙,都可以通过数学归纳法来解决。
  • 力扣 670. 最大交换:虽然题目类型不同,但都涉及到对数字序列的分析和操作,可以锻炼对数组和数字的敏感度。

结尾互动

你们在做这道题的时候有没有遇到什么坑?比如在处理连续 0 序列的边界条件时?或者有没有想到其他更巧妙的解法?欢迎在评论区告诉我!

如果想练习更多类似的算法题目,或者对前端算法面试有任何疑问,欢迎关注我,后续会更新更多前端算法实战内容,助你轻松通关算法面试!

相关推荐
mmmmath_34 小时前
LeetCode.541.反转字符串II
数据结构·算法·leetcode
Navigator_Z4 小时前
LeetCode //MySQL - 1251. Average Selling Price
c语言·算法·leetcode
Captaincc5 小时前
AI用量v0.1.11更新发布 新增 jusage doctor 诊断指令 托盘展示token 和余额 新增 AutoClaw 支持
前端·后端·vibecoding
醇氧5 小时前
MySQL 8.0 系统表损坏与引擎转换故障排查实战
数据结构·算法
大熊背6 小时前
《Color constancy by characterization of illumination chromaticity》之色度色域最大化算法(二)
算法·白平衡·色度·色温
钓鱼的肝6 小时前
梳理(1-5)
c++·经验分享·笔记·算法·青少年编程
HZZD_HZZD6 小时前
CSDN_批发市场水电漏损归因算法LAM的原理与落地
嵌入式硬件·物联网·算法
计算机魔术师6 小时前
德国Wiki被黑后两周,OpenAI终于把模型失控的账本摊开了
前端
kyriewen7 小时前
我让 AI 当面试官面了我一轮:第 3 个追问我就卡住了(附 10 道追问清单)
前端·面试·ai编程