遇到LeetCode 1588这道题时,我第一反应是理解清楚题目要求。题目给定一个正整数数组arr,要求计算所有可能的奇数长度子数组的和。这里的子数组指的是原数组中连续的元素序列。
最直观的解法就是暴力枚举所有可能的奇数长度子数组,然后逐个求和。比如对于数组[1,4,2,5,3],我们需要考虑:
然后把这些子数组的和全部相加。这种方法虽然直观,但时间复杂度很高。对于一个长度为n的数组,奇数长度子数组的数量大约是n²/4个,每个子数组求和需要O(k)时间(k是子数组长度),所以总时间复杂度是O(n³)。
注意:在实际面试中,即使你能想到暴力解法,也应该主动指出它的效率问题,并尝试寻找优化方案。
为了优化计算效率,我们可以引入前缀和(Prefix Sum)的概念。前缀和是一种预处理技术,可以让我们在O(1)时间内计算任意子数组的和。
前缀和数组pre的定义是:pre[i]表示arr[0]到arr[i-1]的和。例如:
有了前缀和数组后,计算arr[i]到arr[j]的和就变成了pre[j+1] - pre[i],这只需要一次减法操作。
在本题的解法中,我们首先构建前缀和数组:
java复制int[] pre = new int[length + 1];
for (int i = 0; i < length; i++) {
pre[i + 1] = pre[i] + arr[i];
}
这样,我们就把暴力解法中重复计算子数组和的问题解决了,时间复杂度降到了O(n²)。
有了前缀和数组后,我们需要用双重循环来遍历所有可能的奇数长度子数组:
具体实现代码如下:
java复制for (int i = 0; i < length; i++) {
for (int j = 1; j <= length; j = j + 2) {
if (i + j - 1 < length) {
res += pre[i + j] - pre[i];
}
}
}
这里有几个关键点需要注意:
让我们分析一下优化后算法的时间和空间复杂度:
时间复杂度:
空间复杂度:
相比暴力解法的O(n³)时间复杂度,这个优化是非常显著的。对于n=100的最大输入规模,O(n²)的算法完全可以胜任。
虽然O(n²)的解法已经足够好,但我在思考是否还有进一步优化的空间。通过数学分析,我们可以发现每个元素arr[i]在所有奇数长度子数组中出现的次数是有规律的。
对于一个长度为n的数组,元素arr[i]出现在:
具体来说,arr[i]出现在:
经过推导,可以得出arr[i]在所有奇数长度子数组中出现的次数为:
((i+1)*(n-i)+1)/2
这样,总和可以表示为:
sum = Σ arr[i] * ((i+1)*(n-i)+1)/2 for all i
这种数学方法可以将时间复杂度降到O(n),空间复杂度降到O(1)。不过实现起来需要一定的数学功底,在面试中如果能想到并解释清楚这个思路会非常加分。
在实现算法时,考虑边界条件非常重要。针对这个问题,我设计了以下几类测试用例:
最小输入测试:
偶数长度数组测试:
典型测试用例:
全相同元素测试:
最大规模测试:
在实际编码时,应该先写出这些测试用例,然后再实现算法,这样可以确保代码的正确性。
基于前面的分析,我给出了完整的Java实现。代码中还有一些可以优化的地方:
变量命名可以更清晰:
内层循环的边界条件可以优化:
可以添加注释说明关键步骤:
优化后的代码如下:
java复制class Solution {
public int sumOddLengthSubarrays(int[] arr) {
int totalSum = 0;
int n = arr.length;
// 构建前缀和数组
int[] prefixSum = new int[n + 1];
for (int i = 0; i < n; i++) {
prefixSum[i + 1] = prefixSum[i] + arr[i];
}
// 遍历所有可能的奇数长度子数组
for (int start = 0; start < n; start++) {
// 最大子数组长度不超过剩余元素数量
for (int len = 1; start + len <= n; len += 2) {
int end = start + len;
totalSum += prefixSum[end] - prefixSum[start];
}
}
return totalSum;
}
}
虽然这个问题看起来像纯粹的编程练习,但实际上它有实际的应用场景。比如在信号处理中,我们可能需要对不同窗口大小的信号片段进行分析;在金融领域,可能需要计算不同时间窗口的指标。
这个问题还可以扩展为:
理解这个问题的解法,可以帮助我们解决更复杂的问题。前缀和技巧在很多场景下都非常有用,比如解决子数组和、区间查询等问题。
在实际工程中,我们可能还需要考虑:
这些思考可以帮助我们更深入地理解算法和数据结构的应用。