手撕:TopK问题--1000个学生里面求成绩最高的10个

该题可归类为TopK问题,即求n个数据中的前k个数据,参考:

该问题需要了解一些前置知识,例如快排和堆排序,参考:

方案一:全局排序

复杂度为 O ( n log ⁡ n ) O(n \log n) O(nlogn)$

对n个数据进行快速排序,代码示例:

public static int[] topK_globalSort(int[] arr, int k) {
    // 全局排序,采用快排
    Arrays.sort(arr);
    // 输出topK
    return Arrays.copyOfRange(arr, arr.length - k, arr.length);
}

方案二:局部排序

复杂度为 O ( n k ) O(nk) O(nk)

采用冒泡排序,但只冒泡k次,剩下的n-k个数据不排序,代码示例:

public static int[] topK_localSort(int[] arr, int k) {
    // 局部排序,采用冒泡排序
    for (int i = 0; i < k; i++) {
        for (int j = i + 1; j < arr.length; j++) {
            if (arr[i] < arr[j]) {
                int temp = arr[i];
                arr[i] = arr[j];
                arr[j] = temp;
            }
        }
    }

    // 输出topK
    return Arrays.copyOfRange(arr, 0, k);
}

方案三:堆

复杂度为 O ( n log ⁡ k ) O(n \log k) O(nlogk)

构建大小为k的堆,遍历所有数据,最后堆内的k个数据就是目标数据,思路类似于局部排序,不过每次遍历时不要求堆内的数据有序,代码示例:

public static int[] topK_heap(int[] arr, int k) {
    // 通过PriorityQueue(底层实现为小顶堆)构建小顶堆
    PriorityQueue<Integer> heap = new PriorityQueue<>(k);
    for (int num : arr) {
        if (heap.size() < k) {
            // 堆中元素不足k个,直接添加
            heap.add(num);
        }

        if (heap.size() == k && num > heap.peek()) {
            // 堆中元素已满,且当前元素大于堆顶元素(堆中的最小值),替换堆顶元素
            heap.poll();
            heap.add(num);
        }
    }

    // 输出堆中的元素
    return heap.stream().mapToInt(Integer::intValue).toArray();
}

方案四:减治法

复杂度为 O ( n ) O(n) O(n)

先区分一下分治法和减治法:

  • 分治法,大问题分解为小问题,小问题都要递归各个分支,例如:快速排序
  • 减治法,大问题分解为小问题,小问题只要递归一个分支,例如:二分查找,随机选择

减治法的目的是尽可能少处理一些数据,应用到TopK问题中,就可以理解为快排的一种变体,只不过TopK是希望求出arr[1,n]中最大的k个数,那如果找到了第k大的数,做一次partition,就等于一次性找到最大的k个数了。所以我们可以把快排改造成以下逻辑:

第一次partition,划分之后 i = partition(arr, 1, n):

  • 如果i大于k,则说明arr[i]左边的元素都大于k,于是只递归arr[1, i-1]里第k大的元素即可;
  • 如果i小于k,则说明说明第k大的元素在arr[i]的右边,于是只递归arr[i+1, n]里第k-i大的元素即可;

代码示例:

public static int[] topK_decreaseAndConquer(int[] arr, int k) {
    // 减治法,目的是找到第k个元素
    int low = 0;
    int high = arr.length - 1;
    while (low < high) {
        int pivot = partition(arr, low, high);
        if (pivot == k - 1) {
            break;
        } else if (pivot < k - 1) {
            low = pivot + 1;
        } else {
            high = pivot - 1;
        }
    }

    // 输出topK
    return Arrays.copyOfRange(arr, 0, k);
}

public static int partition(int[] arr, int low, int high) {
    int pivot = arr[low];
    while (low < high) {
        while (low < high && arr[high] <= pivot) {
            high--;
        }
        arr[low] = arr[high];
        while (low < high && arr[low] >= pivot) {
            low++;
        }
        arr[high] = arr[low];
    }
    arr[low] = pivot;
    return low;
}

测试:

public static void main(String[] args) {
    // 生成长度为n的数组, 值为随机数
    int n = 1000000;
    int[] arr = new int[n];
    for (int i = 0; i < arr.length; i++) {
        arr[i] = (int) (Math.random() * n);
    }
    int k = 100;

    // 打印各个方案的结果以及耗时
    long start = System.currentTimeMillis();
    //System.out.println("topK_globalSort: " + Arrays.toString(topK_globalSort(arr.clone(), k)));
    topK_globalSort(arr.clone(), k);
    System.out.println("topK_globalSort耗时: " + (System.currentTimeMillis() - start) + "ms");

    start = System.currentTimeMillis();
    //System.out.println("topK_localSort: " + Arrays.toString(topK_localSort(arr.clone(), k)));
    topK_localSort(arr.clone(), k);
    System.out.println("topK_localSort耗时: " + (System.currentTimeMillis() - start) + "ms");

    start = System.currentTimeMillis();
    //System.out.println("topK_heap: " + Arrays.toString(topK_heap(arr.clone(), k)));
    topK_heap(arr.clone(), k);
    System.out.println("topK_heap耗时: " + (System.currentTimeMillis() - start) + "ms");

    start = System.currentTimeMillis();
    //System.out.println("topK_decreaseAndConquer: " + Arrays.toString(topK_decreaseAndConquer(arr.clone(), k)));
    topK_decreaseAndConquer(arr.clone(), k);
    System.out.println("topK_decreaseAndConquer耗时: " + (System.currentTimeMillis() - start) + "ms");
}

结果为:

topK_globalSort耗时: 91ms
topK_localSort耗时: 74ms
topK_heap耗时: 13ms
topK_decreaseAndConquer耗时: 8ms

可以看到方案三方案四的性能明显优于方案一方案二

  • 28
    点赞
  • 15
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值