215. 数组中的第K个最大元素 剑指 Offer 40. 最小的k个数 347. 前 K 个高频元素(快排、优先队列)【M】

news/2024/5/19 22:17:17 标签: java, leetcode, 快速排序

leetcode-cn.com/problems/kth-largest-element-in-an-array/">215. 数组中的第K个最大元素 & 剑指 Offer 40. 最小的k个数

这两个题本质上是一个题,典型的topK问题,一定要掌握

这个题的考点:多种解法(快排、优先队列),快排的实现
在这里插入图片描述

可以想到:暴力解,即将数组sort一下,然后获取index=k-1的数字,就是topk——时间复杂度为O(NlogN)

1. 快排

首先,我们知道Java内部的sort就是用了改进版的快排。那么快排的主要思想就是:分而治之,核心方法,就是partition,每一次调用partition之后,都能确定一个结点的位置index(之后不会再变了),而该结点之前都是小于的数,该结点之后都是大于的数

那么我们可以利用这个index来判断是否已经得到了topk个数:

  • 如果index>k-1,那么说明topk出现在index之前,对index再次进行partition;
  • 如果index<k-1,那么说明index之前的就是topk的一部分,还需要再后面再找几个,所以要对index+1~right的范围再次进行partition

——如此循环,最后得到index=k-1,那么k-1及其之前的数就是topk

java">class Solution {
    private int partition(int[] nums, int left, int right){             // 划分
        int pivot = nums[left];
        while(left < right){
            while(left < right && nums[right] < pivot) right--;			// 降序排列
            if(left < right) nums[left++] = nums[right];
            while(left < right && nums[left] > pivot) left++;
            if(left < right) nums[right--] = nums[left];
        }
        nums[left] = pivot;             // 将基准值插入到数组中
        return left;
    }
    private void quickSort(int[] nums, int left, int right, int k){
        if(left > right) return;
        int index = partition(nums, left, right);
        if(index + 1 > k) quickSort(nums, left, index - 1, k);
        else if(index + 1 < k ) quickSort(nums, index + 1, right, k); 
    }
    public int findKthLargest(int[] nums, int k) {
        quickSort(nums, 0, nums.length - 1, k);		// 直接调用
        return nums[k - 1];
    }
}

稍微优化一下,不需要使用递归方法:

java">class Solution {
    private int partition(int[] nums, int left, int right){             // 划分
        int pivot = nums[left];
        while(left < right){
            while(left < right && nums[right] < pivot) right--;
            if(left < right) nums[left++] = nums[right];
            while(left < right && nums[left] > pivot) left++;
            if(left < right) nums[right--] = nums[left];
        }
        nums[left] = pivot;             // 将基准值插入到数组中
        return left;
    }
    public int findKthLargest(int[] nums, int k) {
        int left = 0, right = nums.length - 1;
        while(true){
            int index = partition(nums, left, right);
            if(index == k - 1) break;
            else if(index > k - 1) right = index - 1;
            else left = index + 1;
        }
        return nums[k - 1];
    }
}

当然,这个partition还有多种写法:

java">private int partition(int[] nums, int left, int right){
    int pivot = nums[left];
    int j = left;
    for(int i = left + 1; i <= right; i++){
        if(nums[i] > pivot){			// 将所有大于基准值的数字都和前面小于基准值的数字交换
            j++;
            swap(nums, j, i);
        }
    }
    swap(nums, left, j);			// 将最后一个大于基准值的数字和基准值交换位置,此时的位置就是划分之后的结果
    return j;
}
private void swap(int[] nums, int i, int j){
    int temp = nums[i];
    nums[i] = nums[j];
    nums[j] = temp;
}

——注意如果是选择固定的基准值时间性能并不好,因为存在极端测试样例,所以需要随机选择基准值:

java">private static Random random = new Random(System.currentTimeMillis());
private int partition(int[] nums, int left, int right){             // 划分
    if (right > left) {			// 必须不超过范围
        int randomIndex = left + 1 + random.nextInt(right - left);		// 选择随机index数
        int temp = nums[randomIndex];
        nums[randomIndex] = nums[left];
        nums[left] = temp;
    }
    int pivot = nums[left];
	....
}

时间复杂度分析:快排:T(N)=2T(N/2) + N,根据主定理,得到:O(NlogN),而此时每次都只是查找partition之后的一半,其余一半并不考虑,所以T(N)=T(N/2)+N,根据主定理,得到:O(N)

空间复杂度为O(1)

2. 优先队列

要获取第k大的数,还可以用优先队列——就是排序以后后半部分最小的那个元素

用最小堆(堆顶就是堆中最小的数),堆的大小限制为k:遍历数组

  1. 如果堆未满,就不断添加结点(最小堆始终会维护堆顶是最小的数)
  2. 如果堆满了,那么,结点和堆顶做比较,如果小于堆顶,就放弃;如果大于堆顶就插入,堆顶被抛出

——遍历结束后,堆中存放的就是最大的k个数,且堆顶就是第k大的数(就是其中最小的数)

java">class Solution {
    public int findKthLargest(int[] nums, int k) {
        PriorityQueue<Integer> heap = new PriorityQueue<>();
        for(int i = 0; i < k; i++){
            heap.add(nums[i]);
        }
        for(int i = k; i < nums.length; i++){
            if(nums[i] > heap.peek()){
                heap.poll();
                heap.add(nums[i]);
            }
        }
        return heap.peek();
    }
}

时间复杂度为O(N),空间复杂度为O(N)

当然也可以用最大堆:存在两种方法:

  • 将所有结点均加入到最大堆中,然后弹出k-1个结点,那么此时的栈顶就是第k大的结点;

  • 可以反着来,第k个大的数 = 第n-k+1个小的数,那么就用最大堆存放n-k+1个数,然后栈顶就是需要的数

    ——最大堆和最小堆可以根据实际情况进行转换,转换的分界线在 k = n - k,看哪个需要的堆的空间小。

具体的实现见参考的题解

参考:

  1. https://leetcode-cn.com/problems/kth-largest-element-in-an-array/solution/partitionfen-er-zhi-zhi-you-xian-dui-lie-java-dai-/

leetcode-cn.com/problems/zui-xiao-de-kge-shu-lcof/">剑指 Offer 40. 最小的k个数

在这里插入图片描述

本题和上题一样,只不过这次需要将前面的数字全部输出,本质没有变化。

下面复习一遍:

  1. 快排
java">class Solution {
    private int partition(int[] nums, int left, int right){
        int pivot = nums[left];
        while(left < right){
            while(left < right && nums[right] > pivot) right--;
            if(left < right) nums[left++] = nums[right];
            while(left < right && nums[left] < pivot) left++;
            if(left < right) nums[right--] = nums[left];
        }
        nums[left] = pivot;
        return left;
    }
    public int[] getLeastNumbers(int[] arr, int k) {
        if(k == 0 || arr.length == 0) return new int[0];
        int left = 0, right = arr.length - 1;
        while(true){
            int index = partition(arr, left, right);
            if(index == k - 1) break;
            else if(index > k - 1) right = index - 1;
            else left = index + 1;
        }
        return Arrays.copyOf(arr, k);
    }
}
  1. 优先队列

由于要求的是最小的k个数,那么需要用最大堆

java">class Solution {
    public int[] getLeastNumbers(int[] arr, int k) {
        if(k == 0 || arr.length == 0) return new int[0];
        PriorityQueue<Integer> heap = new PriorityQueue<>(k, (a, b) -> (b - a));
        for(int i = 0; i < k; i++){
            heap.add(arr[i]);
        }
        for(int i = k; i < arr.length; i++){
            if(arr[i] < heap.peek()){
                heap.poll();
                heap.add(arr[i]);
            }
        }
        int[] res = new int[k];
        for(int i = 0; i < k; i++){
            res[i] = heap.poll();
        }
        return res;
    }
}

https://leetcode-cn.com/problems/zui-xiao-de-kge-shu-lcof/solution/3chong-jie-fa-miao-sha-topkkuai-pai-dui-er-cha-sou/还提出了一个二叉搜索树的解法,本质上和最大堆一样,可以调用库TreeMap,来维护一棵二叉搜索树,具体见题解,主要是对TreeMap的使用。

总结:

  1. 快排的模板需要记住,partition不能写错
  2. 最大堆最小堆虽然之间可以转换,但是常规来说:求最小的topk问题用最大堆;求最大的topk问题用最小堆

更新一个topk问题

leetcode-cn.com/problems/top-k-frequent-elements/">347. 前 K 个高频元素

在这里插入图片描述

本题本质上还是topk问题,只不过解题流程上稍微需要修改

主要还是要熟悉哈希和优先队列的常见方法,并能够加以使用

题目要求:统计数组中每个元素出现的频率,然后将出现次数最多的k个数获取出来。

  • 首先,我们需要统计元素出现的个数,由于元素的大小在int范围内均可,所以必须要用到哈希表——遍历一次数组,然后将数组中所有的数字出现的次数进行统计,放入哈希表中——key: 数字;value:出现次数
  • topk问题的解法是快排和优先队列,而快排是对数组进行排序,如果要用这个我们需要遍历哈希表将数字取出来并且按照key构建数组,然后根据value进行排序;如果是优先队列,我们需要构建的是最小堆,并且需要自定义比较器,然后遍历哈希表将topk数字获取出。

优先队列版本:

java">class Solution {
    public int[] topKFrequent(int[] nums, int k) {
        HashMap<Integer, Integer> store = new HashMap<>();
        for(int num: nums){                 // 统计数组中出现的次数
            if(store.containsKey(num)){
                store.put(num, store.get(num) + 1);
            }
            else{
                store.put(num, 1);
            }
        }
        PriorityQueue<Integer>queue = new PriorityQueue<>(new Comparator<Integer>(){        // 最小堆,需要重写比较器
            @Override
            public int compare(Integer o1, Integer o2){
                return store.get(o1) - store.get(o2);				// 比较的是出现的频率,通过调用哈希表来获得频率
            }
        });
        int i = 0;
        for(Integer key: store.keySet()){           // 哈希表遍历
            if(i < k){					// < k直接填充
                queue.add(key);
            }
            if(i >= k){
                int num = store.get(queue.peek());			// >=k 需要与堆顶进行比较,出现频率高才能进行替换
                if(num < store.get(key)){
                    queue.poll();
                    queue.add(key);
                }
            }
            i++;
        }
        int[] res = new int[k];					// 取出堆中元素
        for(int j = 0; j < k; j++){
            res[j] = queue.poll();
        }
        return res;
    }
}

收获:

哈希表的遍历方法:for(Integer key: map.keySet())

自定义堆的比较器:

java">PriorityQueue<Integer> queue = new PriorityQueue<>(new Comparator<Integer>(){
	@Override
    public int compare(Integer o1, Integer o2){
        return map.get(o1) - map.get(o2);
    }
});

快排版本:

java">class Solution {
    private int partition(HashMap<Integer, Integer>store, int[] nums, int left, int right){

        int pivot = nums[left];
        while(left < right){
            while(left < right && store.get(nums[right]) < store.get(pivot)) right--;
            if(left < right) nums[left++] = nums[right];
            while(left < right && store.get(nums[left]) > store.get(pivot)) left++;
            if(left < right) nums[right--] = nums[left];
        }
        nums[left] = pivot;
        return left;
    }
    public int[] topKFrequent(int[] nums, int k) {
        HashMap<Integer, Integer> store = new HashMap<>();
        for(int num: nums){                 // 统计数组中出现的次数
            if(store.containsKey(num)){
                store.put(num, store.get(num) + 1);
            }
            else{
                store.put(num, 1);
            }
        }
        int[] res = new int[store.size()];      
        int i = 0;
        for(Integer key: store.keySet()){           // 遍历哈希表,将所有的key写入一个数组中
            res[i++] = key; 
        }
        int left = 0, right = res.length - 1;
        while(true){
            int mid = partition(store, res, left, right);
            if(mid == k - 1) break;
            else if(mid > k - 1) right = mid - 1;
            else left = mid + 1;
        }
        return Arrays.copyOf(res, k);
    }
}

参考题解,还提出了一个 解法:桶排序(即用哈希表统计每个数字的出现次数后,自己再后构建一个哈希表),长度为nums.length+1的数组,每个index代表出现次数,所以最多是整个数组都是同一个数,那么最大出现次数为nums.length。然后遍历哈希表,根据出现次数进行index对应,如果出现相同的出现次数,就创建链表挂在index上。最后从后向前遍历获得k个结点即可

参考:

https://leetcode-cn.com/problems/top-k-frequent-elements/solution/leetcode-di-347-hao-wen-ti-qian-k-ge-gao-pin-yuan-/


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

相关文章

java -- java.lang.NoSuchMethodError的问题

让这个问题折腾了好长时间&#xff0c;最终在网上看到了一篇文章&#xff0c;解决了这个问题。 转自&#xff1a; http://timen-zbt.iteye.com/blog/1871152 在Java项目进行增量更新的时候经常会出现java.lang.NoSuchMethodError的问题&#xff0c;究其原因有很多&#xff1a…

java -- 将int 转为二进制,再提取二进制中每一位信息

大纲 将int 十进制变为二进制。将二进制&#xff08;n进制&#xff09;字符串变为十进制。 10进制变为3进制&#xff08;n进制&#xff09;字符串 前言 用二进制形式&#xff0c;表示事务的处理状态&#xff0c;是一个比较简单的做法&#xff0c;充分考虑了二进制的开关的…

151. 翻转字符串里的单词(字符串)【M】

151. 翻转字符串里的单词 这题主要就是考查字符串的解法&#xff0c;总体来说比较简单。 它规定了是不能利用额外空间&#xff0c;返回的字符串不算是额外空间&#xff0c;主要就是来限制使用库函数。 1. 双指针 由于不能用额外空间&#xff0c;所以我们需要在字符串上直接进…

编译器或者链接器会优化掉虚表吗

垃圾民科内容&#xff0c;勿读&#xff0c;请搜索devirtualization、LTO等等内容 引子 前一段时间和实验室的同学讨论虚表和RTTI相关的问题&#xff0c;由于我是编译器论调的拥泵&#xff0c;所以我信誓旦旦的说如果编译器发现虚表或者虚函数无用的话&#xff0c;会自动优化掉…

Xshell use

&#xfeff;&#xfeff;转自&#xff1a; http://www.th7.cn/system/lin/201410/72658.shtml (1)命令 ls——列出文件 ls 显示当前目录文件 ls -la 给出当前目录下所有文件的一个长列表&#xff0c;包括以句点开头的隐藏”文件 ls a* 列出当前目录下以字母a开头的所有文件 l…

DSM -- 进入linux文件系统

将DSM 设置为SSH可用。 在DSM中 &#xff1a;控制面板 --》 选择 终端机和SNMP --》 选中 启动ssh 将windows的SSH客户端启动。&#xff08;默认windows是关闭的&#xff09; 见 &#xff1a;http://www.blogbus.com/amazingxiu-logs/242929522.html 下载Xshell远程登陆工…

剑指 Offer 21. 调整数组顺序使奇数位于偶数前面(快排partition)【S】

剑指 Offer 21. 调整数组顺序使奇数位于偶数前面 做到这个题的时候&#xff0c;觉得很像是快排的partition的解法&#xff1a; 其实本质上也是&#xff0c;就是通过一次遍历&#xff0c;将奇偶数组做一个划分。 class Solution {public int[] exchange(int[] nums) {int left…

15. 三数之和 18. 四数之和 16. 最接近的三数之和(双指针) 454. 四数相加 II(哈希表)【M】

15. 三数之和 经典的多数之和题 这个题之前做过一遍&#xff0c;花了挺久的时间才搞懂&#xff0c;这次重新做能够快速想到思路&#xff0c;但是还是存在细节问题。这题的主要难点是如何去重 基本思路&#xff1a;首先&#xff0c;想暴力解&#xff0c;暴力解就是将所有可能…