📚 前置知识点:工欲善其事,必先利其器
在开始刷题之前,我们先回顾两个在算法竞赛和日常开发中非常实用的 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 算法)时,我们需要一个比任何可能出现的数都大的初始值。
- 含义:表示数学上的正无穷大 ()。
- 特性:它比任何有限的浮点数或整数都要大。
- 用途:常用于初始化
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] 之外其余各元素的乘积。
要求:
- 不要使用除法。
- 题目数据保证数组之中任意元素的全部前缀元素和后缀的乘积都在 32 位整数范围内。
示例 1:
-
输入:
nums = [1, 2, 3, 4] -
输出:
[24, 12, 8, 6]
💡 解题思路分析
既然不能用除法(即不能先算总乘积再除以当前数),我们需要换一种思路。
对于数组中的任意一个元素 nums[i],它的结果实际上由两部分组成:
- 左边所有元素的乘积(前缀积)
- 右边所有元素的乘积(后缀积)
方法一:左右乘积列表法(直观解法)
我们可以构建两个辅助数组:
L[i]:存储索引i左侧所有元素的乘积。R[i]:存储索引i右侧所有元素的乘积。
步骤:
-
初始化:
L[0] = 1(第一个元素左边没有数,乘积视为 1)R[n-1] = 1(最后一个元素右边没有数,乘积视为 1)
-
填充 L 数组:从左向右遍历,
L[i] = L[i-1] * nums[i-1]。 -
填充 R 数组:从右向左遍历,
R[i] = R[i+1] * nums[i+1]。 -
计算结果:
answer[i] = L[i] * R[i]。
复杂度分析:
- 时间复杂度:,三次遍历。
- 空间复杂度:,需要额外的 L 和 R 数组。
方法二:空间优化(进阶解法)
题目通常希望我们将空间复杂度优化到 (不计输出数组)。
我们可以发现,L 数组的计算过程可以复用输出数组 answer,而 R 数组可以在遍历时用一个变量动态维护。
优化步骤:
-
初始化
answer为前缀积:answer[0] = 1- 遍历
i从 1 到n-1:answer[i] = answer[i-1] * nums[i-1] - 此时
answer[i]存储的就是L[i]。
-
动态维护后缀积并更新
answer:-
定义变量
R = 1(代表当前元素右边的乘积)。 -
从右向左遍历
i从n-1到0: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] 为例:
-
第一轮(前缀积) :
answer初始化为[1, 1, 1, 1]i=1:answer[1] = answer[0]*1 = 1i=2:answer[2] = answer[1]*2 = 2i=3:answer[3] = answer[2]*3 = 6- 此时
answer=[1, 1, 2, 6](分别对应每个位置左边的乘积)
-
第二轮(后缀积合并) :
R初始化为1i=3:answer[3] = 6 * 1 = 6;R变为1*4=4i=2:answer[2] = 2 * 4 = 8;R变为4*3=12i=1:answer[1] = 1 * 12 = 12;R变为12*2=24i=0:answer[0] = 1 * 24 = 24;R变为24*1=24- 最终
answer=[24, 12, 8, 6]✅
📝 总结
这道题是考察数组预处理和空间换时间思想的经典案例。
- 核心思想:将复杂的全局乘积拆解为“左边部分”和“右边部分”。
- 延伸:这种“左右扫描”的思想在很多数组题中都有应用,比如“接雨水”、“ trapping rain water”等变种问题。
希望这篇笔记能帮你彻底搞懂这道题!如果觉得有用,欢迎点赞收藏 👍。