海量数据-两种方法解决top k问题

假如提供一百万个数据(甚至更大)的无序数组,如何从中获取最大的k个元素?

最容易想到的是先降序排序然后获取前k个元素,那假设我们用最常用的O(nlogn)级别的排序算法,要获取top k,时间成本也就是排序成本了,为O(nlogn),而这里的n是百万级别的,那能不能更快一点呢?

最小堆

假设我们设定一个最小堆,并固定这个最小堆的最大大小为k,那么任何一时刻堆顶元素都是这个堆中的第k大的数据(也是最小的数据)。来一个数据就和堆顶元素比较,如果比堆顶元素大就替换堆顶元素,并堆化。最后当所有数据都走一遍后,堆内的元素就是所有数据中最大的k个元素,而堆顶元素就是第k大的元素。

那么我们来分析一下它在时间复杂度上优化了多少吧。首先所有数据都要走一遍比较,就是n,其次维护了一个大小为k的堆,也就是说堆化的时间复杂度为O(log k),那么总的时间复杂度就是O(nlogk) ,乍一看和先排序的时间复杂度O(nlogn)也没差多少,甚至说是一个衡量级别的,但毕竟这里说的是海量数据,n可能很大,k可能比较小,那么时间上就快很多了。

它的另一个好处是空间复杂度也仅为O(k),不需要额外维护一个O(n)级别的海量数据存储。

代码实现:

public static List<Integer> topKByHeap(int[] arr, int k) {
    if (arr == null || arr.length == 0 || k <= 0 || k > arr.length) {
        return new ArrayList<>();
    }
    PriorityQueue<Integer> minHeap = new PriorityQueue<>();
    for (int i = 0; i < k; i++) {
        minHeap.add(arr[i]);
    }
    for (int i = k; i < arr.length; i++) {
        if (arr[i] > minHeap.peek()) {
            minHeap.poll();
            minHeap.add(arr[i]);
        }
    }
    return new ArrayList<>(minHeap);
}

快排思想

海量数据-两种方法解决top k问题

O(nlogk)好像还是有点不够看,能不能直接优化成O(n)呢?

我们知道快排的思想是每次随机选取一个pivot把数组划分成相对有序的两部分,那假设第一次随机选取pivot并分区之后,后面这部分的元素数量就正好为k,不就说明后面这部分数据就是最大的k个元素,那最好时间复杂度岂不是O(logn)

海量数据-两种方法解决top k问题

那么问题就成为了选中一个pivot,将数组分为前后两部分,后面部分元素的数量为k的问题。

虽然很难一次就中,但我们在一次分区后,通过比较k和后面部分元素的数量就知道了,后面部分的元素是分多了还是分少了。

如果后面的元素数量分多了,就继续在后面这部分数据中随机选取pivot,重复步骤。如果后面的元素分少了,则往前选取pivot,就形成了一种类似二分查找的步骤。

那么我们来分析一下它的时间复杂度,我们知道快速排序每次分区的时间复杂度就是log(n),那么我们假设每次分区前后两部分的数量相近,那么时间复杂度就是log(n)+log(n/2)+log(n/4)+...,取极限为log(2n),即log(n)级别,是不是比一开始的log(nlogn)快很多?特别是在海量数据的情况下。

代码实现:

public static List<Integer> topK(int[] arr, int k) {
    if (arr == null || arr.length == 0 || k <= 0 || k > arr.length) {
        return new ArrayList<>();
    }
    // 如果是 BottomK 的话就不需要单独处理 k==n 情况了,因为划分是小于等于放在左
    // topK 的话如果是将大于的 k 个放在右边,那么 pivot 就变成了-1,会形成死循环。
    if (k == arr.length) {
        return IntStream.of(arr).boxed().collect(Collectors.toList());
    }
    int low = 0;
    int high = arr.length - 1;
    int index = QuickSort.partition(arr, low, high);
    while (index != arr.length - k - 1) {
        if (index > arr.length - k - 1) {
            index = QuickSort.partition(arr, low, index - 1);
        } else {
            index = QuickSort.partition(arr, index + 1, high);
        }
    }
    List<Integer> list = new ArrayList<>(k);
    for (int i = index + 1; i < arr.length; i++) {
        list.add(arr[i]);
    }
    System.out.println(Arrays.toString(arr));
    return list;
}

这里的Quicksort得自己实现了,这里不作为重点,可以参考十大排序算法与Java实现

如果是找第K大的元素呢?

作为拓展,讨论一下如果要找第K大的数据,仍然可以用这两个方法:

  • 对于最小堆则是操作一遍之后直接取堆顶元素。
  • 对于快排思想,则是找一个pivot,使得分区之后它正好处于第arr.length-k+1的位置上。

原创文章,作者:彭晨涛,如若转载,请注明出处:https://www.codetool.top/article/%e6%b5%b7%e9%87%8f%e6%95%b0%e6%8d%ae-%e4%b8%a4%e7%a7%8d%e6%96%b9%e6%b3%95%e8%a7%a3%e5%86%b3top-k%e9%97%ae%e9%a2%98/

(0)
彭晨涛彭晨涛管理者
上一篇 2020年4月4日
下一篇 2020年4月4日

相关推荐

  • leetcode456-132模式

    原题 给定一个整数序列:a1, a2, ..., an,一个132模式的子序列 ai, aj, ak 被定义为:当 i < j < k 时,ai < ak &lt…

    算法 2020年1月30日
    0150
  • 剑指offer40-最小的k个数

    原题(来源Leetcode) 输入整数数组 arr ,找出其中最小的 k 个数。例如,输入4、5、1、6、2、7、3、8这8个数字,则最小的4个数字是1、2、3、4。 示例 1: …

    算法 2020年3月20日
    0390
  • leetcode365-水壶问题

    原题 有两个容量分别为 x升 和 y升 的水壶以及无限多的水。请判断能否通过使用这两个水壶,从而可以得到恰好 z升 的水? 如果可以,最后请用以上水壶中的一或两个来盛放取得的 z升…

    算法 2020年3月21日
    0190
  • 蓝桥杯试题-黑色星期五

    原题 资源限制 时间限制:1.0s 内存限制:512.0MB 问题描述  有些西方人比较迷信,如果某个月的13号正好是星期五,他们就会觉得不太吉利,用古人的说法,就是“诸事不宜”。…

    算法 2020年2月28日
    0120
  • leetcode40-组合总和II

    原题 给定一个数组 candidates 和一个目标数 target ,找出 candidates 中所有可以使数字和为 target 的组合。 candidates 中的每个数字…

    算法 2020年5月2日
    0730
  • leetcode4-寻找两个有序数组的中位数

    这道题我没想出符合条件的思路 原题 给定两个大小为 m 和 n 的有序数组 nums1 和 nums2。 请你找出这两个有序数组的中位数,并且要求算法的时间复杂度为 O(log(m…

    算法 2020年1月9日
    0620
  • leetcode83-删除排序链表中的重复元素

    今天打算多刷几道算法题 原题 给定一个排序链表,删除所有重复的元素,使得每个元素只出现一次。 示例 1: 输入: 1->1->2 输出: 1->2 示例 2: 输入: 1->1->…

    算法 2020年6月16日
    01350
  • leetcode1413-逐步求和得到正数的最小值

    原题 给你一个整数数组 nums 。你可以选定任意的 正数 startValue 作为初始值。 你需要从左到右遍历 nums 数组,并将 startValue 依次累加上 nums…

    算法 2020年6月21日
    03010
  • leetcode279-完全平方数

    原题 给定正整数 n,找到若干个完全平方数(比如 1, 4, 9, 16, …)使得它们的和等于 n。你需要让组成和的完全平方数的个数最少。 示例 1: 输入: n = 12 输出…

    2019年12月11日
    0150
  • leetcode509-斐波那契数

    原题 斐波那契数,通常用 F(n) 表示,形成的序列称为斐波那契数列。该数列由 0 和 1 开始,后面的每一项数字都是前面两项数字的和。也就是: F(0) = 0,   F(1) …

    算法 2020年1月21日
    0120

发表回复

登录后才能评论