我理解的算法 - 2121.相同元素的间隔之和及1685.有序数组中差绝对值之和
这两道题都是leetcode的中等题型,我们可以尝试使用不同解法去做做看,题目大家自行查看:2121.相同元素的间隔之和,1685.有序数组中差绝对值之和
之所以把这两道题目放在一起,是因为这两道题目的解题思路是一样的,大家之后遇到这种间隔和啊,差绝对值和啊都可以试试这种解题思路,那么我们先来看一下leetcode2121
2121.相同元素的间隔之和
首先,最简单的解题思路是一个一个的循环遍历去找相同的值,如果找到相同值,那其间隔就是2个下标值相减的绝对值,将所有循环完的这些相同值的间隔求和,就是最终结果了,代码也很简单
public long[] getDistances(int[] arr) {
long[] result = new long[arr.length];
for(int i = 0; i < arr.length; i++){
long sum = 0;
for(int j = 0; j < arr.length; j++){
if(arr[i] - arr[j] == 0){
sum += Math.abs(i - j);
}
}
result[i] = sum;
}
return result;
}
但是这道题目你用这个方式是过不了的,因为其时间复杂度比较高,达到了 O ( n 2 ) O(n^2) O(n2),所以这个题目我们还是要想其他的办法来降低时间复杂度,然后我们想到,我们是不是可以利用Map来降低其复杂度呢,然后想到用一个Map来存下所有的相同的数的下标,然后用这些下标计算出最终的间隔和呢?我们来试试,代码如下:
public long[] getDistances(int[] arr) {
long[] result = new long[arr.length];
Map<Integer, List<Integer>> map = new HashMap<>();
for(int i = 0; i < arr.length; i++){
if(map.containsKey(arr[i])){
List<Integer> indexList = map.get(arr[i]);
indexList.add(i);
}else{
List<Integer> indexList = new ArrayList<>();
indexList.add(i);
map.put(arr[i], indexList);
}
}
for(int i = 0; i < arr.length; i++){
int sum = 0;
if(map.containsKey(arr[i])){
for(int index : map.get(arr[i])){
sum += Math.abs(i - index);
}
}
result[i] = sum;
}
return result;
}
还是超时,原因是我们在使用下标去计算所有间隔和的时候,如果遇到非常多一样的数,极限下的时间复杂度其实还是 O ( n 2 ) O(n^2) O(n2),官方的case很全面的,所以这种方式也是不行的哈。
所以我们的思路自然而然的想到要有一种方式可以边循环边计算出最终的和那就好了。这时候我们不妨看一下官方的解题,寻找下思路,官方给出了数学的解法,乍一看完全看不明白,不过只要仔细揣摩,还是能看明白的,我们来一起研究一下。
官方解题公式的解析
官方的解题公式是根据最简单的也就是我们第一种暴力法的基础上来推导出来的公式,首先我们可以把暴力解法写成一个数学公式 r e s [ i ] = ∑ j , a r r [ j ] = a r r [ i ] ∣ i − j ∣ res[i]=\sum\limits_{j,arr[j]=arr[i]}|i-j| res[i]=j,arr[j]=arr[i]∑∣i−j∣,这个公式设结果为一个res数组,res数组的第i个值就等于了一个对所有间隔的求和, ∣ i − j ∣ |i-j| ∣i−j∣就是求间隔值的,使用了绝对值来求出,条件是 a r r [ j ] = a r r [ i ] arr[j]=arr[i] arr[j]=arr[i],
这个公式应该是看得明白的,然后到了精髓的地方了,我们可以把这个公式拆分开来,拆分成 j > i j>i j>i和 j < i j<i j<i两种情况来看,就是说比如我一个[2,1,3,1,2,3,3]的数组,我要算res[5]这个位置的3的间隔和,那么我就可以将res[5]这个位置的数前面等于这个数的所有间隔和与这个位置的数之后等于这个数的所有间隔的和相加起来那就是res[5]最后的结果了,即前面的间隔和为 ∣ 5 − 2 ∣ |5-2| ∣5−2∣ + 后面的间隔和为 ∣ 5 − 6 ∣ |5-6| ∣5−6∣,所以上面的公式拆分成 r e s [ i ] = ∑ j > i , a r r [ j ] = a r r [ i ] ∣ i − j ∣ + ∑ j < i , a r r [ j ] = a r r [ i ] ∣ i − j ∣ res[i]=\sum\limits_{j>i,arr[j]=arr[i]}|i-j| + \sum\limits_{j<i,arr[j]=arr[i]}|i-j| res[i]=j>i,arr[j]=arr[i]∑∣i−j∣+j<i,arr[j]=arr[i]∑∣i−j∣
接下来的步骤我们将绝对值展开,绝对值展开公式 i > j 的 时 候 ∣ i − j ∣ = i − j i>j的时候|i-j|=i-j i>j的时候∣i−j∣=i−j, i < j 的 时 候 ∣ i − j ∣ = j − i i<j的时候|i-j|=j-i i<j的时候∣i−j∣=j−i,所以公式又可以展开成 r e s [ i ] = ∑ j > i , a r r [ j ] = a r r [ i ] ( j − i ) + ∑ j < i , a r r [ j ] = a r r [ i ] ( i − j ) res[i]=\sum\limits_{j>i,arr[j]=arr[i]}(j-i) + \sum\limits_{j<i,arr[j]=arr[i]}(i-j) res[i]=j>i,arr[j]=arr[i]∑(j−i)+j<i,arr[j]=arr[i]∑(i−j),
然后我们可以进一步假设在 j < i j<i j<i的情况下,有 n 1 n1 n1个这样的 i i i, j > i j>i j>i的情况下,有 n 2 n2 n2个这样的 i i i,那么我们就能把公式中的 i i i给提出来, r e s [ i ] = ∑ j > i , a r r [ j ] = a r r [ i ] j − n 2 ∗ i + n 1 ∗ i − ∑ j < i , a r r [ j ] = a r r [ i ] j res[i]=\sum\limits_{j>i,arr[j]=arr[i]}j-n2*i + n1*i-\sum\limits_{j<i,arr[j]=arr[i]}j res[i]=j>i,arr[j]=arr[i]∑j−n2∗i+n1∗i−j<i,arr[j]=arr[i]∑j,这一步多想想还是可以想到的,相当于把 i i i踢出了限制条件来展开公式,这样公式就有了,分成2段来看,第一次循环计算 j < i j<i j<i的,第二次循环计算 j > i j>i j>i的,把2次相加起来就ok啦
这个公式是不是有点懵,哈哈,官方不愧是官方啊,看不懂没关系,我个人有想到另一种方式来推出这个公式来,而且我觉得会比这个公式的推导好理解哈。我们一起来看一下
我们拿这个例子[2,1,3,1,2,3,3],我们来看res[6],它的结果是 ∣ 6 − 2 ∣ + ∣ 6 − 5 ∣ = 5 |6 - 2| + |6 - 5| = 5 ∣6−2∣+∣6−5∣=5,我们一样把它分为在当前值前半部分和当前值后半部分,这边没有后半部分,所以我们来算前半部分, ∣ 6 − 2 ∣ + ∣ 6 − 5 ∣ |6-2| + |6-5| ∣6−2∣+∣6−5∣,这个的话,拆开绝对值变成 ( 6 − 2 ) + ( 6 − 5 ) (6-2) + (6-5) (6−2)+(6−5),那么我们可以拆成 6 − 2 + 6 − 5 6-2+6-5 6−2+6−5,把6都写在前面,得 6 + 6 − 2 − 5 = 6 ∗ 2 − ( 2 + 5 ) = 5 6+6-2-5 = 6*2-(2+5)=5 6+6−2−5=6∗2−(2+5)=5,这样就推算出来了哈 i ∗ 2 − s u m ( j ) i*2-sum(j) i∗2−sum(j),前半部分就搞定了哈,那么后半部分的话,由于我们这道题目没有,但是原理是一样的,可以推导出来为 s u m ( j ) − i ∗ 2 sum(j) - i*2 sum(j)−i∗2这样推导出来的哈,是不是简单多了,加减法的交换结合律哈就是,另外,如果数量是为0的话,我们不计算就行了。代码如下
public long[] getDistances(int[] arr) {
long[] result = new long[arr.length];
Map<Integer, long[]> map = new HashMap<>();
for(int i = 0; i < arr.length; i++){
long[] preArray = map.getOrDefault(arr[i], new long[2]);
if(preArray[0] != 0){
result[i] += i * preArray[0] - preArray[1];
}
preArray[0]++;
preArray[1] += i;
map.put(arr[i], preArray);
}
map = new HashMap<>();
for(int i = arr.length - 1; i >= 0; i--){
long[] preArray = map.getOrDefault(arr[i], new long[2]);
if(preArray[0] != 0){
result[i] += preArray[1] - i * preArray[0];
}
preArray[0]++;
preArray[1] += i;
map.put(arr[i], preArray);
}
return result;
}
1685.有序数组中差绝对值之和
这题的思路可以套用我们在【相同元素的间隔之和】当中的公式来做,思路也是将其拆分为目标值的前半部分和后半部分来分别计算,公式的推导和之前的是一模一样的,用我的那种推导方式推导即可,区别以及要注意的地方有2处,
第一处是在计算前半部分的时候,根据题目示例,我们需要把当前值和自己的差值的计算放入到这一部分中来,所以前半部分的个数需要+1,公式变为 n u m s [ i ] ∗ ( i + 1 ) − p r e f i x S u m nums[i] * (i + 1) - prefix Sum nums[i]∗(i+1)−prefixSum。
第二处为后半部分的计算中,如果我们要套用公式,那么我们需要得到从当前计算的值开始到最后的所有和,例如 [ 2 , 3 , 5 ] [2,3,5] [2,3,5]题目中如果我们要计算3这个值,我们只看后半段的计算,公式为: ( 从 3 之 后 开 始 一 直 到 最 后 的 和 ) − 3 ∗ 3 之 后 的 数 的 个 数 (从3之后开始一直到最后的和)- 3 * 3之后的数的个数 (从3之后开始一直到最后的和)−3∗3之后的数的个数(这边由于我们把3本身放到了前半部分计算的缘故,所以是3之后的做计算,不包含3本身) ,这边计算出来则是 5 − 3 ∗ 1 = 2 5-3*1 = 2 5−3∗1=2,那么这边的 5 5 5相当于是一个后缀和,就是5(包括其本身)之后的数都加起来即可,但是这样的话,就会变复杂了,所以我们来换一种方式来计算,首先由于数组数目固定,所以我们在计算当前数之后开始一直到最后的和的时候可以变相的来计算,我们只要用整个数组的和减去当前计算数字的前缀和,这样就得到了我们想要的值,即等于了 10 − ( 2 + 3 ) = 5 10 - (2+3) = 5 10−(2+3)=5, 10 10 10为总数, 2 + 3 2+3 2+3为当前值的前缀和, 5 5 5即为当前数之后开始一直到最后的和,所以我们会事先计算出整个数的和,也就是数组最后一个值的前缀和,这样我们的公式就能够得到套用了。
public int[] getSumAbsoluteDifferences(int[] nums) {
int[] result = new int[nums.length];
int sum = 0;
for (int num : nums) sum += num;
int preSum = 0;
for(int i = 0; i < nums.length; i++){
preSum += nums[i];
int front = nums[i] * (i + 1) - preSum;
int back = (sum - preSum) - nums[i] * (nums.length - 1 - i);
result[i] = front + back;
}
return result;
}
所以知道了题目的核心后,换一下题目,公式也稍微变化一下即可,所以这样的算法题,你理解了吗?