热门标签 | HotTags
当前位置:  开发笔记 > 编程语言 > 正文

TopK问题(快排变形/堆/二叉搜索树/计数排序)leetcode347

目录 TopK问题描述1.快排变形1.1图解过程1.2快排实现2.大根堆(前K小)小根堆(前K大)2.1 大根堆和小根堆2.2利用PriorityQueue实现大根堆3.桶排序3.

目录

 TopK问题描述

1. 快排变形

1.1 图解过程

1.2 快排实现

2. 大根堆(前K小)/小根堆(前K大)

2.1  大根堆和小根堆

2.2 利用PriorityQueue 实现大根堆

3. 桶排序

3.1 图解桶排序

3.2 实现




 TopK问题描述

TopK问题,不管是求前K大/前K小/第K大/第K小等,都有4种不错的方法喔:

1. O(N):用快排变形最最最高效解决TopK问题

2. O(NlogK):大根堆(前K小)/小根堆(前K大)

3. O(N): 桶排序

以 leetcode 347 题为例 https://leetcode-cn.com/problems/top-k-frequent-elements/


1. 快排变形

用快排变形最最最高效解决TopK问题 O(N)

先通过快排切分排好第K小的数,根据快排切分的性质,它左边的K - 1个数都小于等于它,因此它以及它左边的数就是我们要找的前K小的数。


1.1 图解过程

假设我们现在对“6  1  2 7  9  3  4  5 10  8”这个10个数进行排序。

首先在这个序列中随便找一个数作为基准数。为了方便,就让第一个数6作为基准数吧。接下来,需要将这个序列中所有比基准数大的数放在6的右边,比基准数小的数放在6的左边。

方法其实很简单:分别从初始序列“6  1  2 7  9  3  4  5 10  8”两端开始“探测”。先从右往左找一个小于6的数,再从左往右找一个大于6的数,然后交换他们。这里可以用两个变量i和j,分别指向序列最左边和最右边。我们为这两个变量起个好听的名字“哨兵i”和“哨兵j”。刚开始的时候让哨兵i指向序列的最左边(即i=1),指向数字6。让哨兵j指向序列的最右边(即j=10),指向数字8。

首先哨兵j开始出动。因为此处设置的基准数是最左边的数,所以需要让哨兵j先出动,这一点非常重要(请自己想一想为什么)。哨兵j一步一步地向左挪动(即j--),直到找到一个小于6的数停下来。接下来哨兵i再一步一步向右挪动(即i++),直到找到一个数大于6的数停下来。最后哨兵j停在了数字5面前,哨兵i停在了数字7面前。

现在交换哨兵i和哨兵j所指向的元素的值。交换之后的序列如下。

        6  1  2  5  9 3  4  7  10  8

 到此,第一次交换结束。接下来开始哨兵j继续向左挪动(再友情提醒,每次必须是哨兵j先出发)。他发现了4(比基准数6要小,满足要求)之后停了下来。哨兵i也继续向右挪动的,他发现了9(比基准数6要大,满足要求)之后停了下来。此时再次进行交换,交换之后的序列如下。

        6  1  2 5  4  3  9  7 10  8

        第二次交换结束,“探测”继续。哨兵j继续向左挪动,他发现了3(比基准数6要小,满足要求)之后又停了下来。哨兵i继续向右移动,糟啦!此时哨兵i和哨兵j相遇了,哨兵i和哨兵j都走到3面前。说明此时“探测”结束。我们将基准数6和3进行交换。交换之后的序列如下。

        3  1 2  5  4  6  9 7  10  8

到此第一轮“探测”真正结束。剩下的以此类推即可。


1.2 快排实现

public static int[] topKFrequent(int[] nums, int k) {
if(nums.length == 0 || k == 0) return new int[0];
Map map = new HashMap <> (); // 记录元素出现的次数
for(int i = 0; i int[][] array = new int[2][map.size()]; // 第一行是原数组,第二行是出现的次数
int i = 0;
for(Map.Entry entry: map.entrySet()) {
array[0][i] = entry.getKey();
array[1][i++] = entry.getValue();
}
return quickSort(array, 0, array[0].length - 1, k - 1);
}
/**
* 快速排序
*
* @param originArray 原数组
* @param start 开始下标
* @param end 结束下标
* @param target 目标下标
* @return
*/
private static int[] quickSort(int[][] originArray, int start, int end, int target) {
// 每快排切分1次,找到排序后下标为j的元素,如果j恰好等于k就返回j以及j左边所有的数
int k = partition(originArray, start, end);
if(k == target) return Arrays.copyOfRange(originArray[0], 0, k + 1);
// 否则根据下标j与k的大小关系来决定继续切分左段还是右段。
return k > target ? quickSort(originArray, start, k - 1, target) : quickSort(originArray, k + 1, end, target);
}
/**
* 快排切分,返回下标j,使得比nums[j]小的数都在j的左边,比nums[j]大的数都在j的右边。
*
* @param originArray
* @param startIndex
* @param endIndex
* @return
*/
private static int partition(int[][] originArray, int startIndex, int endIndex) {
int freq = originArray[1][startIndex];
int start = startIndex;
int end = endIndex;
// 以起点作为基准
while(start while(start end--;
while(start = freq) // 从前往后找,找到比基准值小的值为止
start++;
if(start }
swap(originArray, start, startIndex); // 基准点和重合点交换位置
return start;
}
private static void swap(int[][] originArray, int start, int end) {
for(int i = 0; i int temp = originArray[i][start];
originArray[i][start] = originArray[i][end];
originArray[i][end] = temp;
}
}

2. 大根堆(前K小)/小根堆(前K大)

2.1  大根堆和小根堆

性质:每个结点的值都大于其左孩子和右孩子结点的值,称之为大根堆;每个结点的值都小于其左孩子和右孩子结点的值,称之为小根堆。如下图

我们对上面的图中每个数都进行了标记,上面的结构映射成数组就变成了下面这个样子

还有一个基本概念:查找数组中某个数的父结点和左右孩子结点,比如已知索引为i的数,那么

1.父结点索引:(i-1)/2(这里计算机中的除以2,省略掉小数)

2.左孩子索引:2*i+1

3.右孩子索引:2*i+2

所以上面两个数组可以脑补成堆结构,因为他们满足堆的定义性质:

大根堆:arr(i)>arr(2*i+1) && arr(i)>arr(2*i+2)

小根堆:arr(i)

2.2 利用PriorityQueue 实现大根堆

public static int[] topKFrequent1(int[] nums, int k) {
// 构造 HashMap。Key:nums 中的每个元素;Value:对应元素出现的次数(即频率)
HashMap store = new HashMap <> ();
for(Integer num: nums)
store.put(num, store.getOrDefault(num, 0) + 1);
// 构造小根堆(即最小堆),用于保存前 k 个出现频率最高的元素
// 目的是让 minHeap 根据数字出现的频率进行升序排序(因为是小根堆,所以是升序)
PriorityQueue minHeap = new PriorityQueue <> (Comparator.comparingInt(store::get));
for(Integer key: store.keySet()) {
if(minHeap.size() minHeap.offer(key);
else if(store.get(key) > store.get(minHeap.peek())) { // 只要满了,则推出最小值
minHeap.poll();
minHeap.offer(key);
}
}
int[] res = new int[k];
int i = 0;
while(!minHeap.isEmpty())
res[i++] = minHeap.poll();
return res;
}

3. 桶排序

一句话总结:划分多个范围相同的区间,每个子区间自排序,最后合并

桶排序是计数排序的扩展版本,计数排序可以看成每个桶只存储相同元素,而桶排序每个桶存储一定范围的元素,通过映射函数,将待排序数组中的元素映射到各个对应的桶中,对每个桶中的元素进行排序,最后将非空桶中的元素逐个放入原序列中。

桶排序需要尽量保证元素分散均匀,否则当所有数据集中在同一个桶中时,桶排序失效。


3.1 图解桶排序


3.2 实现

public int[] topKFrequent(int[] nums, int k) {
// 构造 HashMap。Key:nums 中的每个元素;Value:对应元素出现的次数(即频率)
HashMap store = new HashMap <> ();
for(Integer num: nums)
store.put(num, store.getOrDefault(num, 0) + 1);
// 构造一个桶的集合(即一系列桶),桶的个数为 nums 的长度 +1,因为 buckets[0] 没有意义
// 目的是将出现频率为 i 的数放到第 i 个桶里(即 buckets[i])
List [] buckets = new List[nums.length + 1];
for(Map.Entry entry: store.entrySet()) {
if(buckets[entry.getValue()] == null) // 如果某个桶还未放入过数字(即未初始化),则初始化其为一个数组
buckets[entry.getValue()] = new ArrayList <> ();
buckets[entry.getValue()].add(entry.getKey());
}
int[] res = new int[k];
int index = 0;
for(int i = buckets.length - 1; i > 0; i--) { // 遍历每个桶
if(buckets[i] != null) { // 如果桶里有数字
for(int j = 0; j res[index++] = buckets[i].get(j); // 依次将桶里的数字填充进 res 数组中
}
}
}
return res;
}

 



推荐阅读
  • 海马s5近光灯能否直接更换为H7?
    本文主要介绍了海马s5车型的近光灯是否可以直接更换为H7灯泡,并提供了完整的教程下载地址。此外,还详细讲解了DSP功能函数中的数据拷贝、数据填充和浮点数转换为定点数的相关内容。 ... [详细]
  • 本文介绍了如何在给定的有序字符序列中插入新字符,并保持序列的有序性。通过示例代码演示了插入过程,以及插入后的字符序列。 ... [详细]
  • 广度优先遍历(BFS)算法的概述、代码实现和应用
    本文介绍了广度优先遍历(BFS)算法的概述、邻接矩阵和邻接表的代码实现,并讨论了BFS在求解最短路径或最短步数问题上的应用。以LeetCode中的934.最短的桥为例,详细阐述了BFS的具体思路和代码实现。最后,推荐了一些相关的BFS算法题目供大家练习。 ... [详细]
  • 本文介绍了在rhel5.5操作系统下搭建网关+LAMP+postfix+dhcp的步骤和配置方法。通过配置dhcp自动分配ip、实现外网访问公司网站、内网收发邮件、内网上网以及SNAT转换等功能。详细介绍了安装dhcp和配置相关文件的步骤,并提供了相关的命令和配置示例。 ... [详细]
  • SpringBoot uri统一权限管理的实现方法及步骤详解
    本文详细介绍了SpringBoot中实现uri统一权限管理的方法,包括表结构定义、自动统计URI并自动删除脏数据、程序启动加载等步骤。通过该方法可以提高系统的安全性,实现对系统任意接口的权限拦截验证。 ... [详细]
  • Java容器中的compareto方法排序原理解析
    本文从源码解析Java容器中的compareto方法的排序原理,讲解了在使用数组存储数据时的限制以及存储效率的问题。同时提到了Redis的五大数据结构和list、set等知识点,回忆了作者大学时代的Java学习经历。文章以作者做的思维导图作为目录,展示了整个讲解过程。 ... [详细]
  • LeetCode笔记:剑指Offer 41. 数据流中的中位数(Java、堆、优先队列、知识点)
    本文介绍了LeetCode剑指Offer 41题的解题思路和代码实现,主要涉及了Java中的优先队列和堆排序的知识点。优先队列是Queue接口的实现,可以对其中的元素进行排序,采用小顶堆的方式进行排序。本文还介绍了Java中queue的offer、poll、add、remove、element、peek等方法的区别和用法。 ... [详细]
  • 阿,里,云,物,联网,net,core,客户端,czgl,aliiotclient, ... [详细]
  • C语言注释工具及快捷键,删除C语言注释工具的实现思路
    本文介绍了C语言中注释的两种方式以及注释的作用,提供了删除C语言注释的工具实现思路,并分享了C语言中注释的快捷键操作方法。 ... [详细]
  • 本文详细介绍了Java中vector的使用方法和相关知识,包括vector类的功能、构造方法和使用注意事项。通过使用vector类,可以方便地实现动态数组的功能,并且可以随意插入不同类型的对象,进行查找、插入和删除操作。这篇文章对于需要频繁进行查找、插入和删除操作的情况下,使用vector类是一个很好的选择。 ... [详细]
  • [大整数乘法] java代码实现
    本文介绍了使用java代码实现大整数乘法的过程,同时也涉及到大整数加法和大整数减法的计算方法。通过分治算法来提高计算效率,并对算法的时间复杂度进行了研究。详细代码实现请参考文章链接。 ... [详细]
  • Java学习笔记之面向对象编程(OOP)
    本文介绍了Java学习笔记中的面向对象编程(OOP)内容,包括OOP的三大特性(封装、继承、多态)和五大原则(单一职责原则、开放封闭原则、里式替换原则、依赖倒置原则)。通过学习OOP,可以提高代码复用性、拓展性和安全性。 ... [详细]
  • 本文介绍了iOS数据库Sqlite的SQL语句分类和常见约束关键字。SQL语句分为DDL、DML和DQL三种类型,其中DDL语句用于定义、删除和修改数据表,关键字包括create、drop和alter。常见约束关键字包括if not exists、if exists、primary key、autoincrement、not null和default。此外,还介绍了常见的数据库数据类型,包括integer、text和real。 ... [详细]
  • 本文介绍了深入浅出Linux设备驱动编程的重要性,以及两种加载和删除Linux内核模块的方法。通过一个内核模块的例子,展示了模块的编译和加载过程,并讨论了模块对内核大小的控制。深入理解Linux设备驱动编程对于开发者来说非常重要。 ... [详细]
  • AFNetwork框架(零)使用NSURLSession进行网络请求
    本文介绍了AFNetwork框架中使用NSURLSession进行网络请求的方法,包括NSURLSession的配置、请求的创建和执行等步骤。同时还介绍了NSURLSessionDelegate和NSURLSessionConfiguration的相关内容。通过本文可以了解到AFNetwork框架中使用NSURLSession进行网络请求的基本流程和注意事项。 ... [详细]
author-avatar
晴子suerw_980
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有