使数组唯一的最小增量题解

题目

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 操作是不能让数组的每个值唯一的。

提示:

  1. 0 <= A.length <= 40000
  2. 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),相比普通排序快一些。最后的运行结果如下:
在这里插入图片描述

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章