题目
945题 使数组唯一的最小增量(中等)
给定整数数组 A,每次 move 操作将会选择任意 A[i],并将其递增 1。
返回使 A 中的每个值都是唯一的最少操作次数。
示例 1:
输入:[1,2,2]
输出:1
解释:经过一次 move 操作,数组将变为 [1, 2, 3]。
示例 2:
输入:[3,2,1,2,1,7]
输出:6
解释:经过 6 次 move 操作,数组将变为 [3, 4, 1, 2, 5, 7]。
可以看出 5 次或 5 次以下的 move 操作是不能让数组的每个值唯一的。
提示:
- 0 <= A.length <= 40000
- 0 <= A[i] < 40000
题解
暴力法,HashSet
从题目中看,挨个+1试试。从不认真看题的我,果然没看见提示,也没认为提示多重要。要查询某个数字是否已存在,走来想起的就是HashSet。代码如下:
public int minIncrementForUnique(int[] A) {
if(A.length == 0){
return 0;
}
HashSet<Integer> set = new HashSet<>();
int count = 0;
set.add(A[0]);
for (int i=1;i<A.length;i++){
while (set.contains(A[i])){
count++;
A[i]++;
}
set.add(A[i]);
}
return count;
}
洋洋得意的我,瞬间提交代码。坐等通过。最后,我擦,居然超时了。
痛定思痛,怎么会错误呢,估计是hashset存储太慢导致的,怎么办呢?突然就瞄到提示了,原来数字有大小限制啊。那我直接用数组不就完了吗。于是乎。
暴力法,数组
第一次用的是pp[40000],最后不对,突然想到最坏的情况下是40000个39999,那得定pp[80000],于是乎把上面的代码修改成数组,代码如下:
public int minIncrementForUnique(int[] A) {
if(A.length == 0){
return 0;
}
int []pp = new int[80000];
int count = 0;
pp[A[0]] ++;
for (int i=1;i<A.length;i++){
while (pp[A[i]] != 0){
count++;
A[i]++;
}
pp[A[i]] ++;
}
return count;
}
这一次却有点忐忑,为啥,除了去掉了HashSet的存储慢以外,也没有别的啥提升啊,又超时了,怎么办。还是直接看结果把。
虽然没有超时,这击败5%的用户也是够了。
既然暴力法效果不行,这种数组题目,我们先排序,排序之后,再遍历来看看怎么计算。
排序遍历法
首先将数组进行排序,排序之后再遍历数组,看每个数需要增加的次数。
- 如果当前元素大于上一个元素,保持不变
- 如果当前元素小于等于上个元素,则增加当前元素,直到大于上一个元素
代码如下:
public int minIncrementForUnique(int[] A) {
Arrays.sort(A); // 先排序
int curmax = -1; // 当前数组最大值
int res = 0;
for (int i = 0; i < A.length; i++) {
if (A[i] <= curmax) {
int ai = curmax + 1; // 当前元素 A[i] 需要增加到 curmax + 1
res += (ai - A[i]); // 记录自增次数
A[i] = ai; // 增加当前元素
}
curmax = Math.max(curmax, A[i]);
}
return res;
}
来,这次比上一次做了一些提升,我们看一下效果如何:
这是排序,大部分时间花费在排序上面了。如果不做排序呢,其实和第一种暴力法有点类似,我们先做计数,然后再挨个进行计算。
计数再遍历
这一次计数我们学聪明了,不再用HashSet了,浪费时间,直接用数组,因为只是计算每个元素出现的次数,所以数组大小可以用40000了。
遍历的逻辑如下:
- 当前数字出现次数小于等于1,不需要增大
- 当前数字出现次数大于1,需要增大的数字有出现次数减1个,再把这些数计算次数放到下一个数里面进行计算,直到最后一个数
代码如下:
public int minIncrementForUnique(int[] A) {
int[] count = new int[40000];
int max = 0;
for (int a : A) {
count[a]++; // 计数
max = Math.max(max, a); // 计算数组中的最大值
}
int res = 0;
for (int j = 0; j < max; j++) {
if (count[j] > 1) {
// 有 count[j] - 1 个数需要增加
res += count[j] - 1;
count[j+1] += count[j] - 1;
}
}
// count[max] 单独计算,是因为可能超出 40000 的边界
if (count[max] > 1) {
int d = count[max] - 1;
// 有 d 个数需要增加
// 分别增加为 max + 1, max + 2, ... max + d
// 使用等差数列公式求和
res += (1 + d) * d / 2;
}
return res;
}
这一次去掉了排序,或者说这一种其实叫做计数排序,时间复杂度为O(n+k),相比普通排序快一些。最后的运行结果如下: