问题描述
问题描述:有 N (N>1000000)个数,求出其中的前 k 个最大的数(又被称为 topK 问题)
解决思路:
- 直接思路:将这 N 个数进行完全排序,再进行切片即可,复杂度跟所使用的排序算法有关,一般快排、堆排序和归并排序都能达到 O(n*logn) 的复杂度
- 使用优先队列,选择其中前 k 个数做为数据池,后面的 N-k 个数与这 k 个数进行比较,如果其大于其中任一一个数,则进行替换,算法复杂度为O(n*k)
- 使用冒泡排序:由于冒泡排序属于一次次将当前最大的值移至后面,因此只需要进行 k 次遍历即可,O(n*k)
- 使用小根堆:先对前 k 个元素建立小根堆,然后再对后面的 N - k 个元素与小根堆的堆顶进行比较,如果该值大与小根堆的堆顶,则替换堆顶,再进行一次堆的向下调整,该算法时间复杂度为 O(n*logk)
- 该算法优点:不需要一次性将数据全部加载进内存
- 快速排序:利用快速排序的分划函数找到分划位置k,则前面内容即为所求,时间复杂度O(n)
小根堆实现—java版本
- 小根堆的头结点的元素值为整个小根堆的最小值,只要当前值大于小根堆的头结点,即可以进行覆盖
没进行一次覆盖,均需要进行一次小根堆的向下调整,所以时间复杂度为O(n*logk),k 为需要获取的长度
public class HeapDemo {public static void main(String[] args) {// 生成一个超大的数组int[] arr = new int[10000];// 生成初始化参数Random random = new Random();for (int i = 0; i < 10000; i++) {arr[i] = random.nextInt(10000);}// 选取前100大的元素// 新建结果数组int[] res = new int[100];// 将原数组中的前100个元素拷贝进结果数组System.arraycopy(arr, 0, res, 0, 100);// 创建小根堆build(res, res.length);// 将数组中的后10000-100个元素与小堆顶进行比较,如果大于小根堆的头结点,则将该值赋给小根堆的头结点进行覆盖for (int i = 100; i < 10000; i++) {if (arr[i] > res[0]) {res[0] = arr[i];heapify(res, 0, res.length);}}System.out.println(Arrays.toString(res));}/*** 创建小根堆** @param arr 数组* @param len 数组中有效元素的个数*/public static void build(int[] arr, int len) {// 最后一个非叶子结点的索引int index = (len - 2) >> 1;// 从最后一个非叶子结点开始调整堆while (index >= 0) {// 调整非叶子结点建立小堆heapify(arr, index--, len);}}/*** 堆的向下调整** @param arr 数组* @param root 根结点的索引* @param len 数组中有效元素数量*/private static void heapify(int[] arr, int root, int len) {// 左子结点的位置int left = (root << 1) + 1;// 根结点、左右字节中最小值的元素索引int min = root;// 循环调整while (left < len) {// 左子结点比根结点小if (arr[left] < arr[min]) {min = left;}// 右子节点存在且比左子结点和根结点都小if (left + 1 < len && arr[left + 1] < arr[min]) {min = left + 1;}if (min != root) {// 需要进行结点交换和下一次的循环swap(arr, min, root);// 调整root结点的位置root = min;left = (root << 1) + 1;} else {// 说明根结点最小,已经满足小根堆break;}}}// 交换数组中两个元素的值private static void swap(int[] arr, int x, int y) {arr[x] ^= arr[y];arr[y] ^= arr[x];arr[x] ^= arr[y];}}
