【经典专题】经典中的经典——TopK问题

news/2024/5/19 22:35:37 标签: 算法, 面试, , 快速排序

问题引入

请找出一数据中,最小/最大的k个数。

题目描述非常简单,你有多少种思路去实现它呢?

 
 

解法1——朴素排序

首先可以想到一种非常朴素的思路:将数据从小到大进行排序,取其前k个数即可

不多赘述,直接看代码:

class Solution {
    public int[] getLeastNumbers(int[] arr, int k) {
    	// 排序
        Arrays.sort(arr);
        // 取前k个数
        int[] res = new int[k];
        for (int i = 0; i < k; i++) {
            res[i] = arr[i];
        }
        return res;
    }
}

分析:时间复杂度 O(nlogn) ,即排序的开销。不难想到一个问题,我们只需要得到最小的k个数,却没要求这k个数、以及其他n-k个数内部也要有序啊——这种思路似乎进行了多余的排序操作。

 
 

解法2——小顶

接着,我们想到了一个关键的数据结构——(heap)

我们把所有数据放到小顶中,然后弹出k个数不就行了吗?

class Solution {
    public int[] getLeastNumbers(int[] arr, int k) {
        PriorityQueue<Integer> heap = new PriorityQueue<>();
        // 所有数据入
        for (int n : arr) {
            heap.offer(n);
        }
        // 弹出k个数
        int[] res = new int[k];
        int index = 0;
        while (index < k) {
            res[index++] = heap.poll();
        }
        return res;
    }
}

分析:这种思路其实和朴素排序的思路没什么不同,对于每个元素,入的开销都是O(logn),因此时间复杂度依旧是 O(nlogn) 。这种思路反而更糟糕,因为这还多使用了 O(n)空间。

 
 

解法3——大顶

接下来,才是的正确使用方式 >_< !

我们维护一个大小为k的大顶,将数据依次入,当的大小超过k时,便弹出一个多出的元素;这个弹出的元素是当前中的最大值,它永远不可能包含在最小的k个元素之中;最终中的k个元素即为所有数据中最小的k个元素。

class Solution {
    public int[] getLeastNumbers(int[] arr, int k) {
        PriorityQueue<Integer> heap = new PriorityQueue<>((o1, o2) -> o2 - o1);
        for (int n : arr) {
            heap.offer(n);
            if (heap.size() > k) {
                heap.poll();
            }
        }
        int[] res = new int[k];
        int index = 0;
        while (!heap.isEmpty()) {
            res[index++] = heap.poll();
        }
        return res;
    }
}

还可以进行进一步的优化:如果当前元素已经大于等于顶元素的话,那么就直接跳过,反正入的都会是它。

class Solution {
    public int[] getLeastNumbers(int[] arr, int k) {
        if (arr.length == 0 || k == 0) {
            return new int[0];
        }
        PriorityQueue<Integer> heap = new PriorityQueue<>((o1, o2) -> o2 - o1);
        for (int n : arr) {
            if (heap.size() < k) {
                heap.offer(n);
                continue;
            }
            if (n < heap.peek()) {
                heap.poll();
                heap.offer(n);
            }
        }
        int[] res = new int[k];
        int index = 0;
        while (!heap.isEmpty()) {
            res[index++] = heap.poll();
        }
        return res;
    }
}

分析:相比于小顶,大顶优化的本质是什么呢?的大小固定为k,而无需装入所有n个元素,因此入的开销降为 O(logk),总的时间复杂度为 O(nlogk) 。另外,空间复杂度也由 O(n) 降为 O(k)

 
 

解法4——快速排序

这种思路非常巧妙,也需要有着扎实的基础知识,下面跟着思路体会一下。

本题的要求是:找到左右数据中最小的k个数。

也就等价为:将数据分为前后两组,前面的一组数值较小,后面的一组数值较大,但在这两组的内部并不要求有序。

快速排序的思想是:将数据分为前后两组,前面的一组全部小于基准,后面的一组全部大于基准,但在这两组的内部并不要求有序。

受此启发,得到以下的算法思路:

1)对数据进行一次快速排序,最终基准值(left == right)落在的下标位置为 mid

2)此时基准值的位置,就是整体排序完成后的最终位置(这需要你对快速排序的理解比较深刻);

3)如果 k == mid ,则说明 arr[k] 即为第 k+1 小的数字,那么前k个数字即为最小的k个数字;

4)如果 k < mid ,则说明第 k+1 小的数字在左侧数组中,接着递归左侧数组

5)如果 k > mid ,则说明第 k+1 小的数字在右侧数组中,接着递归右侧数组

看代码吧:

class Solution {
    public int[] getLeastNumbers(int[] arr, int k) {
        if (arr.length == k) {
            return arr;
        }
        return quickSort(arr, 0, arr.length - 1, k);
    }

    private int[] quickSort(int[] arr, int L, int R, int k) {
        int left = L;
        int right = R;
        int temp = arr[left];
        while (left < right) {
            while (left < right && arr[right] >= temp) {
                right--;
            }
            arr[left] = arr[right];
            while (left < right && arr[left] <= temp) {
                left++;
            }
            arr[right] = arr[left];
        }
        arr[left] = temp;
        if (k < left) {
            return quickSort(arr, L, left - 1, k);
        }
        if (k > left) {
            return quickSort(arr, left + 1, R, k);
        }
        return Arrays.copyOf(arr, k);
    }
}

分析:每次都会根据基准的下标位置和k进行比较,并以此为依据进行递归,每次需要排序的部分都会减半,一个等比数列求和即可得到时间复杂度 O(n) 。空间复杂度即为递归深度 O(logn)

 
 

分析总结

1)快速排序思想的使用场景:将数据按某个特征分为两部分,一部分在前,一部分在后,但在这两部分的内部不考虑顺序。

2)使用的思路,时间复杂度 O(nlogk) ,使用快速排序的思路,时间复杂度O(n)

3)快速排序的思路优于的思路吗?从时间复杂度上来看的确如此,但是快速排序的思路有着空间上的局限性:可以处理以流的形式到来的大量数据,而快速排序则要求先存储下来所有的数据;当内存不够用的时候,反而是解决TopK问题的最优解。

 
 
 
 
 
 
 
 
 
 
 
 

E N D END END


http://www.niftyadmin.cn/n/1329883.html

相关文章

【剑指offer】把数字翻译成字符串+礼物的最大价值+n个骰子的点数总和

&#x1f525;题目 给定一个数字&#xff0c;我们按照如下规则把它翻译为字符串&#xff1a;0 翻译成 “a” &#xff0c;1 翻译成 “b”&#xff0c;……&#xff0c;11 翻译成 “l”&#xff0c;……&#xff0c;25 翻译成 “z”。一个数字可能有多个翻译。请计算给出的数字…

【剑指offer】数字序列中某一位的数字

&#x1f525;题目 数字序列为&#xff1a;01234567891011121314151617181920212223242526272829… 输入&#xff1a;13 输出&#xff1a;1 输入&#xff1a;200 输出&#xff1a;0 ☘️解析 比较有难度的数学问题。请耐心看完下面的讲解 >_< &#xff01; 首先&…

【剑指offer】1~n 整数中 1 出现的次数

&#x1f525;题目 输入一个整数n&#xff0c;求1&#xff5e;n这n个整数的十进制表示中1出现的次数。 输入&#xff1a;12 输出&#xff1a;5 解释&#xff1a;1&#xff5e;12这些整数中包含1 的数字有1、10、11和12&#xff0c;1一共出现了5次。 ☘️解析 这是一道比较困…

【剑指offer】股票的最大利润

&#x1f525;题目 假设把某股票的价格按照时间先后顺序存储在数组中&#xff0c;请问买卖该股票一次可能获得的最大利润是多少&#xff1f; 输入&#xff1a;[7, 1, 5, 3, 6, 4] 输出&#xff1a;5 解释&#xff1a;第2天买&#xff0c;第5天卖&#xff0c;获得最大利润 6 -…

【剑指offer】最长不含重复字符的子字符串+和为s的连续正数序列

&#x1f525;题目 请从字符串中找出一个最长的不包含重复字符的子字符串&#xff0c;计算该最长子字符串的长度。 输入&#xff1a;“pwwkew” 输出&#xff1a;3 ☘️解析 滑动指针的核心为&#xff1a;以右指针为驱动&#xff0c;拖着左指针向前走——右指针主动移动&am…

【经典专题】数组中出现1次/2次的数字——垂直方向的位运算

问题引入 有一个数组&#xff1a;nums [2, 2, 3, 3, 6, 6] 。 它具有怎样的性质呢&#xff1f;所有元素异或和为0。 这个位运算好像很简单&#xff1f;别着急&#xff0c;接下来&#xff0c;我们将要把位运算发挥到极致。 情境壹——num出现一次&#xff0c;其余元素出现两…

【剑指offer】数组中出现1次的数字+数组中出现2次的数字

&#x1f525;题目 一个数组nums里除两个数字之外&#xff0c;其他数字都出现了两次。找出这两个数字。 输入&#xff1a;nums [1, 2, 10, 4, 1, 4, 3, 3] 输出&#xff1a;[2,10] 或 [10,2] ☘️解析 此时&#xff0c;我们有些蒙了。因为数组中存在两个只出现一次的数字&…

【剑指offer】翻转单词顺序输出句子

&#x1f525;题目 输入一个英文句子&#xff0c;翻转句子中单词的顺序&#xff0c;但单词内字符的顺序不变。 注意&#xff1a;句子两侧可能会有多余的空格&#xff1b;两个单词之间可能不止一个空格。 输入&#xff1a;“the sky is blue” 输出&#xff1a;“blue is sky…