全 0 子数组的数目

标签: 数组 数学

难度: Medium

给你一个整数数组 nums ,返回全部为 0 的 子数组 数目。

子数组 是一个数组中一段连续非空元素组成的序列。

示例 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 。

提示:

  • 1 <= nums.length <= 105
  • -109 <= nums[i] <= 109

Submission

运行时间: 75 ms

内存: 26.6 MB

class Solution:
    def zeroFilledSubarray(self, nums: List[int]) -> int:
        # res = 0
        # left = 0
        # while left<len(nums):
        #     if nums[left]!=0:
        #         left +=1
        #     else:
        #         right = left+1
        #         while right<len(nums):
        #             if nums[right]==0:
        #                 right +=1
        #             else:
        #                 break
        #         nZeros = right-left
        #         res += (1+nZeros)*nZeros//2
        #         left = right+1
        # return res
        res = 0
        nZeros = 0
        for num in nums:
            if num==0:
                nZeros+=1
            else:
                if nZeros:
                    res += (1+nZeros)*nZeros//2
                    nZeros = 0
        if nZeros:
            res += (1+nZeros)*nZeros//2
     
        return res

Explain

此题解采用一遍扫描的方式计算所有全0子数组的数目。使用变量res来存储子数组的总数,变量nZeros来记录连续0的长度。遍历数组nums,每遇到一个0,nZeros增加1。遇到非0数字时,如果nZeros大于0,说明之前有一段连续的0,根据连续0的数量计算可以形成的子数组数目并加到res上,然后重置nZeros为0。遍历结束后,如果数组的最后一部分是连续的0,需要再处理一次这部分数据。

时间复杂度: O(n)

空间复杂度: O(1)

class Solution:
    def zeroFilledSubarray(self, nums: List[int]) -> int:
        res = 0  # 存储全0子数组的总数
        nZeros = 0  # 连续0的个数
        for num in nums:
            if num == 0:
                nZeros += 1  # 遇到0,连续0的个数加1
            else:
                if nZeros:
                    res += (1+nZeros)*nZeros//2  # 计算连续0形成的子数组数并加到总数上
                    nZeros = 0  # 重置连续0的个数
        if nZeros:
            res += (1+nZeros)*nZeros//2  # 处理数组结尾的连续0
        return res

Explore

这个公式的数学原理基于组合数学中的连续自然数求和公式。假设有n个连续的0,可以形成的全0子数组是:单个0为1个子数组,连续两个0为1个子数组,以此类推,直到连续n个0为1个子数组。这是一个等差数列求和的问题,其中第1项是1,最后1项是n,项数也是n。等差数列求和公式为`(首项 + 末项) * 项数 / 2`,这里即为`(1+n)*n/2`。这个公式直接计算了从1到n的所有可能的连续子数组数目的总和。

代码中单独处理最后一段连续的0是因为在遍历数组时,只有遇到非0元素后才会计算前面连续0形成的子数组。如果数组最后一部分是连续的0,遍历结束时不会再遇到非0元素触发计算,因此需要在循环结束后,单独对nZeros进行一次检查,如果nZeros大于0,则再进行一次子数组数量的计算,确保这部分也被正确统计。

如果输入数组中没有0,那么变量nZeros将始终为0,因此每次遇到非0元素时,由于nZeros为0,计算子数组的分支不会执行。同样,循环结束后的额外处理也不会执行,因为nZeros仍为0。在这种情况下,res初始化为0,且在执行过程中不会有任何增加,最终返回的也是0。这种情况下没有必要进行0子数组的计算,代码自然地处理了这种情况,效率很高。

原始代码并没有显式地处理整数溢出问题。Python的整数类型在内部使用变长表示,理论上只受限于机器的内存,不会像固定位宽的整数类型那样容易溢出。然而,在实际应用中,如果数组非常大,计算`(1+nZeros)*nZeros//2`可能导致内存使用过大。在实际部署中可能需要考虑此类极端情况,通过适当的算法优化或事先检查数组大小来处理可能的资源耗尽问题。