目录
一、优先队列——priority queue(面试常考数据结构)
零、基础
参考链接
预习题目
实战题目
- https://leetcode-cn.com/problems/largest-rectangle-in-histogram
- https://leetcode-cn.com/problems/sliding-window-maximum
课后作业
- 用 add first 或 add last 这套新的 API 改写 Deque 的代码
- 分析 Queue 和 Priority Queue 的源码
- https://leetcode.com/problems/design-circular-deque
- https://leetcode.com/problems/trapping-rain-water/
实战中经常用到的是双端队列——deque。
一、优先队列——priority queue(面试常考数据结构)
1.1 基础知识
- 1. 插入操作——O(1)
- 2. 取出操作——O(logN)——按照元素的优先级取出
- 3. 底层实现的数据结构较为复杂,可以由heap实现、也可以由bst(二叉搜索树)实现等
1.2 预习题
1.2.1 有效的括号
给定一个只包括 '(',')','{','}','[',']' 的字符串,判断字符串是否有效。
有效字符串需满足:
-
左括号必须用相同类型的右括号闭合。
-
左括号必须以正确的顺序闭合。
-
注意空字符串可被认为是有效字符串。
思路
使用栈来进行解决。
答案
class Solution {
public:
bool isValid(string s) {
if (s.length()%2==1)return false;
std::stack<char>st;
for(char i : s) {
if(i == '(' || i=='{' || i== '[')
st.push(i);
else if ( i == ')') {
if(st.empty() || st.top() != '(') return false;
st.pop();
} else if( i == ']') {
if(st.empty() || st.top() != '[') return false;
st.pop();
} else if( i== '}') {
if(st.empty() || st.top() != '{') return false;
st.pop();
} else {
return false;
}
}
return st.empty();
}
};
1.2.2 最小栈
设计一个支持 push ,pop ,top 操作,并能在常数时间内检索到最小元素的栈。
- push(x) —— 将元素 x 推入栈中。
- pop() —— 删除栈顶的元素。
- top() —— 获取栈顶元素。
- getMin() —— 检索栈中的最小元素。
示例:
输入:
["MinStack","push","push","push","getMin","pop","top","getMin"]
[[],[-2],[0],[-3],[],[],[],[]]输出:
[null,null,null,null,-3,null,0,-2]解释:
MinStack minStack = new MinStack();
minStack.push(-2);
minStack.push(0);
minStack.push(-3);
minStack.getMin(); --> 返回 -3.
minStack.pop();
minStack.top(); --> 返回 0.
minStack.getMin(); --> 返回 -2.
提示
pop、top 和 getMin 操作总是在 非空栈 上调用。
解法
class MinStack {
public:
/** initialize your data structure here. */
MinStack() {}
void push(int x) {
st.push(x);
if(!min.empty()){
if(min.top() < x){
x= min.top();
}
}
min.push(x);
}
void pop() {
st.pop();
min.pop();
}
int top() {
return st.top();
}
int getMin() {
return min.top();
}
private:
std::stack<int>st;
std::stack<int>min;
};
1.3 实战题目
- https://leetcode-cn.com/problems/largest-rectangle-in-histogram
- https://leetcode-cn.com/problems/sliding-window-maximum
1.3.1 柱状图中最大的矩形
给定 n 个非负整数,用来表示柱状图中各个柱子的高度。每个柱子彼此相邻,且宽度为 1 。
求在该柱状图中,能够勾勒出来的矩形的最大面积。
以上是柱状图的示例,其中每个柱子的宽度为 1,给定的高度为 [2,1,5,6,2,3]
。
图中阴影部分为所能勾勒出的最大矩形面积,其面积为 10
个单位。
示例:
输入: [2,1,5,6,2,3] 输出: 10
思路:
1. 暴力求解方法一
for i->0, n-2
for j->i+1, n-1
(i,j)->最小高度,area
update max->area
2.暴力求解方法二
依次找到每根柱子的左右边界(即左侧和右侧比自己短的柱子),从而计算出每根柱子的最大面积。
for i->0, n-1
找到left bound, right bound
area = height[i]*(right - left)
update max->area
3. 栈
为什么可以使用栈来解决?
根据方法二,每次求解每根柱子的左右边界时,都需要进行循环查找,因此在内层多了一个O(n)复杂度。
如果使用栈根据高度由低到高的顺序将柱子的进行存储的话,此时会更容易找到当前柱子的左边界
因此我们可以维护一个栈,栈中元素需要由小到大进行排序。
实现方法:从左到右遍历数组,如果当前数组值大于栈顶元素值,则入栈。否则,进行出栈结算操作,因为当前数组元素是栈顶元素的右边界。
例如图中数组,当6和7入栈之后,当前数组中元素的值位5,5小于7,则将栈顶元素弹出,计算出柱子栈顶元素7可形成的最大面积。
之后继续结算,算出栈顶元素 6 可以围成的最大面积。当6和7都弹出之后,栈顶元素位 -1 ,由于 -1 < 5,因此将5入栈。
class Solution {
public:
int largestRectangleArea(vector<int> &height) {
int ret = 0;
height.push_back(0);
vector<int> index;
for(int i = 0; i < height.size(); i++) {
while(index.size() > 0 && height[index.back()] >= height[i]) {
int h = height[index.back()];
index.pop_back();
int sidx = index.size() > 0 ? index.back() : -1;
if(h * (i-sidx-1) > ret)
ret = h * (i-sidx-1);
}
index.push_back(i);
}
return ret;
}
};
1.3.2 滑动窗口最大值
规律:滑动窗口的题目用双端队列进行处理
给定一个数组 nums,有一个大小为 k 的滑动窗口从数组的最左侧移动到数组的最右侧。你只可以看到在滑动窗口内的 k 个数字。滑动窗口每次只向右移动一位。
返回滑动窗口中的最大值。
进阶:
你能在线性时间复杂度内解决此题吗?
示例:
输入: nums = [1,3,-1,-3,5,3,6,7], 和 k = 3
输出: [3,3,5,5,6,7]
解释:滑动窗口的位置 最大值
--------------- -----
[1 3 -1] -3 5 3 6 7 3
1 [3 -1 -3] 5 3 6 7 3
1 3 [-1 -3 5] 3 6 7 5
1 3 -1 [-3 5 3] 6 7 5
1 3 -1 -3 [5 3 6] 7 6
1 3 -1 -3 5 [3 6 7] 7
提示:
-
1 <= nums.length <= 10^5
-
-10^4 <= nums[i] <= 10^4
-
1 <= k <= nums.length
思路
1. 暴力求解
遍历数组中的元素,遍历到每个元素的时候,或者该元素与后面 k-1 个元素中的最大值。时间复杂度是O(n*k)
for i=0, i<nums.length-k;i++
找出i->k中的最大值
答案
class Solution {
public:
vector<int> maxSlidingWindow(vector<int>& nums, int k) {
int len = static_cast<int>(nums.size());
if(k<1 || len - k +1 <=0) {
return {};
}
std::vector<int>res(len-k+1);
res[0] = *std::max_element(nums.begin(), nums.begin()+k);
for(int i=1; i<=len-k; ++i) {
if(nums[i-1] == res[i-1]) {
res[i] = *std::max_element(nums.begin()+i, nums.begin()+k+i);
}else{
res[i] = std::max(res[i-1], nums[i+k-1]);
}
}
return res;
}
};
2. 双端队列
题解:https://leetcode-cn.com/problems/sliding-window-maximum/solution/dan-diao-dui-lie-by-labuladong/
答案:
class Solution {
public:
vector<int> maxSlidingWindow(vector<int>& nums, int k) {
vector<int> result;
deque<int> deq;
for (int i = 0; i < nums.size(); i++) {
if (!deq.empty() && i-k >= 0 && deq.front() == nums[i-k])
deq.pop_front();
while (!deq.empty() && deq.back() < nums[i])
deq.pop_back();
deq.push_back(nums[i]);
if (i >= k-1)
result.push_back(deq.front());
}
return result;
}
};
1.4 课后作业
- https://leetcode.com/problems/design-circular-deque
- https://leetcode.com/problems/trapping-rain-water/
1. 设计循环双端队列
设计实现双端队列。你的实现需要支持以下操作:
- - MyCircularDeque(k):构造函数,双端队列的大小为k。
- - insertFront():将一个元素添加到双端队列头部。 如果操作成功返回 true。
- - insertLast():将一个元素添加到双端队列尾部。如果操作成功返回 true。
- - deleteFront():从双端队列头部删除一个元素。 如果操作成功返回 true。
- - deleteLast():从双端队列尾部删除一个元素。如果操作成功返回 true。
- - getFront():从双端队列头部获得一个元素。如果双端队列为空,返回 -1。
- - getRear():获得双端队列的最后一个元素。 如果双端队列为空,返回 -1。
- - isEmpty():检查双端队列是否为空。
- - isFull():检查双端队列是否满了。
示例:
MyCircularDeque circularDeque = new MycircularDeque(3); // 设置容量大小为3
circularDeque.insertLast(1); // 返回 true
circularDeque.insertLast(2); // 返回 true
circularDeque.insertFront(3); // 返回 true
circularDeque.insertFront(4); // 已经满了,返回 false
circularDeque.getRear(); // 返回 2
circularDeque.isFull(); // 返回 true
circularDeque.deleteLast(); // 返回 true
circularDeque.insertFront(4); // 返回 true
circularDeque.getFront(); // 返回 4
提示:
- - 所有值的范围为 [1, 1000]
- - 操作次数的范围为 [1, 1000]
- - 请不要使用内置的双端队列库。
思路:
使用两个栈实现
答案
static const auto _ = []()
{
ios::sync_with_stdio(false);
cin.tie(nullptr);
return nullptr;
}();
class MyCircularDeque {
public:
/** Initialize your data structure here. Set the size of the deque to be k. */
MyCircularDeque(int k) {
content = k;
}
/** Adds an item at the front of Deque. Return true if the operation is successful. */
bool insertFront(int value) {
if(front.size() + back.size() < content) {
front.push(value);
return true;
}
return false;
}
/** Adds an item at the rear of Deque. Return true if the operation is successful. */
bool insertLast(int value) {
if(front.size() + back.size() < content) {
back.push(value);
return true;
}
return false;
}
/** Deletes an item from the front of Deque. Return true if the operation is successful. */
bool deleteFront() {
if(front.empty()){
if(!back.empty()) {
putBackToFront();
}else{
return false;
}
}
front.pop();
return true;
}
/** Deletes an item from the rear of Deque. Return true if the operation is successful. */
bool deleteLast() {
if(back.empty()){
if(front.empty()){
return false;
}
putFrontToBack();
}
back.pop();
return true;
}
/** Get the front item from the deque. */
int getFront() {
if(front.empty()){
if(!back.empty()) {
putBackToFront();
}else{
return -1;
}
}
return front.top();
}
/** Get the last item from the deque. */
int getRear() {
if(back.empty()){
if(front.empty()){
return -1;
}
putFrontToBack();
}
return back.top();
}
/** Checks whether the circular deque is empty or not. */
bool isEmpty() {
if(front.empty() && back.empty() ) {
return true;
}
return false;
}
/** Checks whether the circular deque is full or not. */
bool isFull() {
if(front.size() + back.size() < content) {
return false;
}
return true;
}
private:
int content;
stack<int>front;
stack<int>back;
void putBackToFront() {
while(!back.empty()) {
front.push(back.top());
back.pop();
}
}
void putFrontToBack() {
while(!front.empty()) {
back.push(front.top());
front.pop();
}
}
};
2. 接雨水
给定 n 个非负整数表示每个宽度为 1 的柱子的高度图,计算按此排列的柱子,下雨之后能接多少雨水。
上面是由数组 [0,1,0,2,1,0,1,3,2,1,2,1] 表示的高度图,在这种情况下,可以接 6 个单位的雨水(蓝色部分表示雨水)。 感谢 Marcos 贡献此图。
示例:
- 输入: [0,1,0,2,1,0,1,3,2,1,2,1]
- 输出: 6
- 解释:上面是由数组 [0,1,0,2,1,0,1,3,2,1,2,1] 表示的高度图,在这种情况下,可以接 6 个单位的雨水(蓝色部分表示雨水)。
提示:
n == height.length
0 <= n <= 3 * 104
0 <= height[i] <= 105
未完待续
后续刷题分类记录
2021 03 06 下一个更大元素 II——思路:单调栈
给定一个循环数组(最后一个元素的下一个元素是数组的第一个元素),输出每个元素的下一个更大元素。数字 x 的下一个更大的元素是按数组遍历顺序,这个数字之后的第一个比它更大的数,这意味着你应该循环地搜索它的下一个更大的数。如果不存在,则输出 -1。
示例 1:
输入: [1,2,1]
输出: [2,-1,2]
解释: 第一个 1 的下一个更大的数是 2;
数字 2 找不到下一个更大的数;
第二个 1 的下一个最大的数需要循环搜索,结果也是 2。
注意: 输入数组的长度不会超过 10000。
思路:
对于「找最近一个比当前值大/小」的问题,都可以使用单调栈来解决。
单调栈就是在栈的基础上维护一个栈内元素单调。
在理解单调栈之前,我们先回想一下「朴素解法」是如何解决这个问题的:
对于每个数而言,我们需要遍历其右边的数,直到找到比自身大的数,每次找下一个最大值,我们是通过「主动」遍历来实现的。
而如果使用的是单调栈的话,我们将当前还没得到答案的下标暂存于栈内,从而实现「被动」更新答案。也就是说,栈内存放的永远是还没更新答案的下标。
具体的做法是:
维护一个单调栈,由栈顶至栈底由小到大。
- (1)当遍历到数组的一个新的元素时,若栈顶比该元素小,那么对此时的栈顶来说,找到了下一个更大元素,便从栈中弹出。继续判断栈顶是否小于该元素,小于则弹出,直到栈为空或栈顶大于该元素。
- (2)当栈为空或栈顶大于该元素时,直接将该元素入栈。
由于本题中未规定数组中元素无重复,所以为了唯一识别一个元素,栈中需要保存数组的索引,而不是数组元素的值。
答案:
class Solution {
public:
vector<int> nextGreaterElements(vector<int>& nums) {
vector<int>res(nums.size(),-1);
stack<int>st;
for(int i=0; i<nums.size()*2;i++){
int temp=nums[i%nums.size()];
while(!st.empty()&& nums[st.top()]<temp){
res[st.top()]=temp;
st.pop();
}
if(i<nums.size()) st.push(i);
}
return res;
}
};