TOP K问题及使用优先队列实现堆

TOP K是一个非常经典的算法问题,灵活运用了排序算法,也是一个高频面试点,不仅要掌握TOP K思想,还应该学会使用该思想解决实际问题,剑指Offer中的“最小的k个数”就是TOP K问题的实际运用

Top K问题是指在N个数的无序序列中找出最大的K个数或者最小的K个数,而其中的N往往都特别大,对于这种问题,最容易想到的办法当然就是先对其进行排序,然后直接取出最大或最小的K个元素就行了,但是这种方法往往是不可靠的,不仅时间效率低而且空间开销大,排序是对所有数都要进行排序,而实际上,这类问题只关心最大或者最小的K个数,并不关心整个序列是否有序,因此,排序实际上是浪费了的很多资源都是没必要的。这里介绍两种常见Top K算法:

  1. 类选择排序:对于正常的选择排序,需要进行N-1轮排序才可以得到一个有序序列,每一轮排序都可以得到整个序列中最大或最小的一个数,而求TOP K问题我们可以只进行k轮排序,便可以得到我们想要的k个数,这就是类选择排序,要求Top K就需要进行K次遍历整个序列,因此算法平均时间复杂度为O(N*K)

  2. 堆排序:假设求的是前K个最小数,那么首先在N个数中随机选择K个数维护一个大根堆(可以借助PriorityQueue优先队列来实现),然后再顺序的遍历剩余的N-K个数,将每个数都与堆顶元素进行比较,如果大于堆顶元素,直接跳过,如果小于堆顶元素,则说明该数有可能成为TOP K,将其与堆顶元素进行交换,并重新调整堆结构,依此步骤进行,当整个序列遍历完毕之后,大根堆中的K个元素就是TOP K了

    堆排序方法中,首先需要对K个元素进行建堆,时间复杂度为O(K);然后对剩下的N-K个数对堆顶进行比较及更新,最好情况下当然是都不需要调整了,那么时间复杂度就只是遍历这N-K个数的O(N-K),这样总体的时间复杂度就是O(K)+O(N-K)≈O(N),而在最坏情况下,N-K个数都需要更新堆顶,每次调整堆的时间复杂度为logK,因此此时时间复杂度就是NlogK了,总的时间复杂度就是O(K)+O(NlogK)≈O(NlogK)。空间复杂度是O(1)。值得注意的是,堆排序法提前只需读入K个数据即可,可以实现来一个数据更新一次,能够很好的实现数据动态读入并找出TopK

剑指 Offer 40. 最小的k个数
题目描述:
输入整数数组 arr ,找出其中最小的 k 个数。例如,输入4、5、1、6、2、7、3、8这8个数字,则最小的4个数字是1、2、3、4。
示例:
输入:arr = [3,2,1], k = 2
输出:[1,2] 或者 [2,1]

import java.util.Arrays;
import java.util.Comparator;
import java.util.PriorityQueue;
public class 最小的k个数 {
    /**
     * 方法一:类选择排序
     * 思路:选择排序每一轮排序之后都可以得到一个最小值
     *      由于只需要返回k个最小的数,所以无需进行完整的n-1趟排序
     *      只需要进行k趟排序,并最终输出数组的前k个数即可
     * @param arr
     * @param k
     * @return
     */
    public static int[] getLeastNumbers1(int[] arr, int k) {
        if (arr == null || arr.length == 0) {
            return new int[0];
        }
        // 外层循环,控制排序的趟数,如果是正常的选择排序,则需要进行n-1趟排序
        // top k 问题则只需要进行k趟排序即可
        for (int i = 0; i < k; i++) {
            int min = i;
            for (int j = i + 1; j < arr.length; j++) {
                if (arr[min] > arr[j]) {
                    min = j;
                }
            }
            if (i != min) {
                int temp = arr[i];
                arr[i] = arr[min];
                arr[min] = temp;
            }
        }
        // 创建一个新数组,将该数组的前k个元素依次赋值新数组
        int[] a = new int[k];
        for (int i = 0; i < k; i++) {
            a[i] = arr[i];
        }
        return a;
    }

    /**
     * 方法二:传统堆排序
     * 思路:借助优先队列PriorityQueue实现堆排序
     *      将数组中的元素依次入到优先队列中,默认就会构建一个小根堆
     *      再顺序的从堆中弹出k个元素赋值给数组即可
     * @param arr
     * @param k
     * @return
     */
    public static int[] getLeastNumbers2(int[] arr, int k) {
        PriorityQueue<Integer> queue = new PriorityQueue<>();
        for (int temp : arr) {
            queue.offer(temp);
        }
        int[] a = new int[k];
        for (int i = 0; i < k; i++) {
            a[i] = queue.poll();
        }
        return a;
    }

    /**
     * 方法三:TOP K思想(效率最高)
     * 思路:借助优先队列PriorityQueue构建一个大小为k的大根堆(由于是大根堆,需要传入一个比较器,并重写compare方法)
     *      然后从第k+1个元素开始顺序遍历数组,如果元素值小于大根堆堆定元素,则该元素入堆,并且堆顶元素移除
     *      最后再将大根堆中的元素依次赋值给数组即可
     * @param arr
     * @param k
     * @return
     */
    public static int[] getLeastNumbers3(int[] arr, int k) {
        if (k <= 0) {
            return new int[0];
        }
        PriorityQueue<Integer> queue = new PriorityQueue<>(k, new Comparator<Integer>() {
            @Override
            public int compare(Integer t1, Integer t2) {
                return t2 - t1;
            }
        });
        // 首先构建一个大小为k的大根堆
        for (int i = 0; i < k; i++) {
            queue.offer(arr[i]);
        }
        // 从第k+1个元素开始遍历,与堆定元素进行比较
        for (int i = k; i < arr.length; i++) {
            if (arr[i] < queue.peek()) {
                queue.poll();
                queue.offer(arr[i]);
            }
        }
        // 将堆中的元素依次赋值给数组
        int[] a = new int[k];
        for (int i = 0; i < k; i++) {
            a[i] = queue.poll();
        }
        return a;
    }
}

补充:PriorityQueue是一个优先队列,可以利用该队列的特性来进行堆排序,当往队列添加N个元素之后,底层实际采用的就是利用堆排序对这N个元素进行排序,从而构造一个小顶堆,因此当依次弹出队列中的元素时,就可以得到一个从小到大的排序序列;如果要利用它来构造一个大根堆,也就是当依次弹出队列中的元素时,希望得到一个从大到小的排序序列,那么就需要给优先队列传入一个比较器Comparator,并重写compare方法

你可能感兴趣的:(数据结构与算法,TOP,K,PriorityQueue,优先队列,堆排序,最小的K个数)