目录
①力扣1046. 最后一块石头的重量
难度 简单
有一堆石头,每块石头的重量都是正整数。
每一回合,从中选出两块 最重的 石头,然后将它们一起粉碎。假设石头的重量分别为 x
和 y
,且 x <= y
。那么粉碎的可能结果如下:
- 如果
x == y
,那么两块石头都会被完全粉碎; - 如果
x != y
,那么重量为x
的石头将会完全粉碎,而重量为y
的石头新重量为y-x
。
最后,最多只会剩下一块石头。返回此石头的重量。如果没有石头剩下,就返回 0
。
示例:
输入:[2,7,4,1,8,1] 输出:1 解释: 先选出 7 和 8,得到 1,所以数组转换为 [2,4,1,1,1], 再选出 2 和 4,得到 2,所以数组转换为 [2,1,1,1], 接着是 2 和 1,得到 1,所以数组转换为 [1,1,1], 最后选出 1 和 1,得到 0,最终数组转换为 [1],这就是最后剩下那块石头的重量。
提示:
1 <= stones.length <= 30
1 <= stones[i] <= 1000
解析代码
此题其实就是一个模拟的过程: 每次从石堆中拿出最大的元素以及次大的元素,然后将它们粉碎;如果还有剩余,就将剩余的石头继续放在原始的石堆里面。
重复上面的操作,直到石堆里面只剩下一元素,或者没有元素(因为所有的石头可能全部抵消了)。那么主要的问题就是解决:
- 如何顺利的拿出最大的石头以及次大的石头。
- 并且将粉碎后的石头放入石堆中之后,也能快速找到下一轮粉碎的最大石头和次大石头。
此时正好可以利用堆的特性来实现:创建一个大根堆,然后将所有的石头放入大根堆中,每次拿出前两个堆顶元素粉碎一下,如果还有剩余,就将剩余的石头继续放入堆中,这样就能快速地模拟出这个过程。
class Solution {
public:
int lastStoneWeight(vector<int>& stones) {
priority_queue<int> heap(stones.begin(), stones.end());
while(heap.size() > 1)
{
int x1 = heap.top();
heap.pop();
int x2 = heap.top();
heap.pop();
heap.push(x1 - x2);
}
return heap.empty() ? 0 : heap.top();
}
};
②力扣703. 数据流中的第 K 大元素
难度 简单
设计一个找到数据流中第 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 <= 10^4
0 <= nums.length <= 10^4
-10^4 <= nums[i] <= 10^4
-10^4 <= val <= 10^4
- 最多调用
add
方法10^4
次 - 题目数据保证,在查找第
k
大元素时,数组中至少有k
个元素
class KthLargest {
public:
KthLargest(int k, vector<int>& nums) {
}
int add(int val) {
}
};
/**
* Your KthLargest object will be instantiated and called as such:
* KthLargest* obj = new KthLargest(k, nums);
* int param_1 = obj->add(val);
*/
解析代码
一道TopK 问题:
数据结构与算法⑬(第四章_中_续二)堆解决Topk问题+堆的概念选择题_16,23,53 ,31,94,72为什么是堆-优快云博客
class KthLargest
{
// 创建⼀个⼤⼩为 k 的⼩跟堆
priority_queue<int, vector<int>, greater<int>> heap;
int _k;
public:
KthLargest(int k, vector<int>& nums)
: _k(k)
{
for(auto& e : nums)
{
heap.push(e);
if(heap.size() > _k)
{
heap.pop();
}
}
}
int add(int val)
{
heap.push(val);
if(heap.size() > _k)
{
heap.pop();
}
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);
*/
③力扣692. 前K个高频单词
难度 中等
给定一个单词列表 words
和一个整数 k
,返回前 k
个出现次数最多的单词。
返回的答案应该按单词出现频率由高到低排序。如果不同的单词有相同出现频率, 按字典顺序 排序。
示例 1:
输入: words = ["i", "love", "leetcode", "i", "love", "coding"], k = 2 输出: ["i", "love"] 解析: "i" 和 "love" 为出现次数最多的两个单词,均为2次。 注意,按字母顺序 "i" 在 "love" 之前。
示例 2:
输入: ["the", "day", "is", "sunny", "the", "the", "the", "sunny", "is", "is"], k = 4 输出: ["the", "is", "sunny", "day"] 解析: "the", "is", "sunny" 和 "day" 是出现次数最多的四个单词, 出现次数依次为 4, 3, 2 和 1 次。
注意:
1 <= words.length <= 500
1 <= words[i] <= 10
words[i]
由小写英文字母组成。k
的取值范围是[1, 不同 words[i] 的数量]
进阶:尝试以 O(n log k)
时间复杂度和 O(n)
空间复杂度解决。
class Solution {
public:
vector<string> topKFrequent(vector<string>& words, int k) {
}
};
解析代码
一道Topk问题的拓展,有点考验语法能力:
稍微处理一下原数组:
- 需要知道每一个单词出现的频次,因此可以先使用哈希表,统计出每一个单词出现的频次。
- 然后在哈希表中,选出前 k 大的单词(为什么不在原数组中选?因为原数组中存在重复的单词,哈希表里面没有重复单词,并且还有每一个单词出现的频次)
如何使用堆,拿出前 k 大元素:
一、先定义一个自定义排序,我们需要的是前 k 大,因此需要一个小根堆。但是当两个字符串的频次相同的时候,我们需要的是字典序较小的,此时是一个大根堆的属性,在定义比较函数的时候需要注意。
- 当两个字符串出现的频次不同的时候:需要的是基于频次比较的小根堆
- 当两个字符串出现的频次相同的时候:需要的是基于字典序比较的大根堆
二、定义好比较器之后,依次将哈希表中的字符串插入到堆中,维持堆中的元素不超过 k 个。
三、遍历完整个哈希表后,堆中的剩余元素就是前 k 大的元素
class Solution {
public:
struct cmp
{
bool operator()(const pair<string, int>& p1, const pair<string, int>& p2)
{
if(p1.second == p2.second) // 频次相同,字典序排序,小的在前,大根堆less
return p1.first < p2.first;
return p1.second > p2.second; // 频次大的在前->小根堆greater
}
};
vector<string> topKFrequent(vector<string>& words, int k) {
unordered_map<string, int> hash;
for(auto& e : words) // 字符和频次映射到哈希表
{
hash[e]++;
}
priority_queue<pair<string, int>, vector<pair<string, int>>, cmp> heap;
for(auto& e : hash) // Topk主逻辑
{
heap.push(e);
if(heap.size() > k)
heap.pop();
}
vector<string> ret(k); // 提取结果返回
for(int i = k - 1; i >= 0; --i)
{
ret[i] = heap.top().first;
heap.pop();
}
return ret;
}
};
④力扣295. 数据流的中位数
难度 困难
中位数是有序整数列表中的中间值。如果列表的大小是偶数,则没有中间值,中位数是两个中间值的平均值。
- 例如
arr = [2,3,4]
的中位数是3
。 - 例如
arr = [2,3]
的中位数是(2 + 3) / 2 = 2.5
。
实现 MedianFinder 类:
-
MedianFinder()
初始化MedianFinder
对象。 -
void addNum(int num)
将数据流中的整数num
添加到数据结构中。 -
double findMedian()
返回到目前为止所有元素的中位数。与实际答案相差10-5
以内的答案将被接受。
示例 1:
输入 ["MedianFinder", "addNum", "addNum", "findMedian", "addNum", "findMedian"] [[], [1], [2], [], [3], []] 输出 [null, null, null, 1.5, null, 2.0] 解释 MedianFinder medianFinder = new MedianFinder(); medianFinder.addNum(1); // arr = [1] medianFinder.addNum(2); // arr = [1, 2] medianFinder.findMedian(); // 返回 1.5 ((1 + 2) / 2) medianFinder.addNum(3); // arr[1, 2, 3] medianFinder.findMedian(); // return 2.0
提示:
-10^5 <= num <= 10^5
- 在调用
findMedian
之前,数据结构中至少有一个元素 - 最多
5 * 10^4
次调用addNum
和findMedian
class MedianFinder {
public:
MedianFinder() {
}
void addNum(int num) {
}
double findMedian() {
}
};
/**
* Your MedianFinder object will be instantiated and called as such:
* MedianFinder* obj = new MedianFinder();
* obj->addNum(num);
* double param_2 = obj->findMedian();
*/
解析代码
此题有三个解法,三个解法的find函数的时间复杂度都是O(1)。看看add的时间复杂度:
- 第一个解法是进来一个数,sort一下,这样add的时间复杂度是O(N*logN)。
- 第二个解法是插入排序的思想,这样add的时间复杂度是O(N)。
- 第三个解法是用两个堆来维护中位数,这样add的时间复杂度是O(logN)。
第三个解法难想,但是做完这道题以后能想起来就行。这里用第三个解法:
将整个数组按照大小平分成两部分(如果不能平分,那就让较小部分的元素多一个), 较小的部分称为左侧部分,较大的部分称为右侧部分:
将左侧部分放入大根堆中,然后将右侧元素放入小根堆中,这样就能在 O(1) 的时间内拿到中间的一个数或者两个数,进而求的平均数。
于是问题就变成了如何将一个一个从数据流中过来的数据,动态调整到大根堆或者小根堆中,并且保证两个堆的元素一致,或者左侧堆的元素比右侧堆的元素多一个。
为了方便叙述,将左侧的大根堆记为 left ,右侧的小根堆记为 right ,数据流中来的数据记为 x 。
其实,就是分类讨论的过程:
- 如果左右堆的数量相同, left.size() == right.size() :
如果两个堆都是空的,直接将数据 x 放入到 left 中; 如果两个堆非空:
- 如果元素要放入左侧,也就是 x <= left.top() :那就直接放,因为不会影响我们制定的规则;
- 如果要放入右侧可以先将 x 放入 right 中, 然后把 right 的堆顶元素放入 left 中
- 如果左右堆的数量不相同。那就是 left.size() = right.size() + 1:
这个时候我们关心的是 x 是否会放入 left 中,导致 left 变得过多:
- 如果 x 放入 right (小根堆)中,也就是 x >= right.top() ,直接放。
- 反之,就是需要放入 left 中: 可以先将 x 放入 left 中,然后把 left 的堆顶元素放入 right 中。
只要每一个新来的元素按照上述规则执行,就能保证 left 中放着整个数组排序后的左半部分, right 中放着整个数组排序后的右半部分,就能在 O(1)的时间内求出平均数,且插入的时间复杂度首O(logN)。
class MedianFinder {
// 较小的部分称为左侧部分,较大的部分称为右侧部分:
// 将左侧部分放入大根堆中,然后将右侧元素放入小根堆中,
priority_queue<int> left; // ⼤根堆
priority_queue<int, vector<int>, greater<int>> right; // ⼩根堆
public:
MedianFinder() {
}
void addNum(int num) {
if(left.size() == right.size())
{
if(right.empty() || left.top() > num)
{
left.push(num);
}
else
{
right.push(num);
left.push(right.top());
right.pop();
}
}
else
{
if(left.top() > num)
{
left.push(num);
right.push(left.top());
left.pop();
}
else
{
right.push(num);
}
}
}
double findMedian() {
if(left.size() == right.size())
return (left.top() + right.top()) / 2.0;
return left.top();
}
};
/**
* Your MedianFinder object will be instantiated and called as such:
* MedianFinder* obj = new MedianFinder();
* obj->addNum(num);
* double param_2 = obj->findMedian();
*/
本篇完。
下一篇动态规划系列的是两个数组dp类型的OJ。
下下篇是BFS解决FloodFill算法。