【LeetCode栈与队列#06】前K个高频元素(TopK问题),以及pair、priority_queue的使用
前 K 个高频元素
给定一个非空的整数数组,返回其中出现频率前 k 高的元素。
示例 1:
- 输入: nums = [1,1,1,2,2,3], k = 2
- 输出: [1,2]
示例 2:
- 输入: nums = [1], k = 1
- 输出: [1]
提示:
- 你可以假设给定的 k 总是合理的,且 1 ≤ k ≤ 数组中不相同的元素的个数。
- 你的算法的时间复杂度必须优于 $O(n \log n)$ , n 是数组的大小。
- 题目数据保证答案唯一,换句话说,数组中前 k 个高频元素的集合是唯一的。
- 你可以按任意顺序返回答案
思路
(两个解法,记一种,不要乱就行)
初见思路是:遍历数组,根据下标在set中记录元素出现的次数,遍历结束,将set中的元素放入优先级队列中排序,取出前k个元素
显然是不行的,虽然将获取到了元素出现的次数并排序,但是丢失了原始数组中元素的位置信息导致无法返回对应元素
所以应该用map啊,key对应数组中的元素,value对应其出现过的次数
统计完成之后,再以value为对象进行一个排序,从而得到前k个高频出现的元素
怎么排序?
用 堆
这种数据结构比较合适,具体来说就是 大顶堆 或 小顶堆
因为题目对时间复杂度有要求,所以以前通小顶堆来做可以满足要求(限制堆的大小进而控制复杂度)
但是现在好像两种方式都可以了
使用小顶堆
我们只需将堆的大小规定为k,然后不断往堆里push元素,同时将多余的元素pop掉,最后就剩下想要的前k个元素了。
注意,这里具体我们应该使用的是小顶堆
举个例子,假设k = 3,我们使用小顶堆
1
/ \
2 3
接下来往堆中push进一个新value,那么堆顶的元素就要被pop掉,然后堆中元素重新排序,使新的最小值位于堆顶
1 2
/ \ / \
2 3 → 3 6
/
6
如此循环,最小值不断被pop掉,最后这个大小为k的小顶堆中剩下的就是前k个最大的值了
这也就是为什么用小顶堆而不是大顶堆的原因,用大顶堆的话效果正好相反(在限制k的情况下),会得到前k个最小的值
以value(也就是元素出现次数)排序好之后,只需再将对应的key返回即可
使用大顶堆
这种方式就很直观,但是相比小顶堆(限制k的值)而言复杂度会高一些
遍历数组,将所有元素放入堆中,此时堆顶就是最大值,然后我们只需将堆顶元素pop出来k次,即可得到前k个高频元素
补充知识:pair容器
简介
pair是一种将两个数据组合成一个数据的容器(如stl中的map就是将key和value放在一起来保存)
pair<T1, T2> p1(v1, v2);
p1
-----------------
| T1 V1 | T2 V2 |
如上图,v1、v2组合之后又变成了一个数据p1
当想要在一个函数中返回两个值的时候也可以用pair,先将数据打包成一个,返回之后在解包
定义
其标准库类型 --pair 类型定义在#include<utility>
头文件中,定义如下:
类模板:template<class T1,class T2> struct pair
参数:T1是第一个值的数据类型,T2是第二个值的数据类型。
功能:pair将一对值(T1和T2)组合成一个值,
这一对值可以具有不同的数据类型(T1和T2),
两个值可以分别用pair的两个公有函数first和second访问。
构造函数
pair<T1, T2> p1; //创建一个空的pair对象(使用默认构造),它的两个元素分别是T1和T2类型,采用值初始化。
pair<T1, T2> p1(v1, v2); //创建一个pair对象,它的两个元素分别是T1和T2类型,其中first成员初始化为v1,second成员初始化为v2。
make_pair(v1, v2); // 以v1和v2的值创建一个新的pair对象,其元素类型分别是v1和v2的类型。
p1 < p2; // 两个pair对象间的小于运算,其定义遵循字典次序:如 p1.first < p2.first 或者 !(p2.first < p1.first) && (p1.second < p2.second) 则返回true。
p1 == p2; // 如果两个对象的first和second依次相等,则这两个对象相等;该运算使用元素的==操作符。
p1.first; // 返回对象p1中名为first的公有数据成员
p1.second; // 返回对象p1中名为second的公有数据成员
使用
利用make_pair创建新的pair对象
pair<int, double> p1;
p1 = make_pair(1, 1.2);
cout << p1.first << p1.second << endl;
//output: 1 1.2
int a = 8;
string m = "James";
pair<int, string> newone;
newone = make_pair(a, m);
cout << newone.first << newone.second << endl;
//output: 8 James
补充知识:优先级队列
简介
普通的队列具有先进先出的特性,元素追加在队尾,如果删除的话,从队头删除。
而在优先队列中,队列中的数据被赋予了优先级;
当访问元素时,优先级最高的会先被删除;所以说优先队列是最高级数据先出。
定义
其位于头文件#include <queue>
中
基本操作:
- empty() 如果队列为空,则返回真
- pop() 删除对顶元素,删除第一个元素
- push() 加入一个元素
- size() 返回优先队列中拥有的元素个数
- top() 返回优先队列对顶元素,返回优先队列中有最高优先级的元素
在默认的优先队列中,优先级高的先出队。在默认的int型中先出队的为较大的数。
声明
普通方式
priority_queue<vector<int>, less<int> > pq1; // 使用递增less<int>函数对象排序
priority_queue<deque<int>, greater<int> > pq2; // 使用递减greater<int>函数对象排序
//greater和less是std实现的两个仿函数(就是使一个类的使用看上去像一个函数。其实现就是类中实现一个operator(),这个类就有了类似函数的行为,就是一个仿函数类了)
自定义优先级
通过构建一个仿函数去规定优先级队列的比较规则进而实现自定义优先级
struct cmp {//自定义的比较仿函数
operator bool ()(int x, int y){
return x > y;// x小的优先级高
//也可以写成其他方式,如: return p[x] > p[y];表示p[i]小的优先级高
}
};
priority_queue<int, vector<int>, cmp> q; //定义方法
//其中,第二个参数为容器类型。第三个参数为比较函数。
使用
用pair做优先队列元素:先比较frist,如果frist相等,那么就比较第二个
#include <iostream>
#include <queue>
#include <vector>
using namespace std;
int main()
{
priority_queue<pair<int, int> > a;
pair<int, int> b(1, 2);
pair<int, int> c(1, 3);
pair<int, int> d(2, 5);
a.push(d);
a.push(c);
a.push(b);
while (!a.empty())
{
cout << a.top().first << ' ' << a.top().second << '\n';
a.pop();
}
}
代码
大顶堆实现的代码比起小顶堆的要简单一些
大顶堆实现
步骤
1、创建unordered_map,遍历数组,在map中对应位置记录
2、以pair作为元素创建优先级队列priority_queue,遍历map(iterator方式)
- 定义pair时使用 [值,键] 的方式,也就是让频率在前,对应元素在后。因为如果没有自定义优先级,使用pair的priority_queue会默认先比较第一个值进行排序(当然还是构成大顶堆)
3、定义结果数组res,从堆顶循环取值k次,将pair中的second值取出放入res,同时pop掉当前值
代码
// 时间复杂度:O(nlogn)
// 空间复杂度:O(n)
class Solution {
public:
vector<int> topKFrequent(vector<int>& nums, int k) {
//创建unorder_map
unordered_map<int, int> map;
//遍历nums,在map中对应位置记录
for(int i = 0; i < nums.size(); ++i){
map[nums[i]]++;
}
//创建优先级队列priority_queue
priority_queue<pair<int, int>> que;//大顶堆
//遍历map
for(auto pair : map){
//取出pair中的数据,以[值,键] 的方式加入大顶堆
que.push(make_pair(pair.second, pair.first));
}
vector<int> res;
while(k--){
res.push_back(que.top().second);//即将pair.first(元素值)放入res中
que.pop();
}
return res;
}
};
不熟练的点
1、map的构造
构造:
map<T1, T2> mp;
//map默认构造函数:map(const map &mp);
//拷贝构造函数
2、队列的使用方法
构造函数:
queue<T> que;
//queue采用模板类实现,queue对象的默认构造形式queue(const queue &que);
//拷贝构造函数
赋值操作:
queue& operator=(const queue &que);
//重载等号操作符
数据存取:
push(elem);
//往队尾添加元素pop();
//从队头移除第一个元素back();
//返回最后一个元素front();
//返回第一个元素
大小操作:
empty();
//判断堆栈是否为空size();
//返回栈的大小
小顶堆实现
仿函数
使用小顶堆实现需要自定义优先级规则,如下:
通过构建一个仿函数定义如何比较两个 pair
输入为两个pair的引用,比较的是pair中的second值
struct cmp {//自定义的比较仿函数
bool operator()(const pair<int, int>& lefths, const pair<int, int>& righths) {
return lefths.second > righths.second;//从小到大
}
};
这里比较阴间的是:左大于右就会建立小顶堆,反之则建立大顶堆
刚好和认知相反
步骤
1、创建一个仿函数,定义优先级规则
2、创建unordered_map,遍历数组,对应位置记录
3、创建priority_queue
4、遍历map,pair存入队列
- 当队列元素个数大于k时,pop掉当前队首元素
5、创建vector res,逆序遍历队列获取结果
-
res直接创建大小为k的就行
-
注意,这里需要获取的是pair的first
代码
class Solution {
public:
class cmp{
public:
bool operator()(pair<int, int>& left, pair<int, int>& right){
return left.second > right.second;
}
};
vector<int> topKFrequent(vector<int>& nums, int k) {
//定义map
unordered_map<int, int> map;
for(int i = 0; i < nums.size(); ++i){
map[nums[i]]++;
}
//定义优先级队列
priority_queue<pair<int, int>, vector<pair<int, int>>, cmp> que;
for(unordered_map<int, int>::iterator it = map.begin(); it != map.end(); it++){
que.push(*it);
//控制堆的大小
if(que.size() > k){
que.pop();
}
}
//使用了 vector<int> res(k) 来初始化 res,是因为我们知道 res 的长度是 k,所以可以直接指定它的大小
vector<int> res(k);
//逆序遍历
for(int i = k - 1; i >= 0; --i){
res[i] = que.top().first;//往 res 中添加元素时使用下标访问方式赋值是因为在遍历的过程中,我们需要保证各个元素填充到正确的位置上,而 push_back() 只能将元素添加到数组的最后面
//如果使用 push_back() 方法来添加元素,则需要在最后再执行一次 reverse(res.begin(), res.end()) 来翻转数组,降低了效率
que.pop();
}
return res;
}
};
不熟练的点
1、vector创建的方式
要创建指定大小的vector,用括号
例如:`vector
二刷问题
1、自定义优先级队列的比较规则时不熟练,operator写错(多写点)
2、使用迭代器iterator来遍历map的操作不熟练
3、创建键值对可以使用make_pair(),也可以使用花括号
4、大小根堆概念有遗忘
5、使用下标方式填充vector的操作不熟练