1、堆
最大堆与最小堆
- 堆是一种特殊的树形数据结构,根据根节点的值与子节点的值的大小关系,堆又分为最大堆和最小堆。
- 在最大堆中,每个节点的值总是大于或等于其任意子节点的值,因此最大堆的根节点就是整个堆的最大值
- 在对小堆中,每个节点的值总是小于或等于其任意子节点的值,因为最小堆的根节点就是整个堆的最小值。
堆通常用完全二叉树来实现
堆的操作
- 完全二叉树可以使用数组来保存堆中结点,堆中根节点与左右子节点的关系,在数组中的下标存在一定逻辑关系
- 往堆中添加新元素:通常是在堆的最后一个位置插入一个元素,然后判断该元素与父节点的大小关系,如果是最大堆,如果新插入的元素值比父节点的值要大,则要进行交换,这个操作会不断递归下去,直到父节点为根节点。
- 在堆中删除元素,删除的元素是堆顶元素,如果是最大堆,则将堆中最下一层的最右边元素替换到根节点,并与左右子节点进行比较,如果存在左右子节点比父节点大的结点,则将较大的结点与父节点进行交换,并继续判断下去,直到没有子节点或父节点值最大为止。
C++中最大堆与最小堆的使用
cpp
复制代码
默认就是最大堆
priority_queue<int, std::vector<int>> maxHeap;
也可以指定:
priority_queue<int, std::vector<int>, std::less<int>> max_heap; // 最大堆
最小堆:
priority_queue<int, std::vector<int>, std::greater<int>> min_heap; // 最小堆
也可以指定堆的判断方法
堆的应用
- 堆经常用来求取一个数据集合中值最大和最小的k个元素。最小堆用来求取数据集合中k个值最大的元素,最大堆用来求取数据集合中k个值最小的元素。
2、LCR 059. 数据流中的第 K 大元素
题目信息:
cpp
复制代码
设计一个找到数据流中第 k 大元素的类(class)。注意是排序后的第 k 大元素,不是第 k 个不同的元素。
请实现 KthLargest 类:
KthLargest(int k, int[] nums) 使用整数 k 和整数流 nums 初始化对象。
int add(int val) 将 val 插入数据流 nums 后,返回当前数据流中第 k 大的元素。
示例:
输入:
["KthLargest", "add", "add", "add", "add", "add"]
[[3, [4, 5, 8, 2]], [3], [5], [10], [9], [4]]
输出:
[null, 4, 5, 5, 8, 8]
解释:
KthLargest kthLargest = new KthLargest(3, [4, 5, 8, 2]);
kthLargest.add(3); // return 4
kthLargest.add(5); // return 5
kthLargest.add(10); // return 5
kthLargest.add(9); // return 8
kthLargest.add(4); // return 8
提示:
1 <= k <= 104
0 <= nums.length <= 104
-104 <= nums[i] <= 104
-104 <= val <= 104
最多调用 add 方法 104 次
题目数据保证,在查找第 k 大元素时,数组中至少有 k 个元素
解题思路:
- 1、审题:实现一个获取集合中第k大的值的一个类,在构造函数中输入数组nums和数值k
- 然后调用add方法,往集合中插入一个元素val,并且该方法返回集合中的第k大的值
- 2、解题:
- 使用最小堆来求解集合中的最大值,priportyQueue,在构造函数中就开始组装数据,并保证最小堆只有k个元素
- 调用add方法插入元素时,先判断最小堆的元素个数是否大于k,如果大于k,则先删除堆中最小元素,然后添加新元素,
- 如果最小堆的元素个数小于k,则直接添加元素,最后返回最小堆的根节点元素值
代码实现:
cpp
复制代码
class KthLargest
{
public:
KthLargest(int k, vector<int> &nums)
{
this->k = k;
// 往最小堆中添加元素,直接调用add方法
for (auto num : nums)
{
add(num);
}
}
/**
* 往最小堆中添加元素:
* - 如果当前堆的元素个数小于k,则直接添加元素
* - 如果当前堆的元素个数大于等于k了,则需要先判断新插入的元素如果大于最小堆顶元素,则堆顶元素出队再插入,否则不插入
*/
int add(int val)
{
if (min_heap.size() < k)
{
min_heap.push(val);
}
else if (min_heap.top() < val)
{
min_heap.pop();
min_heap.push(val);
}
return min_heap.top();
}
private:
priority_queue<int, std::vector<int>, std::greater<int>> min_heap; // 最小堆
int k;
};
3、LCR 060. 前 K 个高频元素
题目信息:
cpp
复制代码
给定一个整数数组 nums 和一个整数 k ,请返回其中出现频率前 k 高的元素。可以按 任意顺序 返回答案。
示例 1:
输入: nums = [1,1,1,2,2,3], k = 2
输出: [1,2]
示例 2:
输入: nums = [1], k = 1
输出: [1]
提示:
1 <= nums.length <= 105
k 的取值范围是 [1, 数组中不相同的元素的个数]
题目数据保证答案唯一,换句话说,数组中前 k 个高频元素的集合是唯一的
进阶:所设计算法的时间复杂度 必须 优于 O(n log n) ,其中 n 是数组大小。
解题思路:
- 1、审题:输入一个数组和k值,要求从数组中找出出现相同的数字,并且出现的次数排列在前k个的元素,并返回数组
- 2、解题:
- 要求数字出现的次数,需要使用哈希表map保存每个数字,和其对应出现的次数
- 要求数字出现的最多的k次数,可以使用最小堆来比较元素出现的次数,次数比堆顶元素大的,则删除堆顶元素,并加入到最小堆中。如果比堆顶元素还小的,则直接过滤掉
- 最后将最小堆中的元素添加到集合中返回
代码实现:
cpp
复制代码
static bool camp(std::pair<int, int> &m, std::pair<int, int> &n)
{
return m.second > n.second;
}
vector<int> topKFrequent(vector<int> &nums, int k)
{
std::map<int, int> map;
// 将数组元素添加到map哈希表中
for (auto num : nums)
{
if (map.find(num) != map.end())
{
// 存在
map[num]++;
}
else
{
map[num] = 1;
}
}
// 最小堆,需要保存键值对,使用pair作为堆中存储的元素
priority_queue<pair<int, int>, std::vector<pair<int, int>>, decltype(&camp)> min_heap(camp);
// 遍历哈希表map
for (auto mapItem : map)
{
if (min_heap.size() < k)
{
min_heap.emplace(mapItem.first, mapItem.second);
}
else
{
// 比较最小堆的堆顶元素
if (min_heap.top().second < mapItem.second)
{
min_heap.pop();
min_heap.emplace(mapItem.first, mapItem.second);
}
}
}
vector<int> res;
while (!min_heap.empty())
{
res.push_back(min_heap.top().first);
min_heap.pop();
}
return res;
}
4、总结:
- 堆这种结构的认识,本质上一棵完全儿叉树,又分为最大堆和最小堆,根据父节点值与左右子节点的值来判断的。
- 注意堆数据结构的操作,删除堆顶元素和添加原始的操作
- 堆结构的应用场景: 在一组数据集合中,求最大的k个元素,使用最小堆来保存数据,遍历集合,当堆中元素超过k个时,比堆顶元素大的情况下,则删除堆顶元素,并添加新元素,否则直接过滤。反之使用最大堆来保存数据。