前言
在LeetCode算法题库中,2615. 等值距离和是一道中等难度的数组经典题 ,核心考察哈希表分组+前缀和优化 的解题思想。题目看似可以用暴力枚举解决,但面对10^5的大数据量,暴力解法会直接超时。本文将从题目解析、暴力解法缺陷、前缀和优化思路、代码详解、复杂度分析五个维度,深度拆解这道题的最优解法。
一、题目详细解析
1. 题目描述
给定一个下标从 0 开始的整数数组 nums,构造一个等长数组 arr:
-
对于每个下标
i,arr[i]等于所有满足nums[j] == nums[i] 且 j != i的下标j与i的绝对差之和|i-j|; -
若没有相同值的其他下标,
arr[i] = 0。
2. 示例说明
输入:nums = [1,3,1,1,2] 输出:[5,0,3,4,0]
-
下标0:值为1,相同值下标为2、3 →
|0-2| + |0-3| = 5 -
下标1:值为3,无相同值 →
0 -
下标2:值为1,相同值下标为0、3 →
|2-0| + |2-3| = 3
3. 核心约束
-
数组长度
1 <= nums.length <= 10^5(大数据量,必须线性复杂度) -
元素值范围
0 <= nums[i] <= 10^9(需用哈希表存储)
二、暴力解法:思路与缺陷
1. 暴力思路
最直观的想法:遍历每个下标 i,再遍历整个数组找到所有与 nums[i] 相等的下标 j,累加绝对差。
2. 代码伪码
vector<long long> distance(vector<int>& nums) {
int n = nums.size();
vector<long long> ans(n, 0);
for (int i = 0; i < n; i++) {
for (int j = 0; j < n; j++) {
if (i != j && nums[i] == nums[j]) {
ans[i] += abs(i - j);
}
}
}
return ans;
}
3. 致命缺陷
时间复杂度为 **O(n²)**,当 n=10^5 时,运算量达到 10^10,远超算法时间限制,直接超时。因此必须优化。
三、优化核心:前缀和+哈希表分组
题目提示明确给出了优化方向:前缀和,我们分两步实现优化:
步骤1:哈希表分组
相同数值的下标才需要计算绝对差,因此用哈希表对数组分组:
-
Key:数组中的数值
-
Value:该数值对应的所有下标集合(有序数组)
例如示例 [1,3,1,1,2],分组结果: 1: [0,2,3],3: [1],2: [4]
步骤2:前缀和公式推导(核心)
对于一组相同数值的下标数组 pos = [p0, p1, p2, ..., pk-1],我们需要计算每个 pos[i] 与组内其他元素的绝对差之和。
设:
-
组内元素个数:
k -
当前下标:
pos[i] -
前缀和:
preSum[i] = pos[0] + pos[1] + ... + pos[i] -
组内总下标和:
totalSum = preSum[k-1]
公式拆分
-
左侧元素和 :
pos[i]左侧有i个元素[p0, p1, ..., pi-1]绝对差和 =i * pos[i] - preSum[i-1] -
右侧元素和 :
pos[i]右侧有k-i-1个元素[pi+1, ..., pk-1]绝对差和 =(totalSum - preSum[i]) - (k-i-1) * pos[i] -
总绝对差和:左侧和 + 右侧和
这个公式将O(k)的遍历计算 降为O(1)的数学计算,是线性复杂度的关键!
四、最优解法代码逐行解析
以下是题目对应的C++最优代码,我们逐行拆解:
class Solution {
public:
vector<long long> distance(vector<int>& nums) {
int n = nums.size();
// 答案数组:必须用long long,避免int溢出
vector<long long> ans(n, 0);
// 哈希表:key=数值,value=对应下标集合
std::unordered_map<int, std::vector<int>> hashMap;
// 第一步:遍历数组,完成下标分组
for (int i = 0; i < n; i++) {
hashMap[nums[i]].push_back(i);
}
// 第二步:遍历每个分组,用前缀和计算答案
for (const auto& pair : hashMap) {
// 分组内的下标数组
const auto& pos = pair.second;
int k = pos.size();
// 计算分组内所有下标的总和
long long totalSum = 0;
for (int idx : pos) {
totalSum += idx;
}
// 前缀和:动态更新,无需额外数组
long long prefixSum = 0;
for (int i = 0; i < k; ++i) {
long long cur = pos[i];
// 计算左侧绝对差和
long long leftSum = (long long)i * cur - prefixSum;
// 计算右侧绝对差和
long long rightSum = (totalSum - prefixSum - cur) - (long long)(k - i - 1) * cur;
// 赋值到答案数组的对应下标位置
ans[cur] = leftSum + rightSum;
// 更新前缀和
prefixSum += cur;
}
}
return ans;
}
};
关键细节说明
-
数据类型 :答案必须用
long long!因为10^5个下标相乘会超出int范围,强制类型转换(long long)避免溢出; -
动态前缀和 :无需单独开辟前缀和数组,用变量
prefixSum动态累加,节省空间; -
哈希表遍历:只处理有多个下标的分组,单个下标的分组直接跳过(答案默认为0)。
五、复杂度分析
1. 时间复杂度:O(n)
-
分组遍历:O(n),每个下标仅入组一次;
-
前缀和计算:O(n),所有分组的元素总数等于原数组长度;
-
总复杂度:线性级别,完美适配
10^5的大数据量。
2. 空间复杂度:O(n)
-
哈希表存储所有下标,空间占用为O(n);
-
答案数组为O(n),属于必要空间。
五、总结
这道题是前缀和在数组问题中的经典应用,核心解题思路可以总结为三点:
-
分组思想:用哈希表将相同数值的下标聚合,排除无关元素;
-
数学优化:通过前缀和公式将绝对差和的计算从O(k)降为O(1);
-
细节规避 :大数据量下必须使用
long long避免整型溢出。
掌握这种「分组+前缀和」的思路,还可以解决LeetCode上同类的下标距离和问题,是算法面试中的高频考点~