LCR059-LCR061堆的应用

剑指offer(专项突破版)9.2 堆的应用

LCR059.数据流中的第K大元素

分析

如果能够找出k个最大的数字,那么第k大的数字就是这k个最大数字中最小的一个。由于每次都需要找出k个数字中的最小值,因此可以把这k个数字保存到最小堆中。每当从数据流中读出一个数字,就先判断这个新的数字是不是有必要添加到最小堆中。如果最小堆中元素的数目还小于k,那么直接将它添加到最小堆中。如果最小堆中已经有k个元素,那么将其和位于堆顶的最小值进行比较。如果新读出的数字小于或等于堆中的最小值,那么堆中的k个数字都比它大,因此它不可能是k个最大的数字中的一个。由于只需要保存最大的k个数字,因此新读出的数字可以忽略。如果新的数字大于堆顶的数字,那么堆顶的数字就是第k+1大的数字,可以将它从堆中删除,并将新的数字添加到堆中,这样堆中保存的仍然是到目前为止从数据流中读出的最大的k个数字,此时第k大的数字正好位于最小堆的堆顶。

使用STL自带的priority_queue:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
class KthLargest {
public:
priority_queue<int, vector<int>, greater<int>> heap;
int size;
KthLargest(int k, vector<int>& nums) {
size = k;
for(int i = 0; i < nums.size(); i++){
add(nums[i]);
}
}

int add(int val) {
// 如果堆大小小于k,直接插入
if(heap.size() < size){
heap.push(val);
}
// 否则如果大于堆顶,就把堆顶删除
else if(val > heap.top()){
heap.pop();
heap.push(val);
}
return heap.top();
}
};

/**
* Your KthLargest object will be instantiated and called as such:
* KthLargest* obj = new KthLargest(k, nums);
* int param_1 = obj->add(val);
*/
  • 时间复杂度:O(logk)
  • 空间复杂度:O(k)

LCR059结果

LCR060.前K个高频元素

分析

首先使用哈希表存储元素及其出现的次数,然后遍历每一个频次,建立最小堆。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
class Solution {
public:
struct cmp{
bool operator() (const pair<int, int>& a, const pair<int, int>& b){
return a.second > b.second;
}
};

vector<int> topKFrequent(vector<int>& nums, int k) {
// 存储至哈希表
unordered_map<int, int> hash;
for(int i = 0; i < nums.size(); i++){
hash[nums[i]]++;
}
// 根据哈希表建立大小为k的最小堆
priority_queue<pair<int,int>, vector<pair<int, int>>, cmp> minHeap;
for( auto it = hash.begin(); it != hash.end(); it++){
if(minHeap.size() < k){
minHeap.push(*it);
}
else if(it->second > minHeap.top().second){
minHeap.pop();
minHeap.push(*it);
}
}
// 返回结果
vector<int> res;
for(int i = 0; i < k; i++){
int cur = minHeap.top().first;
res.push_back(cur);
minHeap.pop();
}
return res;

}
};
  • 时间复杂度:假设输入数组的长度为n。在大小为k的堆中进行添加或删除操作的时间复杂度是O(logk),因此上述代码的时间复杂度是O(nlogk)。
  • 空间复杂度:上述代码需要一个大小为O(n)的哈希表,以及一个大小为O(k)的最小堆,因此总的空间复杂度是O(n)。

LCR060结果

LCR061.查找和最小的k对数字

最大堆

这个题目要求找出和最小的k个数对。可以用最大堆来存储这k个和最小的数对。逐一将m×n个数对添加到最大堆中。当堆中的数对的数目小于k时,直接将数对添加到堆中。如果堆中已经有k个数对,那么先要比较待添加的数对之和及堆顶的数对之和(也是堆中最大的数对之和)。如果待添加的数对之和大于或等于堆顶的数对之和,那么堆中的k个数对之和都小于或等于待添加的数对之和,因此待添加的数对可以忽略。如果待添加的数对之和小于堆顶的数对之和,那么删除堆顶的数对,并将待添加的数对添加到堆中,这样可以确保堆中存储的是和最小的k个数对。每次都是将待添加的数对与堆中和最大的数对进行比较,而这也是用最大堆的原因。

接下来考虑如何优化。题目给出的条件是输入的两个数组都是递增排序的,这个特性我们还没有用到。如果从第1个数组中选出第k+1个数字和第2个数组中的某个数字组成数对p,那么该数对之和一定不是和最小的k个数对中的一个,这是因为第1个数组中的前k个数字和第2个数组中的同一个数字组成的k个数对之和都要小于数对p之和。因此,不管输入的数组nums1有多长,最多只需要考虑前k个数字。同理,不管输入的数组nums2有多长,最多也只需要考虑前k个数字。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
class Solution {
public:
struct cmp{
bool operator() (const pair<int,int>& a, const pair<int,int>& b){
return (long)(a.first + a.second) < (long)(b.first + b.second);
}
};
vector<vector<int>> kSmallestPairs(vector<int>& nums1, vector<int>& nums2, int k) {
priority_queue<pair<int,int>, vector<pair<int,int>>, cmp> heap; // 最大堆
for(int i = 0; i < k && i < nums1.size(); i++){
for(int j = 0; j < k && j < nums2.size(); j++){
if(heap.size() < k){
heap.push(pair<int,int>(nums1[i],nums2[j]));
}
else if(nums1[i] + nums2[j] < heap.top().first + heap.top().second){
heap.pop();
heap.push(pair<int,int>(nums1[i],nums2[j]));
}
}
}
// 输出结果
vector<vector<int>> res;
while(!heap.empty()){
auto it = heap.top();
vector<int> cur;
cur.push_back(it.first);
cur.push_back(it.second);
res.push_back(cur);
heap.pop();
}
return res;
}
};
  • 时间复杂度:上述代码有两个相互嵌套的for循环,每个循环最多执行k次。在循环体内可能在最大堆中进行添加或删除操作,由于最大堆中最多包含k个元素,因此添加、删除操作的时间复杂都是$O(logk)$。这两个for循环的时间复杂度是$O(k^2logk)$。另外,上述代码还有一个while循环,它逐一从最大堆中删除元素并将对应的数对添加到链表中,这个while循环的时间复杂度是O(klogk)。因此,上述代码总的时间复杂度是$O(k^2logk)$。
  • 空间复杂度:$O(k)$

LCR061最大堆结果

最小堆

首先将nums1中的前k个元素与nums2的第一个元素组成的数对依次加入到堆中。这样做是为了初始化堆,确保堆中包含了每个nums1元素与nums2第一个元素组成的数对。然后进行循环处理,每次取出堆顶元素(当前和最小的数对),将其加入结果数组中,并将堆顶元素的索引往后移动一位,如果不超过nums2的范围,则将其加入堆中。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
class Solution {
public:
// 找到两个已排序数组中的 k 个最小对
vector<vector<int>> kSmallestPairs(vector<int>& nums1, vector<int>& nums2, int k) {
// 处理特殊情况:数组为空或 k 非正数
if (nums1.empty() || nums2.empty() || k <= 0) {
return {};
}

// Lambda 函数,根据对的和进行比较
auto compare = [&](const pair<int,int>& a, const pair<int,int>& b) {
return (nums1[a.first] + nums2[a.second]) > (nums1[b.first] + nums2[b.second]);
};

// 优先队列用于维护 k 个最小对,使用自定义比较函数进行初始化
priority_queue<pair<int,int>, vector<pair<int,int>>, decltype(compare)> heap(compare);

// 初始化优先队列,将 nums1 和 nums2 的开始元素组成的对加入队列
for(int i = 0; i < nums1.size() && i < k; i++){
heap.push({i, 0});
}

// 从队列中提取对,直到找到 k 个最小对为止
vector<vector<int>> res;
while(!heap.empty() && res.size() < k){
auto ids = heap.top();
heap.pop();
res.push_back({nums1[ids.first], nums2[ids.second]});

// 如果仍有 nums2 中的下一个元素,将下一个对加入优先队列
if(ids.second < nums2.size() - 1){
heap.push({ids.first, ids.second + 1});
}
}

return res;
}
};

decltype 是一个 C++ 关键字,用于获取某个表达式的类型。在这里,decltype(compare) 取得的类型就是 compare 这个 lambda 函数的类型。所以 decltype(compare)> 实际上是告诉编译器我们要使用 compare 这个类型的比较函数来对优先队列中的元素进行比较。

  • 时间复杂度:上述代码先用一个for循环构建一个大小为k的最小堆,该循环的时间复杂度是O(klogk)。接下来是一个执行k次的while循环,每次对大小为k的最小堆进行添加或删除操作,因此这个while循环的时间复杂度也是O(klogk)。上述代码总的时间复杂度为O(klogk)。
  • 空间复杂度:O(k)

LCR061最小堆结果

总结

本章介绍了堆这种数据结构。堆又可以分成最大堆和最小堆。在最大堆中最大值总是位于堆顶,在最小堆中最小值总是位于堆顶。因此,在堆中只需要O(1)的时间就能得到堆中的最大值或最小值。

堆经常用来解决在数据集合中找出k个最大值或最小值相关的问题。通常用最大堆找出数据集合中的k个最小值,用最小堆找出数据集合中的k个最大值。

[!WARNING]

题目都是用priority_queue来做的,以后要补一下最大堆最小堆的具体实现。


LCR059-LCR061堆的应用
http://example.com/2024/05/19/posts/LCR059/
作者
Xuan Yang
发布于
2024年5月19日
许可协议