vlambda博客
学习文章列表

945. 使数组唯一的最小增量:计数排序思想的应用

最近,LeetCode 中文站上线了一个“打卡刷题计划”的活动。从 3 月 1 号到 4 月 30 号,每天题库列表会置顶一道题目,帮助大家每天养成做题的习惯。

这里是 LeetCode “打卡刷题计划” 题解系列的第三篇,3 月 19 日 ~ 3 月 25 日的部分题目题解。很高兴我的题解得到了 LeetCode 网站官方的认可,目前这三篇题解均已入选精选题解!

本文讲解的题目:

对数组中的元素做若干次 “+1” 操作,如何用最小的操作次数让数组元素各不相同?本题有两种看似截然不同的解法。然而在比较复杂度的时候,我们发现,两种解法都可以看成是基于排序的,只不过一个是普通的比较排序,一个是特殊的计数排序。

本文的题解已经被 LeetCode 网站选为“精选题解”了!点击底部的“阅读原文”,可以围观我在网站上的题解原文哦!

以下是题解原文。


这道题比较容易想到的有两种解法:

  • 先排序再遍历的方法,时间复杂度
  • 计数方法,时间复杂度 ,其中 是整数的取值范围大小

方法一:先排序再遍历

首先将数组进行排序,然后从左到右遍历数组:

  • 如果当前元素大于上一个元素,保持不变;
  • 如果当前元素小于等于上一个元素,就需要增加当前元素,直到大于上一个元素。

例如输入 [3, 2, 1, 2, 1, 7],排序后为 [1, 1, 2, 2, 3, 7]。遍历数组的过程如下图所示:

数字增加的过程(动图)

写成代码,只需要用一个变量保存当前的最大值即可。题解代码:

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) {
// 当前元素 A[i] 需要增加到 curmax + 1
res += (curmax + 1 - A[i]); // 记录自增次数
}
curmax = Math.max(curmax + 1, A[i]);
}
return res;
}

时间复杂度: ,主要的复杂度在排序上。

方法二:先计数再遍历

上面方法中,排序需要 的时间,比较昂贵。我们尝试不进行排序的方法。

例如输入 [3, 2, 1, 2, 1, 7],计数之后有两个 1 和两个 2。我们先看最小的数,两个 1 重复了,需要有一个增加到 2,这样 2 的数量变成了三个。在三个 2 中,又有两个需要增加到 3,然后又出现了两个 3…… 以此类推,可以计算出需要增加的次数。

我们可以用 map(如 C++ 的 unordered_map,Java 的 HashMap)来做计数。不过既然题目中说明了整数的范围在 0 到 40000 之间,我们不妨直接用一个大小为 40000 的数组做计数。

需要注意的是,虽然整数的范围是 0 到 40000,但是由于整数还会因为增加而变大,超出 40000 的范围。例如极端的情况:所有数都是 39999。所以需要对整数中最大的数单独处理。代码如下:

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;
}

这种解法的时间复杂度不能简单地写成 。设 为数组元素的个数, 为数组元素的可能取值个数(本期中 ),这个算法的时间复杂度是

两种解法的时间复杂度比较

  • 先排序再遍历的方法,时间复杂度
  • 计数方法,时间复杂度

如果 比较小的话,计数方法的时间复杂度可以认为是 ,比排序方法要快。这道题 取值 40000 算比较小的数,所以用计数方法会很快。如果 的值很大,就不能用计数方法。

虽然计数方法不需要排序,但我们可以把它看成是一种计数排序(不了解计数排序的同学可以自行查阅维基百科)。计数排序的时间复杂度恰好也是 。所以实际上两种方法都是用的排序,只不过一个是普通排序,一个是计数排序。我们在算法书中学过,计数排序这种排序方法是非比较排序,可以突破 的复杂度下限。但是它会对输入的性质有所要求,例如要求 比较小。

如果你能理解比较排序和非比较排序的关系,就很容易理解这两种解法的时间复杂度的关系了。


本文的题解已经被 LeetCode 网站选为“精选题解”了!点击底部的“阅读原文”,可以围观我在网站上的题解原文哦!