小知识,大挑战!本文正在参与“程序员必备小知识”创作活动。
介绍
这是一个生活中很常见的问题,我们往往不是找最大或者最小的值,而是去找第x大的值(比如第二或者第三大)。本文就以寻找第三大的数为例来看看这个问题的解法。
例题如下:
给你一个非空数组,返回此数组中 第三大的数 。如果不存在,则返回数组中最大的数。
示例 1:
输入:[3, 2, 1]
输出:1
解释:第三大的数是 1 。
示例 2:
输入:[1, 2]
输出:2
解释:第三大的数不存在, 所以返回最大的数 2 。
示例 3:
输入:[2, 2, 3, 1]
输出:1
解释:注意,要求返回第三大的数,是指在所有不同数字中排第三大的数。
此例中存在两个值为 2 的数,它们都排第二。在所有不同数字中排第三大的数为 1 。
解
注意到示例3,说明这个题是要求严格的第三大的数。
排序
从大到小排序后直接取第三个元素,这里不多赘述。
最大堆
这是我首先想到的方法,即使用大根堆这种数据结构,线性地遍历一遍数组并存入数据,然后从顶部依次推出三个元素,第三个即为答案。这种方法复杂度应该是O(nlogn)。
但要注意这里有三个坑:
- 由于是要求严格的第三大数,所以需要一个set来去重。
- 要注意若没有3个不重复的数,在弹出大根堆顶端元素三次时会报错。
- 注意数据的范围,在重写 compare 方法时不能使用简单的
a-b,这样会导致超出 int 类型的范围。
class Solution {
public int thirdMax(int[] nums) {
int n = nums.length;
Set<Integer> set = new HashSet<>();
PriorityQueue<Integer> pq = new PriorityQueue<>(n, new Comparator<Integer>() {
@Override
public int compare(Integer a, Integer b) {
if (a < b) return 1;
else if (a > b) return -1;
else return 0;
}
});
for (int num : nums) {
if (set.contains(num)) continue;
pq.add(num);
set.add(num);
}
if (set.size() < 3) return pq.poll();
pq.poll();
pq.poll();
return pq.poll();
}
}
最小堆
相比于上面的方法,可以不用把所有元素放进最小堆中,只需维护一个只有3个不同数字的最小堆即可,每次若有更大的元素,则弹出顶部元素,加入这个更大元素。
TreeSet 可以完美符合我们的需求,内部是排好序的,且不允许重复。
class Solution {
public int thirdMax(int[] nums) {
TreeSet<Integer> ts = new TreeSet<>();
for (int num : nums) {
ts.add(num);
if (ts.size() > 3) {
ts.pollFirst();
}
}
return ts.size() == 3 ? ts.pollFirst() : ts.pollLast();
}
}
一次遍历
还能有更快的方法,维护三个变量,分别来存储前三大的元素,每次遍历新元素的时候分类讨论:
- 若该元素比最大的还大,则它顶替最大元素的位置,原来的最大元素到第二大元素,原来的第二大元素到第三大元素,原来的第三大元素舍弃;
- 若该元素介于第二大与最大数之间,则它顶替第二大元素位置,原来的第二大元素到第三大元素,原来的第三大元素舍弃;
- 若该元素介于第三大与第二大数之间,则它顶替第三大元素位置,原来的第三大元素舍弃。
除此之外,为了避免范围问题(某个元素刚好就是 int 的最小值),用 long 的最小值来初始化这三个值。若遍历过程中有重复的元素,不管即可。最后,要注意元素不满三个的情况,只要看第三大元素是否被赋值即可。
class Solution {
public int thirdMax(int[] nums) {
long a = Long.MIN_VALUE, b = Long.MIN_VALUE, c = Long.MIN_VALUE;
for (int num : nums) {
if (num > a) {
c = b;
b = a;
a = num;
} else if (a > num && num > b) {
c = b;
b = num;
} else if (b > num && num > c) {
c = num;
}
}
return c == Long.MIN_VALUE ? (int) a : (int) c;
}
}
总结
针对这么一道普通的题目,我们可以想出三四种不同的解法,说明寻找第X大的数确实是一个经典的问题。同时这些解法也值得我们来反复学习。