一、题目描述
给你一个整数数组 nums
,返回 nums
中所有 等差子序列 的数目。
如果一个序列中 至少有三个元素 ,并且任意两个相邻元素之差相同,则称该序列为等差序列。
- 例如,
[1, 3, 5, 7, 9]
、[7, 7, 7, 7]
和[3, -1, -5, -9]
都是等差序列。 - 再例如,
[1, 1, 2, 5, 7]
不是等差序列。
数组中的子序列是从数组中删除一些元素(也可能不删除)得到的一个序列。
- 例如,
[2,5,10]
是[1,2,1,2,4,1,5,10]
的一个子序列。
题目数据保证答案是一个 32-bit 整数。
示例 1:
输入:nums = [2,4,6,8,10] 输出:7 解释:所有的等差子序列为: [2,4,6] [4,6,8] [6,8,10] [2,4,6,8] [4,6,8,10] [2,4,6,8,10] [2,6,10]
示例 2:
输入:nums = [7,7,7,7,7] 输出:16 解释:数组中的任意子序列都是等差子序列。
提示:
1 <= nums.length <= 1000
-2^31 <= nums[i] <= 2^31 - 1
二、解题思路
我们可以使用动态规划的方法来解决这个问题。定义一个二维数组 dp
,其中 dp[i][d]
表示以 nums[i]
结尾,公差为 d
的等差子序列的数量。对于每个 nums[i]
,我们遍历它之前的所有元素 nums[j]
(其中 j < i
),计算公差 d = nums[i] - nums[j]
,并根据 dp[j][d]
更新 dp[i][d]
。
具体步骤如下:
- 初始化
dp
数组,大小为n x (max(nums) - min(nums))
,初始值为0。 - 遍历数组
nums
,对于每个nums[i]
,再遍历它之前的所有元素nums[j]
。 - 对于每一对
(nums[i], nums[j])
,计算公差d = nums[i] - nums[j]
。 - 更新
dp[i][d]
:dp[i][d] = dp[j][d] + 1
。这里加1是因为nums[i]
和nums[j]
本身可以构成一个长度为2的等差子序列。 - 如果
dp[j][d]
大于0,说明存在以nums[j]
结尾,公差为d
的等差子序列,那么nums[i]
可以加入到这些子序列中,从而形成新的等差子序列。 - 最终结果为所有
dp[i][d]
的和,但要减去长度为2的子序列的数量。
三、具体代码
import java.util.HashMap;
import java.util.Map;
public class Solution {
public int numberOfArithmeticSlices(int[] nums) {
int n = nums.length;
int total = 0;
Map<Integer, Integer>[] dp = new HashMap[n];
for (int i = 0; i < n; i++) {
dp[i] = new HashMap<>();
for (int j = 0; j < i; j++) {
long diff = (long) nums[i] - (long) nums[j];
if (diff > Integer.MAX_VALUE || diff < Integer.MIN_VALUE) {
continue; // 防止溢出
}
int d = (int) diff;
int countAtJ = dp[j].getOrDefault(d, 0);
int countAtI = dp[i].getOrDefault(d, 0);
total += countAtJ; // 累加以 nums[j] 结尾的等差子序列数量
dp[i].put(d, countAtI + countAtJ + 1); // 更新以 nums[i] 结尾的等差子序列数量
}
}
return total;
}
}
这段代码中,我们使用了一个 HashMap
数组 dp
来存储每个位置和公差对应的等差子序列数量,避免了直接使用二维数组可能导致的内存浪费。同时,我们在计算公差时使用了 long
类型来防止溢出。最终,我们累加所有长度大于2的等差子序列的数量,得到最终结果。
四、时间复杂度和空间复杂度
1. 时间复杂度
- 我们有一个双重循环,外层循环遍历数组
nums
的每一个元素,这需要 O(n) 的时间,其中 n 是数组nums
的长度。 - 对于外层循环中的每一个元素
nums[i]
,内层循环遍历它之前的所有元素nums[j]
(其中 j < i),这同样需要 O(i) 的时间。 - 因此,对于每个元素
nums[i]
,内层循环的时间复杂度是 O(i)。 - 将所有内层循环的时间复杂度相加,我们得到一个等差数列的和:O(1) + O(2) + … + O(n-1),这等价于 O(n*(n-1)/2),即 O(n^2)。
- 所以,总的时间复杂度是 O(n^2)。
2. 空间复杂度
- 我们定义了一个
HashMap
数组dp
,其大小与输入数组nums
的长度相同,即 O(n)。 - 对于数组中的每个元素
nums[i]
,我们存储了一个HashMap
,其大小取决于数组nums
中元素之间的差异。在最坏的情况下,如果所有元素都是不同的,那么这个HashMap
可能包含所有可能的差值,但差值的范围是有限的,即 [Integer.MIN_VALUE, Integer.MAX_VALUE],因此这个HashMap
的大小最多是 O(2^31),这是一个常数。 - 由于
HashMap
的大小与输入数组的大小无关,因此我们可以认为dp
数组的空间复杂度是 O(n)。
综上所述,代码的时间复杂度是 O(n^2),空间复杂度是 O(n)。
五、总结知识点
-
动态规划(Dynamic Programming):
- 动态规划是一种用于求解最优化问题的算法思想,它通过将问题分解为更小的子问题并存储这些子问题的解(即状态),来避免重复计算,从而提高效率。
-
哈希表(HashMap):
- 在 Java 中,
HashMap
是一种基于哈希表的映射结构,它存储键值对,并提供快速的查找、插入和删除操作。
- 在 Java 中,
-
数组(Array):
- 代码中使用了数组
dp
来存储每个位置的动态规划状态,这些状态是HashMap
的实例。
- 代码中使用了数组
-
循环(Loop):
- 代码使用了两个嵌套的
for
循环来遍历数组nums
的所有元素,并计算每个元素作为子序列结尾时的等差子序列数量。
- 代码使用了两个嵌套的
-
类型转换:
- 使用了类型转换
(long)
来避免计算两个整数之差时可能发生的整数溢出。
- 使用了类型转换
-
条件语句(if):
- 通过
if
语句来检查计算得到的差值是否在int
类型的范围内,以防止溢出。
- 通过
-
键值获取与更新:
- 使用了
HashMap
的getOrDefault
方法来获取指定键的值,如果键不存在则返回默认值。 - 使用了
put
方法来更新HashMap
中的键值对。
- 使用了
-
累加操作:
- 通过累加操作来计算所有可能的等差子序列的数量。
-
常量(Constants):
- 使用了
Integer.MAX_VALUE
和Integer.MIN_VALUE
来表示int
类型的最大值和最小值。
- 使用了
以上就是解决这个问题的详细步骤,希望能够为各位提供启发和帮助。