java数据结构与算法刷题目录(剑指Offer、LeetCode、ACM)-----主目录-----持续更新(进不去说明我没写完):https://blog.youkuaiyun.com/grd_java/article/details/123063846 |
---|
1. 法一:桶排序
解题思路:时间复杂度O(n),空间复杂度O(n) |
---|
- 我们先通过hash表,将每个元素的出现频率统计出来
- 根据出现频率进行桶排序
- 那么越后面的桶的元素,出现次数越多。所以从后向前遍历桶,将桶中的元素依次取出
- 题目要求取k个,那就取k个为止,放入结果数组中。
代码 |
---|
class Solution {
public int[] topKFrequent(int[] nums, int k) {
// 1. 准备计数排序的数组,也就是hash表,这里用数组来充当hash表,做题时,处理速度要优于使用HashMap,
//注意实际工作场景中,使用线程安全的HashMap容器,而不用使用数组来作为hash
int max = Integer.MIN_VALUE;
int min = Integer.MAX_VALUE;
for(int num: nums) {
if(num > max)
max = num;
if(num < min)
min = num;
}
//创建可以容纳数组中所有取值的hash表,用下标索引代表nums数组中元素值
if (max == min) return new int[] {nums[0]};
int [] count = new int[max - min + 1];
int bucketLength = 0;//桶排序,桶的数量,我们要对出现频率桶排序,所以,最大的出现频率+1就是桶的数量
for(int num: nums) {//hash数组下标从0开始,下标0代表nums中的最小值,因此每个元素存储时,要进行-min+1
count[num - min]++;
bucketLength = bucketLength<count[num-min]?count[num-min]:bucketLength;//最大出现频率
}
// 2. 将计数排序数组转化为 存放频率的桶,对hash表中存储的元素出现频率,进行桶排序
List<Integer> [] bucket = new ArrayList[bucketLength + 1];//最大出现频率+1为桶的数量
//进行桶排序
for(int i = 0; i < count.length; i++) {
if(count[i] > 0) {
if(bucket[count[i]] == null) {//每个桶都是一个链表
bucket[count[i]] = new ArrayList();
}
bucket[count[i]].add(i + min);//存放时,不在存放下标,而是元素值本身
}
}
// 3. 从高频率往低频率遍历(从后往前遍历),构造结果数组
int [] res = new int[k];
for(int i = bucket.length - 1, j = 0; j < k && i >= 0; i--) {
List<Integer> fdata = bucket[i];//获取当前桶中的所有结点
if(fdata != null) {//如果有结点
int size = fdata.size();
//将当前出现频率的结点,依次放入res结果数组中
// for(int index = size - 1; index >= 0; index--) {
// res[j++] = fdata.get(index);
// }//桶内无论怎么输出,都无所谓
for(int index = 0; index <= size-1; index++) {
res[j++] = fdata.get(index);
}
}
}
return res;
}
}
2. 法二:小根堆(更具实战意义)
此方法更推荐实际工作中使用,因为桶排序的不确定因素有很多,很可能出现桶的数量过多,从而浪费大量空间的情况。
解题思路 |
---|
- 和桶排序基本类似。只不过将桶排序,换成了堆排序(小根堆)
- 先hash统计出现次数
- 通过优先级队列,实现小根堆。保证堆顶永远是最少出现次数。
- 先插入k个元素到小根堆。因为题目要求返回k个出现频率最高的
- 之后如果还有元素,和堆顶进行比较,如果比堆顶小,就舍弃。否则取出堆顶元素,让当前新元素入堆。
代码 |
---|
class Solution {
public int[] topKFrequent(int[] nums, int k) {
//建立hash映射
Map<Integer, Integer> occurrences = new HashMap<Integer, Integer>();
for (int num : nums) {
occurrences.put(num, occurrences.getOrDefault(num, 0) + 1);
}
// int[] 的第一个元素代表数组的值,第二个元素代表了该值出现的次数
//优先级队列,就是堆。默认是大根堆
PriorityQueue<int[]> queue = new PriorityQueue<int[]>(new Comparator<int[]>() {//改为小根堆
public int compare(int[] m, int[] n) {//出现次数谁高,谁优先级就高,会最优先出堆
//n表示堆顶,m是新插入
//compare方法,如果返回值 >0 那么m放入n后面
//如果m更大,m就插入到n的下面,n继续是堆顶,从而实现小根堆
return m[1] - n[1];//优先级高的先出堆。也就是它们在堆的最下面,最优先出堆
//n表示堆顶,m是新插入,如果返回值 < 0 m就作为新的堆顶,从而实现大根堆
//如果m更大,n - m必然小于0, 那么m成为新的堆顶
//return n[1] - m[1];
}
});
//构建小根堆。堆中元素=k.检查堆顶,和当前想要入堆的出现次数,
//因为我们只需要k个出现频率最高的,所以已经有k个了,后面想入堆的需要进行判断
//如果想入堆的出现次数更高,就弹出堆顶,让其入堆。如果没有堆顶大,就舍去想入堆的
//而如果堆中元素 < k,说明还没有凑够题目要求的k个元素,无脑入堆
for (Map.Entry<Integer, Integer> entry : occurrences.entrySet()) {
int num = entry.getKey(), count = entry.getValue();
if (queue.size() == k) {//如果堆中已经有k个元素
if (queue.peek()[1] < count) {//看看堆顶是否 < 当前元素
queue.poll();//如果小,说明当前元素出现频率更高,那么堆顶不要了,新元素入堆
queue.offer(new int[]{num, count});
}
} else {//如果堆中没有够k个,无脑入堆
queue.offer(new int[]{num, count});
}
}
int[] ret = new int[k];//答案数组
for (int i = 0; i < k; ++i) {
ret[i] = queue.poll()[0];//依次出堆
}
return ret;
}
}