Top K问题的解决方案

问题定义

Top K 问题指的是从一组无序数据中找到前 K 大(也可以是前 K 小)的数。这个问题是十分经典的问题,不论在面试中还是实际开发中,都非常典型。

解决方案

对于 TOP K 问题,常见的解决方案有以下三种:

  • 排序
  • 快速选择

下面以剑指Offer 40.最小的K个数为例,实现上述的解决方案。

排序

排序是最容易想到的方法,将数据排序之后,取出最小的K个数即为所求。代码如下:

public int[] getLeastNumbers(int[] arr, int k) {
    Arrays.sort(arr);
    int[] res = new int[k];
    for (int i = 0; i < k; i++) {
        res[i] = arr[i];
    }
    return res;
}

Java语言中对于基本数据类型的数组排序使用的是快排,所以时间复杂度是O(nlogn)。
由于是对所有数据进行排序,所以时间复杂度太高,其实我们只需要 TOP K,所以能不能不全局排序,只对局部进行排序?其实,可以使用冒泡排序,这样只需要冒 K 次泡就可以得到 TOP K 了。代码如下:

public int[] getLeastNumbers(int[] arr, int k) {
    int[] res = new int[k];
    for (int i = 0; i < k; i++) {
        int index = i;
        for (int j = arr.length - 1; j > i; j--) {
            if (arr[j] < arr[index]) {
                index = j;
            }
        }
        int tmp = arr[i];
        arr[i] = arr[index];
        arr[index] = tmp;
        res[i] = arr[i];
    }
    return res;
}

时间复杂度是O(N * K),将全局排序转化为局部排序,节省了资源,提高了效率。

前面是排序的解决方案,那么能不能不排序就得到 TOP K呢?这时就可以借助堆来实现。
该问题是求前 K 小的数,那么可以创建一个最大堆,这个堆的容量为 K ,
当元素数量少于 K 个时,直接加入堆,等于 K 个时,将当前元素和堆顶元素进行比较。如果当前元素小于堆顶元素,堆顶元素出堆,当前元素入堆,这样比较完所有数据,堆中元素即为前 K 小的数。代码如下:

public int[] getLeastNumbers(int[] arr, int k) {
    if (arr.length == 0 || k == 0) {
        return new int[0];
    }
    PriorityQueue<Integer> heap = new PriorityQueue<>(((o1, o2) -> arr[o2] - arr[o1]));
    for (int i = 0; i < arr.length; i++) {
        if (heap.size() < k) {
            heap.offer(i);
        } else if (heap.comparator().compare(i, heap.peek()) > 0){
            heap.poll();
            heap.offer(i);
        }
    }
    int[] res = new int[k];
    int index = 0;
    for (Integer integer : heap) {
        res[index++] = arr[integer];
    }
    return res;
}

对于求前 K 大的情况,只需要创建小根堆,比较情况相反即可。

快速选择

快速排序是选择一个基准值,小于基准值的在左面,大于基准值的在右面。那么对于 TOP K 问题,只需要基准值的位置为 K 就可以了,这样左边的是前 K 小的,右边是前 K 大的。代码如下:

public int[] getLeastNumbers(int[] arr, int k) {
        if (arr.length == 0 || k == 0) {
            return new int[0];
        }
        return quickSearch(arr, 0, arr.length - 1, k);
    }

private int[] quickSearch(int[] array, int start, int end, int k) {
    int index = partiton(array, start, end);
    if (index == k - 1) {
        return Arrays.copyOf(array, k);
    }
    return index > k - 1 ? quickSearch(array, start, index - 1, k) : quickSearch(array, index + 1, end, k);
}

private int partiton(int[] array, int start, int end) {
    int pivot = start;
    swap(array, pivot, end);
    int index = start - 1;
    for (int i = start; i < end; i++) {
        if (array[i] < array[end]) {
            index++;
            if (index != i) {
                swap(array, index, i);
            }
        }
    }
    swap(array, index + 1, end);
    return index + 1;
}

private void swap(int[] array, int i, int j) {
    int tmp = array[i];
    array[i] = array[j];
    array[j] = tmp;
}

这是一个典型的减治算法,递归内的两个分支,最终只会执行一个,它的时间复杂度是O(n)。

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

相关题目

下面是 LeetCode 上与 TOP K 相关的问题:

  • LeetCode 347. 前 K 个高频元素
  • LeetCode 215. 数组中的第K个最大元素
  • LeetCode 692. 前K个高频单词
  • LeetCode 973. 最接近原点的 K 个点
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值