LeetCode算法日记 - Day 85: 等差数列划分
目录
1. 等差数列划分
1.1 题目解析
1.2 解法
1.3 代码实现
1. 等差数列划分
https://leetcode.cn/problems/arithmetic-slices/description/
如果一个数列 至少有三个元素 ,并且任意两个相邻元素之差相同,则称该数列为等差数列。
- 例如,
[1,3,5,7,9]、[7,7,7,7]和[3,-1,-5,-9]都是等差数列。
给你一个整数数组 nums ,返回数组 nums 中所有为等差数组的 子数组 个数。
子数组 是数组中的一个连续序列。
示例 1:
输入:nums = [1,2,3,4] 输出:3 解释:nums 中有三个子等差数组:[1, 2, 3]、[2, 3, 4] 和 [1,2,3,4] 自身。
示例 2:
输入:nums = [1] 输出:0
提示:
1 <= nums.length <= 5000-1000 <= nums[i] <= 1000
1.1 题目解析
题目本质
统计数组中所有满足等差数列条件的连续子数组个数。核心是三个元素一组,根据条件判断是否合理。
常规解法
枚举所有可能的子数组(双重循环确定起点和终点),对每个子数组判断是否为等差数列。具体做法是:外层循环固定起点 i,内层循环遍历终点 j(j >= i+2),检查 nums[i...j] 是否等差。
// 暴力枚举法 - O(n³) 时间复杂度
static class Solution {public int numberOfArithmeticSlices(int[] nums) {int n = nums.length;if (n < 3) return 0;int count = 0;// 外层循环:固定起点 ifor (int i = 0; i < n - 2; i++) {// 内层循环:遍历终点 j(至少需要3个元素,所以 j >= i+2)for (int j = i + 2; j < n; j++) {// 检查子数组 nums[i...j] 是否为等差数列if (isArithmetic(nums, i, j)) {count++;}}}return count;}// 辅助函数:检查 nums[start...end] 是否为等差数列private boolean isArithmetic(int[] nums, int start, int end) {// 计算第一个差值作为基准int diff = nums[start + 1] - nums[start];// 检查后续所有相邻元素的差值是否都等于基准差值for (int k = start + 1; k < end; k++) {if (nums[k + 1] - nums[k] != diff) {return false;}}return true;}
}
问题分析
暴力枚举的时间复杂度是 O(n³)(双重循环 O(n²) × 等差判定 O(n)),对于 n=5000 的数据规模会超时。关键问题在于:大量重复计算了相邻元素的差值,没有利用"连续性"这个特点。
思路转折
等差数列有个重要性质——如果 [a,b,c] 是等差,且 [b,c,d] 也是等差(差值相同),那么 [a,b,c,d] 必然也是等差。这意味着我们可以用动态规划,记录"以当前元素结尾的等差子数组个数",通过前一个状态递推得到当前状态。这样只需要一次遍历 O(n),避免重复计算。
1.2 解法
算法思想:定义 dp[i] 表示以 nums[i] 结尾的等差子数组个数。当检查到第 i 个元素时,如果 nums[i] - nums[i-1] == nums[i-1] - nums[i-2](最近三个元素等差)
则有两种情况:
-
新增一个长度为3的等差子数组:[nums[i-2], nums[i-1], nums[i]]
-
所有以 nums[i-1] 结尾的等差子数组都可以向右扩展一位
递推公式:
dp[i] = dp[i-1] + 1 (当 nums[i]-nums[i-1] == nums[i-1]-nums[i-2])dp[i] = 0 (否则)
最终答案为所有 dp[i] 的和。
i)初始化 dp 数组(长度为 n)和结果变量 sum = 0
ii)从索引 i=2 开始遍历(因为至少需要3个元素)
iii)对于每个位置 i,计算差值:d1 = nums[i] - nums[i-1],d2 = nums[i-1] - nums[i-2]
iv)如果 d1 == d2,则 dp[i] = dp[i-1] + 1;否则 dp[i] = 0
v)累加 dp[i] 到 sum
vi)返回 sum
易错点:
-
起始索引错误:循环必须从 i=2 开始,因为需要访问 nums[i-2]。从 i=0 或 i=1 开始会数组越界
-
理解 dp[i] 的含义:dp[i] 不是"最长等差子数组长度",而是"以 i 结尾的等差子数组个数"。例如 dp[3]=2 表示有2个等差子数组以索引3结尾
-
忘记累加结果:每次计算出 dp[i] 后要立即累加到 sum,不能只保存 dp[i] 最后再处理
-
边界条件:当数组长度 < 3 时,直接返回0(题目要求至少3个元素)
1.3 代码实现
static class Solution {public int numberOfArithmeticSlices(int[] nums) {int n = nums.length;if (n < 3) return 0; // 少于3个元素无法构成等差数列int[] dp = new int[n]; // dp[i]: 以 nums[i] 结尾的等差子数组个数int result = 0;for (int i = 2; i < n; i++) {// 检查最近三个元素是否等差if (nums[i] - nums[i-1] == nums[i-1] - nums[i-2]) {// 可以扩展:继承 dp[i-1] 个 + 新增1个(最短的3元素数组)dp[i] = dp[i-1] + 1;}// 否则 dp[i] = 0(默认值)result += dp[i]; // 累加当前位置的贡献}return result;}
}
复杂度分析
-
时间复杂度:O(n),只需遍历数组一次
-
空间复杂度:O(n),使用了 dp 数组
