【力扣-238. 除了自身以外数组的乘积 ✨】Python笔记

0 阅读5分钟

📚 前置知识点:工欲善其事,必先利其器

在开始刷题之前,我们先回顾两个在算法竞赛和日常开发中非常实用的 Python 技巧,它们能极大地简化我们的代码逻辑。

1. collections.defaultdict(int):自动初始化的计数器

在处理统计类问题(如词频统计、数字计数)时,我们经常会遇到“键不存在则默认为 0”的需求。

  • 普通字典的痛点:访问不存在的键会报 KeyError,或者需要繁琐的 if key in dict 判断。

  • defaultdict 的优势:它是 dict 的子类。当你访问一个不存在的键时,它会自动调用传入的工厂函数(如 int)来生成默认值。

    • int() 不带参数时返回 0
    • 因此,defaultdict(int) 创建的字典,缺失键的默认值就是 0
from collections import defaultdict

# 初始化
counts = defaultdict(int)

# 直接使用 += 操作,无需判断键是否存在
counts['apple'] += 1 
counts['banana'] += 1
counts['apple'] += 1

print(counts) 
# 输出: defaultdict(<class 'int'>, {'apple': 2, 'banana': 1})

2. float('inf'):正无穷大的妙用

在寻找最小值或初始化距离数组(如 Dijkstra 算法)时,我们需要一个比任何可能出现的数都大的初始值。

  • 含义:表示数学上的正无穷大 (++\infty)。
  • 特性:它比任何有限的浮点数或整数都要大。
  • 用途:常用于初始化 min_val,确保第一个实际数值能顺利更新它。
# 寻找列表最小值
nums = [5, 2, 9, 1]
min_val = float('inf') # 初始化为无穷大

for num in nums:
    if num < min_val:
        min_val = num

print(min_val) # 输出: 1

同理,float('-inf') 表示负无穷,常用于寻找最大值。


🎯 题目引入:LeetCode 238

掌握了上述工具后,我们来看一道经典的数组处理题目。

题目描述

给你一个整数数组 nums,返回数组 answer,其中 answer[i] 等于 nums 中除 nums[i] 之外其余各元素的乘积。

要求:

  1. 不要使用除法
  2. 题目数据保证数组之中任意元素的全部前缀元素和后缀的乘积都在 32 位整数范围内。

示例 1:

  • 输入: nums = [1, 2, 3, 4]

  • 输出: [24, 12, 8, 6]

💡 解题思路分析

既然不能用除法(即不能先算总乘积再除以当前数),我们需要换一种思路。

对于数组中的任意一个元素 nums[i],它的结果实际上由两部分组成:

  1. 左边所有元素的乘积(前缀积)
  2. 右边所有元素的乘积(后缀积)

方法一:左右乘积列表法(直观解法)

我们可以构建两个辅助数组:

  • L[i]:存储索引 i 左侧所有元素的乘积。
  • R[i]:存储索引 i 右侧所有元素的乘积。

步骤:

  1. 初始化

    • L[0] = 1 (第一个元素左边没有数,乘积视为 1)
    • R[n-1] = 1 (最后一个元素右边没有数,乘积视为 1)
  2. 填充 L 数组:从左向右遍历,L[i] = L[i-1] * nums[i-1]

  3. 填充 R 数组:从右向左遍历,R[i] = R[i+1] * nums[i+1]

  4. 计算结果answer[i] = L[i] * R[i]

复杂度分析:

  • 时间复杂度:O(N)O(N),三次遍历。
  • 空间复杂度:O(N)O(N),需要额外的 L 和 R 数组。

方法二:空间优化(进阶解法)

题目通常希望我们将空间复杂度优化到 O(1)O(1)(不计输出数组)。
我们可以发现,L 数组的计算过程可以复用输出数组 answer,而 R 数组可以在遍历时用一个变量动态维护。

优化步骤:

  1. 初始化 answer 为前缀积

    • answer[0] = 1
    • 遍历 i 从 1 到 n-1answer[i] = answer[i-1] * nums[i-1]
    • 此时 answer[i] 存储的就是 L[i]
  2. 动态维护后缀积并更新 answer

    • 定义变量 R = 1(代表当前元素右边的乘积)。

    • 从右向左遍历 in-10

      • R = R * nums[i] (更新 R,为下一个左边的元素做准备)

💻 代码实现 (Python)

这里展示空间优化后的最终代码,简洁且高效。

from typing import List

class Solution:
    def productExceptSelf(self, nums: List[int]) -> List[int]:
        n = len(nums)
        answer = [1] * n
        
        # 1. 先计算前缀积,存入 answer
        # answer[i] 将包含 nums[0]...nums[i-1] 的乘积
        for i in range(1, n):
            answer[i] = answer[i-1] * nums[i-1]
            
        # 2. 动态计算后缀积,并直接乘到 answer 中
        # R 代表当前元素右侧所有元素的乘积
        R = 1
        for i in range(n - 1, -1, -1):
            # 此时 answer[i] 是左侧乘积,R 是右侧乘积
            answer[i] = answer[i] * R
            
            # 更新 R,把当前元素纳入右侧乘积中,供下一次循环(左边的元素)使用
            R *= nums[i]
            
        return answer

代码图解演示

nums = [1, 2, 3, 4] 为例:

  1. 第一轮(前缀积)

    • answer 初始化为 [1, 1, 1, 1]
    • i=1: answer[1] = answer[0]*1 = 1
    • i=2: answer[2] = answer[1]*2 = 2
    • i=3: answer[3] = answer[2]*3 = 6
    • 此时 answer = [1, 1, 2, 6] (分别对应每个位置左边的乘积)
  2. 第二轮(后缀积合并)

    • R 初始化为 1
    • i=3: answer[3] = 6 * 1 = 6; R 变为 1*4=4
    • i=2: answer[2] = 2 * 4 = 8; R 变为 4*3=12
    • i=1: answer[1] = 1 * 12 = 12; R 变为 12*2=24
    • i=0: answer[0] = 1 * 24 = 24; R 变为 24*1=24
    • 最终 answer = [24, 12, 8, 6]

📝 总结

这道题是考察数组预处理空间换时间思想的经典案例。

  1. 核心思想:将复杂的全局乘积拆解为“左边部分”和“右边部分”。
  2. 延伸:这种“左右扫描”的思想在很多数组题中都有应用,比如“接雨水”、“ trapping rain water”等变种问题。

希望这篇笔记能帮你彻底搞懂这道题!如果觉得有用,欢迎点赞收藏 👍。