力扣【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 序列的边界条件时?或者有没有想到其他更巧妙的解法?欢迎在评论区告诉我!

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

相关推荐
石小石Orz几秒前
效率提升一倍!谈谈我的高效开发工具链
前端·后端·trae
EndingCoder2 分钟前
测试 Next.js 应用:工具与策略
开发语言·前端·javascript·log4j·测试·全栈·next.js
吧唧霸2 分钟前
golang读写锁和互斥锁的区别
开发语言·算法·golang
xw55 分钟前
免费的个人网站托管-PinMe篇
服务器·前端
!win !10 分钟前
免费的个人网站托管-PinMe篇
前端·前端工具
牧天白衣.11 分钟前
CSS中linear-gradient 的用法
前端·css
军军36026 分钟前
Git大型仓库的局部开发:分步克隆 + 指定目录拉取
前端·git
前端李二牛31 分钟前
Vue3 特性标志
前端·javascript
coding随想37 分钟前
JavaScript事件处理程序全揭秘:从HTML到IE的各种事件绑定方法!
前端
搞个锤子哟37 分钟前
关键词匹配,过滤树
前端