寻找第X大的数

494 阅读3分钟

小知识,大挑战!本文正在参与“程序员必备小知识”创作活动。

介绍

这是一个生活中很常见的问题,我们往往不是找最大或者最小的值,而是去找第x大的值(比如第二或者第三大)。本文就以寻找第三大的数为例来看看这个问题的解法。

例题如下:

给你一个非空数组,返回此数组中 第三大的数 。如果不存在,则返回数组中最大的数。

示例 1:

输入:[3, 2, 1]
输出:1
解释:第三大的数是 1 。

示例 2:

输入:[1, 2]
输出:2
解释:第三大的数不存在, 所以返回最大的数 2 。

示例 3:

输入:[2, 2, 3, 1]
输出:1
解释:注意,要求返回第三大的数,是指在所有不同数字中排第三大的数。
此例中存在两个值为 2 的数,它们都排第二。在所有不同数字中排第三大的数为 1 。

注意到示例3,说明这个题是要求严格的第三大的数。

排序

从大到小排序后直接取第三个元素,这里不多赘述。

最大堆

这是我首先想到的方法,即使用大根堆这种数据结构,线性地遍历一遍数组并存入数据,然后从顶部依次推出三个元素,第三个即为答案。这种方法复杂度应该是O(nlogn)。

但要注意这里有三个坑:

  1. 由于是要求严格的第三大数,所以需要一个set来去重。
  2. 要注意若没有3个不重复的数,在弹出大根堆顶端元素三次时会报错。
  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();
    }
}

一次遍历

还能有更快的方法,维护三个变量,分别来存储前三大的元素,每次遍历新元素的时候分类讨论:

  1. 若该元素比最大的还大,则它顶替最大元素的位置,原来的最大元素到第二大元素,原来的第二大元素到第三大元素,原来的第三大元素舍弃;
  2. 若该元素介于第二大与最大数之间,则它顶替第二大元素位置,原来的第二大元素到第三大元素,原来的第三大元素舍弃;
  3. 若该元素介于第三大与第二大数之间,则它顶替第三大元素位置,原来的第三大元素舍弃。

除此之外,为了避免范围问题(某个元素刚好就是 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大的数确实是一个经典的问题。同时这些解法也值得我们来反复学习。