算法训练营(五) 堆、栈、队列、优先队列

目录

零、基础

参考链接

预习题目

实战题目

课后作业

一、优先队列——priority queue(面试常考数据结构)

1.1 基础知识

1.2 预习题

1.2.1 有效的括号

1.2.2 最小栈

1.3 实战题目

1.3.1 柱状图中最大的矩形

1.3.2 滑动窗口最大值

1.4 课后作业

1. 设计循环双端队列

2. 接雨水

后续刷题分类记录

2021 03 06 下一个更大元素 II——思路:单调栈


零、基础

参考链接

预习题目

实战题目

课后作业

实战中经常用到的是双端队列——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 实战题目

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 课后作业

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;
    }
};

  • 2
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值