栈、队列专题

文章目录

    • 栈的概述
    • 栈的实现
    • 栈在函数调用中的应用
    • 栈在表达式求值中的应用
      • 逆波兰表达式求值
    • 栈在括号匹配中的应用
      • 有效的括号
      • 最长的有效括号
      • 删除字符串中的所有相邻重复项
    • 如何获取栈内最小元素呢
    • 如何实现浏览器的前进和后退
  • 队列
    • 队列的定义
    • 队列的实现
    • 循环队列
    • 队列的应用
    • 队列在线程池等有限资源池中的应用
    • 设计循环双端队列
    • 滑动窗口最大值
  • 用栈实现队列
  • 用队列实现栈

之前一篇文章介绍了" 数组和链表",今天继续介绍"栈和队列"。

我们先来看一个有趣的现象。

  • 首先打开"百度"随意搜索一个名词
    在这里插入图片描述

  • 点击"视频"
    在这里插入图片描述

  • 点击"文库"
    在这里插入图片描述

  • 点击"<-“箭头,看看会发生什么。回退到了"视频"页面并且出现了”->"箭头。
    在这里插入图片描述

  • 再次点击"<-“箭头,依然会回退到上一个页面"网页”。如果点击"->“箭头则会前进到下一个页面"文库”。这里就不演示了。

  • 此时点击"图片"看看会发生什么。"->"箭头消失了。
    在这里插入图片描述

  • 点击"<-“箭头,回退到了"视频页面”。再点击"<-"箭头回退到了"网页"页面,但是"文库"页面丢失了,找不到了。

  • 这是怎么实现的呢?我们一起来看看吧

栈的概述

上一篇文章提到过,栈和队列都是操作受限的线性表。栈只允许在线性表的一端进行删除和增加操作,另外一端是封闭的状态。就像是一摞叠在一起的盘子,放的时候只能从下往上一个一个放;取的时候,从上往下一个一个取;既不能从中间放也不能从中间抽取;像这种"先进后出,后进先出"的结构就是栈。

在这里插入图片描述

栈的实现

栈既可以基于数组实现,也可以基于链表实现。用数组实现的栈叫做顺序栈,用链表实现的栈叫做链式栈。
栈的基本操作有以下几个

  • 判断栈是否为空
  • 判断栈是否已满[链式栈无需判断,顺序栈需要判断]
  • 获取栈的元素个数
  • 获取栈顶元素
  • 入栈
  • 出栈

以下是一个简单的顺序栈的实现

#include<iostream>
using namespace std;

template <typename T>
class mystack{
public:
        mystack(int _c):cap(_c),in(0),st(new T[_c]){}
        ~mystack(){delete []st;}
        bool empty();
        int size();
        bool full();
        void push(T);
        void pop();
        T top();
private:
        int cap;//the cap of stack
        int in;//the top of stack
        T *st;
};
template <typename T>
bool mystack<T>::empty(){
        return this->in==0;
}
template <typename T>
int mystack<T>::size(){
        return this->in;
}
template <typename T>
bool mystack<T>::full(){
        return this->in==this->cap;
}

template <typename T>
void mystack<T>::push(T a){
        st[this->in++]=a;
}
template <typename T>
void mystack<T>::pop(){
        --this->in;
}

template <typename T>
T mystack<T>::top(){
        return st[in-1];
}    

如果想要实现动态扩容栈,就让底层的数组实现动态扩容即可。

栈在函数调用中的应用

栈作为一个比较基础的数据结构,应用场景很多。其中,比较经典的一个应用场景就是函数调用栈[也是递归程序实现的基础]。
操作系统给每个线程分配了一块独立的内存空间,这块内存被组织成"栈"这种结构,用来存储函数调用时的局部变量也称之为临时变量。每进入一个函数之前,就会将调用者作为一个栈帧入栈,当被调用函数执行完之后,将这个函数对应的栈帧出栈。

为了更好的理解栈在函数中调用中的应用,观察下面的程序,main函数调用add函数,返回数值存储到变量ret,变量a与ret相加存储到res变量,最后打印res的数值。

int main() {
	int a = 1; 
 	int ret = 0;
 	int res = 0;
 	ret = add(3, 5);
 	res = a + ret;
 	printf("%d", res);
 	reuturn 0;
}
int add(int x, int y) {
 	int sum = 0;
 	sum = x + y;
 	return sum;

以下是main函数调用add函数时,函数调用栈的情况。
在这里插入图片描述

栈在表达式求值中的应用

为了方便解释,将算术表达式简化为只包含加减乘除四则运算,比如"13+3*9+44-24/4"。对于这个四则运算,人脑可以很快地求解出答案,但是对于计算机来说,理解这个表达式本身就很困难。那我们该如何编写程序才能让计算机理解呢?需要借助什么样的数据结构呢?

实际上,可以通过两个栈来实现。一个用来保存操作数称之为操作数栈,另一个用来保存运算符称之为运算符栈,乘除的运算优先级高于加减。从左向右遍历表达式,当遇到数字,直接压入操作数栈;遇到运算符,先与运算符栈顶运算符进行比较:如果比运算符栈顶运算符的优先级高,就将当前运算符压栈;如果比运算符栈顶元素的优先级低或者相同,从运算符栈中取栈顶运算符,从操作数栈的栈顶取出两个操作数,然后进行计算,在把计算完的结果压入操作数栈,继续上述的步骤直到当前运算符优先级高于栈顶运算符优先级。

这里有一个小细节需要注意:栈结构的特点是"先进后出",所以从操作数栈取出的第一个整数是运算符右边的操作数,取出的第二个操作数才是运算符左边的操作数。对于"+“操作,不必特别区分左右操作数,但是对于”-,*,/"操作必须要区分左右操作数,否则会计算出错。

为了加深理解,对于表达式"5+3*8-8"的计算过程,画成了一张图,如下所示。
在这里插入图片描述

  • 第五个步骤。遇到运算符’-‘,优先级低于"*",取出操作数"8[右操作数]和3[左操作数]"进行乘法运算的24,将24压入操作数栈。’-‘优先级等于’+',取出操作数"24和5"进行加法运算的29,29重新压入操作数栈。
  • 第六个步骤。取出操作数"8[右操作数]和29[左操作数]"进行减法操作,得到最后结果21.

分析完了"栈在表达式求值中的作用",来做一道算法题吧

逆波兰表达式求值

https://leetcode.cn/problems/evaluate-reverse-polish-notation/description/

由于给定的表达式是一个后缀表达式所以只需要一个操作数栈即可
后缀表达式:是指运算符在操作数的后边,按顺序遍历遇到操作数进栈,遇到运算符直接出栈取数运算即可。

class Solution {
public:
    int evalRPN(vector<string>& tokens) {
        stack<int> nums;//操作数栈
        int left,right,sum;//左右操作数和计算结果
        for(const string&to:tokens){
            if(!(to=="+" || to=="-" || to=="*" || to=="/")){
                //数字
                int te =stoi(to);
                nums.push(te);
                continue;
            }
            right = nums.top();nums.pop();
            left = nums.top();nums.pop();
            if(to=="+"){
                sum=left+right;
            }else if(to=="-"){
                sum=left-right;
            }else if(to=="*"){
                sum=left*right;
            }else if(to=="/"){
                sum=left/right;
            }
            nums.push(sum);
        }
        return nums.top();
    }
};
func evalRPN(tokens []string) int {
    //用切片模拟栈
    st:=make([]int,0,len(tokens))
    var left,right,sum int

    for _,to:=range tokens{
        if !(to=="+" || to=="-" || to=="*" || to=="/"){
            te,_:=strconv.Atoi(to)
            st=append(st,te)
            continue
        }
        right = st[len(st)-1]
        left = st[len(st)-2]
        st=st[:len(st)-2]
        if to=="+"{
            sum=left+right
        }else if to=="-"{
            sum=left-right
        }else if to=="*"{
            sum=left*right
        }else if to=="/"{
            sum=left/right
        }
        st=append(st,sum)
    }

    return st[0]
}

栈在括号匹配中的应用

除了用栈实现表达式求值,还可以借助栈检查表达式中的括号是否匹配。

假设括号的类型只有三种:圆括号()、方括号[]、花括号{},并且它们可以任意嵌套。比如{[()]}、[({}),{()}]等都为合法格式,但是[(}]、[{})等是不合法的格式。那我们如何检查一个括号是否是合法的格式呢?

这里也可以使用栈解决。用栈保存未匹配的左括号,从左到右依次扫描字符串。扫描到左括号入栈,扫描到右括号从栈顶取出一个左括号,如果左右括号不匹配则括号不合法,如果合法继续向后扫描。扫描完之后发现栈不为空则括号不合法[左括号比右括号多],反之则合法。

为了方便理解,对于括号"[({})]"的合法性验证,画成如下一张图

在这里插入图片描述

  • 推算到最后栈为空,括号合法

分析完了"栈在括号匹配中的作用",来做一道算法题吧

有效的括号

https://leetcode.cn/problems/valid-parentheses/description/

class Solution {
public:
    bool isValid(string s) {
        stack<char> st;char te;
        for (const char&ss:s){
            if(ss=='[' || ss=='(' || ss=='{') st.push(ss);
            else {
                if(st.empty()) return false;
                te=st.top();st.pop();
                if(ss==']'){
                    if(te!='[') return false;
                }else if(ss==')'){
                    if(te!='(') return false;
                }else if(ss=='}'){
                    if(te!='{') return false;
                }
            }
        }
        return st.empty();
    }
};
func isValid(s string) bool {
    //用slice模拟栈
    st:=make([]rune,0,len(s))
    for _,ss:=range s{
        if ss=='[' || ss=='(' || ss=='{'{
            st=append(st,ss)
        }else{
            if len(st)==0 {return false}
            te:=st[len(st)-1]
            st = st[:len(st)-1]
            switch ss{
                case ']':
                    if te!='[' {return false}
                case ')':
                    if te!='(' {return false}
                case '}':
                    if te!='{' {return false}
                default:
                
            }
        }
    }
    return len(st)==0
}

最长的有效括号

https://leetcode.cn/problems/longest-valid-parentheses/

思路:

  • 除了要考虑左右括号是否匹配之外,还需要考虑这些匹配的括号是否连续,连续的话累加计数,不连续的话重新计数。从左向右遍历,有一下两种情况:
  • 遇到的是"("。左括号必须要找到匹配的右括号才能够算是一对有效的括号,所以直接将其入栈等待被匹配。
  • 遇到的是")“。栈顶元素出栈,如果栈顶元素是”("则找到一对有效的括号。

以上只是简单的思路,现在考虑一些需要注意的细节。

  • 由于有效括号要求格式正确且连续,为了连续我们不能只是简单的判断左右括号是否匹配,还需要考虑它们所在的位置也就是下标。那不妨存储括号的下标而不是括号本身[因为括号无非两种"(“或者”)"]。
  • 当遇到的是"("将其下标存储在栈中。
  • 当遇到的是")"。直接出栈,说明找到一对有效括号。此时有效括号的长度就是i-st.top。出栈之后栈空需要将当前的下标入栈。[出现这种情况就是右括号比左括号多]
  • 为了考虑")“、”()"的特殊情况,需要在开始的时候将-1入栈。
class Solution {
public:
    int longestValidParentheses(string s) {
        stack<int> st;st.push(-1);
        int len=0;
        for(int i=0;i<s.size();++i){
            if(s[i]=='('){
                st.push(i);
            }else {
                st.pop();
                if(st.empty()) {
                    st.push(i);
                }else{
                    len=max(len,i-st.top());
                }
            }
        }
        return len;
    }
};
func longestValidParentheses(s string) int {
    st:=make([]int,0,len(s))
    st=append(st,-1)
    maxlen:=0
    for i,_:=range s{
        if s[i]=='('{
            st=append(st,i)
        }else {
            st=st[:len(st)-1]
            if len(st)==0{
                st=append(st,i)
            }else {
                maxlen=max(maxlen,i-st[len(st)-1])
            }
        }
    }
    return maxlen
}
func max(a,b int)int{
    if a>b{
        return a
    }
    return b
}

删除字符串中的所有相邻重复项

class Solution {
public:
    string removeDuplicates(string s) {
        stack<char> st;
        for(const char&ss:s){
            if (st.empty() || ss!=st.top()) st.push(ss);
            else while(!st.empty()&&ss==st.top()) st.pop();
        }
        string ans(st.size(),' ');
        int in = st.size()-1;
        while(!st.empty()) {ans[in--]=st.top();st.pop();}
        return ans;
    }
};
func removeDuplicates(s string) string {
    st:=make([]int32,0,len(s))
    for _,ss:=range s{
        if len(st)==0 || ss!=st[len(st)-1] {
            st=append(st,ss)
        }else{
            for len(st)!=0 && ss==st[len(st)-1]{
                st=st[:len(st)-1]
            }
        }
    }
    res:=""
    for _,ss:=range st{
        res+=string(ss)
    }
    return res
}

如何获取栈内最小元素呢

由于栈结构"先进后出"的特性,决定了访问栈内的元素必须按照顺序,并且每访问一个元素都需要将其从栈中丢弃,才能继续访问下一个元素,那对于这种操作受限的结构无法直接遍历,该如何找到最小的元素呢。

实际上可以使用两个栈来解决,假设现有A、B两个栈。

  • 新元素加入先加入到A栈;
  • 要想获取到栈内最小元素需要借助辅助栈B。新元素加入到A栈的同时,将其加入到B栈。若B栈为空直接加入,否则元素小于等于栈顶元素直接入栈,大于栈顶元素忽略。
  • 出栈操作。从A栈出栈元素,如果A出栈的元素和B栈栈顶元素吻合,B栈也要执行出栈操作,反之不用。

为了帮助理解我画了一张演示图
在这里插入图片描述
理解了上述的思路我们来编程实现一下
https://leetcode.cn/problems/bao-han-minhan-shu-de-zhan-lcof/description/

class MinStack {
public:
    /** initialize your data structure here. */
    MinStack() {

    }
    
    void push(int x) {
        a.push(x);
        if(b.empty() || x<=b.top()) b.push(x);
    }
    
    void pop() {
        int x=a.top();a.pop();
        if (x==b.top()) b.pop();
    }
    
    int top() {
        if(a.empty()) return -1;
        return a.top();
    }
    
    int getMin() {
        return b.top();
    }
private:
    stack<int>a;
    stack<int>b;
};


type MinStack struct {
    a,b []int
}


/** initialize your data structure here. */
func Constructor() MinStack {
    return MinStack{
        a:[]int{},
        b:[]int{},
    }
}


func (this *MinStack) Push(x int)  {
    this.a=append(this.a,x)
    if len(this.b)==0 || x<=this.b[len(this.b)-1] {
        this.b=append(this.b,x)
    }
}


func (this *MinStack) Pop()  {
    x:=this.a[len(this.a)-1]
    this.a=this.a[:len(this.a)-1]
    if x==this.b[len(this.b)-1]{this.b=this.b[:len(this.b)-1]}
}


func (this *MinStack) Top() int {
    return this.a[len(this.a)-1]

}


func (this *MinStack) GetMin() int {
    return this.b[len(this.b)-1]

}



如何实现浏览器的前进和后退

看到这里相信你已经了解了栈的概念和特性。那现在来分析如何实现开头说的那个有趣的现象。

  • 实际上,可以使用两个栈来实现。
  • 使用两个栈X和Y,把首页浏览的页面a,b,c依次压入栈X,当点击后退按钮时,再依次从栈X中出栈并加入栈Y。当点击前进按钮时,依次从栈Y中取出数据并加入栈X。栈X中没有数据不能再进行后退,栈Y中没有数据不能进行前进。

接下来用图演示一下

  • 假设顺序查看了a,b,c三个页面,依次把a,b,c压入栈中。两个栈的数据如下

在这里插入图片描述

  • 通过浏览器的后退按钮,从页面c回退到页面a,旧依次把b\c从x栈中弹出,依次放入到y栈中。两个栈的数据如下:

在这里插入图片描述

  • 这个时候想看页面b,于是点击前进按钮进入到页面b。此时两个栈的数据如下所示

在这里插入图片描述

  • 这个时候通过页面b跳转到页面e,这个时候Y栈被清空了,所以页面c就找不到了。两个栈的数据如下

在这里插入图片描述

队列

进入正题之前,先考虑这样一个问题。计算机一旦启动,就无时无刻不在处理着任务。例如,打开某个应用程序,点击某个页面,点击某个图片,向键盘敲下的每一个字符等等,对计算机来说都是一系列的任务。那计算机该如何处理这些任务的,学过操作系统的都知道,计算机会为每一个任务分配一个线程。那么任务的处理速度与线程的数量成正比吗?实际上并不是的,因为线程需要得到CPU的调度,但是CPU的资源是有限的,过多的线程还会导致频繁的上下文切换,导致效率低下。所以引入了线程池,也即是一个固定大小的池子中有一定数量的创建好的线程,这一步是为了减少线程频繁创建和销毁的开销。当向固定大小的线程池中请求一个线程时,如果线程池中没有空闲的资源,这个时候线程池该如何处理这个请求?是拒绝请求还是排队请求?各种处理策略又是如何实现的?带着这些疑问,学习队列吧

队列的定义

队列与栈一样都是操作受到限制的线性表,队列是一种"先进先出,后进后出"的结构,就像是排队买糕点,先来的人先买到后来的人后买到。入队的时候从队尾进入,出队的时候从队头出去。

下图把"队列和栈"放在一起对比
在这里插入图片描述

队列的实现

队列和栈一样,也有顺序队列和链式队列两种实现。一些基本操作如下:

  • 判断队列是否为空
  • 判断队列是否已满[链式队列无需判断]
  • 获取队头元素
  • 入队
  • 出队
  • 获取队列元素总数

以下是是一个顺序队列的实现

#include<iostream>
using namespace std;

template <typename T>
class mystack{
public:
        mystack(int _c):cap(_c),head(0),tail(0),st(new T[_c]){}
        ~mystack(){delete[] st;}
        bool empty();
        bool full();
        int size();
        void push(T);
        void pop();
        T front();
private:
        int cap;//the cap of stack
        int head,tail;
        T *st;
};
template <typename T>
bool mystack<T>::empty(){
        return tail==0;
}
template <typename T>
bool mystack<T>::full(){
        return tail==cap;
}
template <typename T>
int mystack<T>::size(){
        return tail-head+1;
}

template <typename T>
void mystack<T>::push(T a){
        st[tail++]=a;
}
template <typename T>
void mystack<T>::pop(){
        ++head;
}

template <typename T>
T mystack<T>::top(){
        return st[head];
}

  • 从代码中可以发现,这样的实现方式有一定的弊端:刚开始一直向队尾添加元素直到队列满,此时从队头出队元素直到元素都出队。此时再向队列中添加元素,由于"判断出栈是满的"而无法将元素入队。
  • 可以发现这样的实现方式,非常浪费空间。打个比方,就像是排队去买冰激淋,每服务完一个客户,店家就像前走一步服务下一个客户,这样就导致有能力服务更多客户的时候,却因为没有位置而失去这些客户。
  • 那根据生活中的尝试,前一个客户走了,后面的客户向前移动不久好了。理论上这样做没错,但是如果元素很多的时候会是不小的开销,如果频繁的执行出队元素,这样的开销更大。
  • 为了解决上述两种实现方案的弊端,引入了循环队列

循环队列

"循环队列"顾名思义就是,队头队尾相连接形成一个环状的结构。接下来看看如何实现吧

  • 设置循环队列有一个需要注意的点,队列为空的时候队列的头尾指针重合,队列满的时候队列的头尾指针也重合,所以需要一些特殊的标识区分两种情况。
  • 第一种方法就是,设置一个变量size表示队列中的元素个数,如果头尾指针重合的时候size达到队列的容量那么队列是一种满的状态,否则就是空的状态。
  • 第二种方法就是,将队列的一个位置空出来不存储任何元素,这样当头尾指针重合就是队列空的状态,尾指针的下一个位置是头指针就是满的状态。[由于空出一个位置,所以如果队列需要多申请一块空间]

https://leetcode.cn/problems/design-circular-queue/description/

size变量

class MyCircularQueue {
public:
    MyCircularQueue(int k):cap(k),size(0),head(0),tail(0),buf(new int[k]) {

    }
    ~MyCircularQueue(){delete []buf;}
    
    bool enQueue(int value) {
        if(isFull()) return false;
        buf[tail]=value;
        tail=(tail+1)%cap;
        ++size;
        return true;
    }
    
    bool deQueue() {
        if(isEmpty()) return false;
        head=(head+1)%cap;
        --size;
        return true;
    }
    
    int Front() {
        if(isEmpty()) return -1;
        return buf[head];
    }
    
    int Rear() {
        if(isEmpty()) return -1;
        return buf[(tail-1+cap)%cap];
    }
    
    bool isEmpty() {
        return head==tail && size==0;
    }
    
    bool isFull() {
        return head==tail && size==cap;
    }
private:
    int cap;
    int size;
    int head,tail;
    int *buf;
};


type MyCircularQueue struct {
    cap,size int
    head,tail int
    buf []int
}


func Constructor(k int) MyCircularQueue {
    return MyCircularQueue{
        cap:k,
        size:0,
        head:0,
        tail:0,
        buf:make([]int,k),
    }
}


func (this *MyCircularQueue) EnQueue(value int) bool {
    if this.IsFull(){return false}
    this.buf[this.tail]=value
    this.tail=(this.tail+1)%this.cap
    this.size++
    return true
}


func (this *MyCircularQueue) DeQueue() bool {
    if this.IsEmpty(){return false}
    this.head=(this.head+1)%this.cap
    this.size--
    return true
}


func (this *MyCircularQueue) Front() int {
    if this.IsEmpty() {return -1}
    return this.buf[this.head]
}


func (this *MyCircularQueue) Rear() int {
    if this.IsEmpty() {return -1}
    return this.buf[(this.tail-1+this.cap)%this.cap]
}


func (this *MyCircularQueue) IsEmpty() bool {
    return this.head==this.tail && this.size==0
}


func (this *MyCircularQueue) IsFull() bool {
    return this.tail ==this.head && this.size==this.cap
}



空出一个位置

class MyCircularQueue {
public:
    MyCircularQueue(int k):size(k+1),head(0),tail(0),buf(new int[k+1]) {

    }
    ~MyCircularQueue(){delete []buf;}
    
    bool enQueue(int value) {
        if(isFull()) return false;
        buf[tail]=value;
        tail=(tail+1)%size;
        return true;
    }
    
    bool deQueue() {
        if(isEmpty()) return false;
        head=(head+1)%size;
        return true;
    }
    
    int Front() {
        if(isEmpty()) return -1;
        return buf[head];
    }
    
    int Rear() {
        if(isEmpty()) return -1;
        return buf[(tail-1+size)%size];
    }
    
    bool isEmpty() {
        return head==tail;
    }
    
    bool isFull() {
        return (tail+1)%size == head;
    }
private:
    int size;
    int head,tail;
    int *buf;
};


type MyCircularQueue struct {
    cap int
    head,tail int
    buf []int
}


func Constructor(k int) MyCircularQueue {
    return MyCircularQueue{
        cap:k+1,
        head:0,
        tail:0,
        buf:make([]int,k+1),
    }
}


func (this *MyCircularQueue) EnQueue(value int) bool {
    if this.IsFull(){return false}
    this.buf[this.tail]=value
    this.tail=(this.tail+1)%this.cap
    return true
}


func (this *MyCircularQueue) DeQueue() bool {
    if this.IsEmpty(){return false}
    this.head=(this.head+1)%this.cap
    return true
}


func (this *MyCircularQueue) Front() int {
    if this.IsEmpty() {return -1}
    return this.buf[this.head]
}


func (this *MyCircularQueue) Rear() int {
    if this.IsEmpty() {return -1}
    return this.buf[(this.tail-1+this.cap)%this.cap]
}


func (this *MyCircularQueue) IsEmpty() bool {
    return this.head==this.tail
}


func (this *MyCircularQueue) IsFull() bool {
    return (this.tail+1)%this.cap==this.head
}



队列的应用

队列的应用非常广泛,特别是一些具有某些特殊特性的队列,比如刚刚介绍的循环队列、阻塞队列、并发队列。在很多偏底层系统,框架、中间件的开发中,起着关键性的作用。

  • 阻塞队列其实就是在队列基础上增加了阻塞操作。简单来说,就是队列为空的时候,从队头取数据会被阻塞直到有数据可取。队列满的时候,插入数据会被阻塞,直到队列中有空闲位置。我们可以使用阻塞队列来实现一个"生产者-消费者"模型。
    基于阻塞队列实现的"生产者-消费者模型",可以有效协调生产和消费的速度。当"生产者"生产数据的速度过快,"消费者"来不及消费的时候,存储数据的队列很快就满。生产者就会被阻塞直到消费者消费了数据,生产者才会被唤醒继续生产。

  • 在多线程情况下,会有多个线程同时操作队列,这个时候就会存在线程安全问题,如何实现一个线程安全的队列?
    线程安全的队列叫做并发队列。最简单直接的方式是在执行"入队"和"出队"操作之前加锁保证线程安全。但是锁的颗粒度比较大导致并发度低,同一时刻允许一个操作要么入队要么出队。实际上,基于数组的循环队列,利用CAS操作,可以实现非常高效的并发队列。这也是循环队列比链式队列更加广泛的原因。

队列在线程池等有限资源池中的应用

队列的知识就介绍完了,现在看看如何解决讲解队列之前提出的那个问题"为了防止线程的数量过多导致频繁的创建销毁和切换,利用线程池提前创建好一些线程。如果此时线程池没有空闲的线程,是拒绝到来的请求还是将其排队。"
一般有以下两种处理策略。

  • 第一种是非阻塞的处理方式,直接决绝找不到空闲线程的请求。
  • 第二种是阻塞的处理方式。将暂时找不到空闲线程的请求排队,等到有空闲线程的时候,按序取出排队的请求继续处理。那该如何存储这些排队的请求呢?
  • 可以类比生活中的排队情景,谁来的早谁先被服务,也就是"先进先出,后进后出"的特性,所以可以使用队列来存储这些排队的请求。
  • 队列有两种实现,一种基于链表,一种基于数组。基于链表的实现方式,可以实现一个不限长度的无界队列,但可能对导致排队的请求过多,使得请求的响应时间过长。所以对响应时间要求比较高的系统,不适合使用链表实现的队列。
  • 基于数组的实现方式,可以实现一个固定容量的有界队列,排队的请求一旦超过队列的容量就会被丢弃。要根据应用的场景设置一个合适的容量,设置的太大会导致排队的请求过多,设置的太小会导致很多请求被丢弃无法充分利用系统资源。

实际上,对于大部分资源有限的场景,当没有空闲资源时,都可以通过队列这种数据结构实现请求的排队。队列除了应用在线程池中还可以应用在数据库连接池中。

设计循环双端队列

前面我们分析过"循环队列"的实现,为了区分队满和队空的时候头尾指针重合的情况,空出一个位置或者用一个额外的变量记录队列中的元素个数。循环双端队列也一样需要考虑这些,相比与循环队列唯一不同的就是头尾都可以插入和删除元素。
使用size

class MyCircularDeque {
public:
    MyCircularDeque(int k):cap(k),size(0),head(0),tail() ,buf(new int[k]){

    }
    
    bool insertFront(int value) {
        if(isFull()) return false;
        buf[head]=value;
        head=(head-1+cap)%cap;
        ++size;
        if(size==1) tail=(tail+1)%cap;
        return true;
    }
    
    bool insertLast(int value) {
        if(isFull()) return false;
        buf[tail]=value;
        tail=(tail+1)%cap;
        ++size;
        if(size==1) head=(head-1+cap)%cap;
        return true;
    }
    
    bool deleteFront() {
        if(isEmpty()) return false;
        head=(head+1)%cap;
        --size;
        if(size==0) tail=head;
        return true;
    }
    
    bool deleteLast() {
        if(isEmpty()) return false;
        tail=(tail-1+cap)%cap;
        --size;
        if(size==0) head=tail;
        return true;
    }
    
    int getFront() {
        if(isEmpty()) return -1;
        return buf[(head+1)%cap];
    }
    
    int getRear() {
        if(isEmpty()) return -1;
        return buf[(tail-1+cap)%cap];
    }
    
    bool isEmpty() {
        return size==0;
    }
    
    bool isFull() {
        return size==cap;
    }
private:
    int cap,size;
    int head,tail;
    int *buf;
};

空出一个位置

class MyCircularDeque {
public:
    MyCircularDeque(int k):cap(k+1),head(0),tail() ,buf(new int[k+1]){

    }
    
    bool insertFront(int value) {
        if(isFull()) return false;
        head=(head-1+cap)%cap;
        buf[head]=value;
        return true;
    }
    
    bool insertLast(int value) {
        if(isFull()) return false;
        buf[tail]=value;
        tail=(tail+1)%cap;
        return true;
    }
    
    bool deleteFront() {
        if(isEmpty()) return false;
        head=(head+1)%cap;
        return true;
    }
    
    bool deleteLast() {
        if(isEmpty()) return false;
        tail=(tail-1+cap)%cap;
        return true;
    }
    
    int getFront() {
        if(isEmpty()) return -1;
        return buf[head];
    }
    
    int getRear() {
        if(isEmpty()) return -1;
        return buf[(tail-1+cap)%cap];
    }
    
    bool isEmpty() {
        return  head==tail;
    }
    
    bool isFull() {
        return (tail+1)%cap==head;
    }
private:
    int cap;
    int head,tail;
    int *buf;
};
type MyCircularDeque struct {
    cap int 
    head,tail int
    buf []int
}


func Constructor(k int) MyCircularDeque {
    return MyCircularDeque{
        cap:k+1,
        head:0,
        tail:0,
        buf:make([]int,k+1),
    }
}


func (this *MyCircularDeque) InsertFront(value int) bool {
    if this.IsFull(){return false}
    this.head=(this.head-1+this.cap)%this.cap
    this.buf[this.head]=value
    return true
}


func (this *MyCircularDeque) InsertLast(value int) bool {
    if this.IsFull(){return false}
    this.buf[this.tail]=value
    this.tail=(this.tail+1)%this.cap
    return true
}


func (this *MyCircularDeque) DeleteFront() bool {
    if this.IsEmpty(){return false}
    this.head=(this.head+1)%this.cap
    return true
}


func (this *MyCircularDeque) DeleteLast() bool {
    if this.IsEmpty(){return false}
    this.tail=(this.tail-1+this.cap)%this.cap
    return true
}


func (this *MyCircularDeque) GetFront() int {
    if this.IsEmpty(){return -1}
    return this.buf[this.head]
}


func (this *MyCircularDeque) GetRear() int {
    if this.IsEmpty(){return -1}
    return this.buf[(this.tail-1+this.cap)%this.cap]
}


func (this *MyCircularDeque) IsEmpty() bool {
    return this.head==this.tail
}


func (this *MyCircularDeque) IsFull() bool {
    return (this.tail+1)%this.cap==this.head
}

滑动窗口最大值

https://leetcode.cn/problems/sliding-window-maximum/

  • 这道题的本质呢就是求固定长度窗口内的最大值。
  • 最容易想到的思路就是"暴力双层循环",外层以窗口长度为步长遍历列表,内层循环寻找固定窗口内的最大值。时间复杂度为O(nk)其中k为固定窗口的长度。
  • 由于有些固定窗口的最大值是相同的,我们可以将其保存在一种数据结构中,除了要找到最大值还要判断该最大值是否是当前查找的窗口内的最大值,所以还要保存数值对应的下标。不同窗口的最大值排在一起形成一个队列,队头的元素就是窗口的最大值,若队顶的元素不在当前窗口的范围内将其出队。所以队列除了队尾进,队尾出还要能保证队头出,所以需要使用双端队列[上一题也分析过如何实现]。
typedef pair<int,int> pa;
class Solution {
public:
    vector<int> maxSlidingWindow(vector<int>& nums, int k) {
        deque<pa> de;//双端队列
        vector<int> ans(nums.size()-k+1);//存储最终的答案
        int in=0;
        for(int i=0;i<nums.size();++i){
            while(!de.empty()&&nums[i]>de.back().first){
                de.pop_back();
            }
            de.push_back(make_pair(nums[i],i));
            if(i>=k-1){
                while(de.front().second<=i-k){de.pop_front();}
                ans[in++]=de.front().first;
            }
        }
        return ans;
    }
};
  • 由于可以通过下标找到元素数值,所以只存储元素所在下标也可以
func maxSlidingWindow(nums []int, k int) []int {
    ans:=make([]int,len(nums)-k+1)
    in:=0
    de:=make([]int,0,len(nums))
    for i,_:=range nums{
        for len(de)!=0 && nums[i]>nums[de[len(de)-1]]{
            de=de[:len(de)-1]
        }
        de=append(de,i)
        if i>=k-1{
            for de[0]<=i-k{
                de=de[1:]
            }
            ans[in]=nums[de[0]]
            in++
        }
    }
    return ans

}

讲解完了"队列和栈",来看看如何用栈实现队列,如何用队列实现栈吧

用栈实现队列

https://leetcode.cn/problems/implement-queue-using-stacks/

  • 栈的结构特性是"先进后出",队列的结构特性是"先进先出"。单用一个栈实现队列是不可能的,所以需要一个辅助栈。也就是用两个栈实现队列。假设现有A\B两个栈,
  • 对于Push操作。新的元素直接推进A栈的尾部。
  • 对于Pop操作。对于队列来说应该pop最先加入的元素,但是位于栈顶的元素是最后加入的。所以依次对A栈进行pop操作,并将元素加入到B栈,此时B栈栈顶的元素就是最先加入的元素也就是队头元素。
  • 对于empty操作。A\B栈都为空,队列才为空。
class MyQueue {
public:
    MyQueue() {

    }
    
    void push(int x) {
        a.push(x);
    }
    
    int pop() {
        int x = peek();
        b.pop();
        return x;
    }
    
    int peek() {
        if(!b.empty()) return b.top();
        while(!a.empty()) {b.push(a.top());a.pop();}
        return b.top();
    }
    
    bool empty() {
        return a.empty()&&b.empty();
    }
private:
    stack<int>a;
    stack<int>b;
};


type MyQueue struct {
    a,b []int
}


func Constructor() MyQueue {
    return MyQueue{
        a:[]int{},
        b:[]int{},
    }
}


func (this *MyQueue) Push(x int)  {
    this.a=append(this.a,x)
}


func (this *MyQueue) Pop() int {
    x:=this.Peek()
    this.b=this.b[:len(this.b)-1]
    return x
}


func (this *MyQueue) Peek() int {
    if len(this.b)!=0 {return this.b[len(this.b)-1]}
    for i:=len(this.a)-1;i>=0;i--{this.b=append(this.b,this.a[i])}
    this.a=this.a[:0]
    return this.b[len(this.b)-1]
}


func (this *MyQueue) Empty() bool {
    return len(this.a)==0 && len(this.b)==0
}



用队列实现栈

https://leetcode.cn/problems/implement-stack-using-queues/description/

用两个队列实现栈的思路

  • 现有A、B两个队列。
  • 对于Push操作,直接推入A队列的尾部。
  • 对于Pop操作,队头的元素是先加入的元素,而对于栈来说pop应该是最后加入的元素。如何保存这个最后加入的元素呢。这时候需要用到辅助队列,每次新加入的元素都是加入到A队列的尾部,为了避免再次加入的元素排到上次加入元素的后面,先将A队列复制给B队列A队列清空,然后再将新元素加入到A队列,之后将B队列的元素重新拿到A队列,这样就可以保证队头的元素永远是最新加入的。
  • 这样一来,Pop操作的时间复杂度为O(1),Push操作的时间复杂度为O(n)
  • 对于empty操作,A队列为空则栈为空。
class MyStack {
public:
    MyStack() {

    }
    
    void push(int x) {
        while(!a.empty()) {b.push(a.front());a.pop();}
        a.push(x);
        while(!b.empty()) {a.push(b.front());b.pop();}
    }
    
    int pop() {
        int x = a.front();
        a.pop();
        return x;
    }
    
    int top() {
        return a.front();
    }
    
    bool empty() {
        return a.empty();
    }
private:
    queue<int> a;
    queue<int> b;
};

type MyStack struct {
    a,b []int
}


func Constructor() MyStack {
    return MyStack{
        a:[]int{},
        b:[]int{},
    }
}


func (this *MyStack) Push(x int)  {
    for i:=0;i<len(this.a);i++{
        this.b=append(this.b,this.a[i])
    }
    this.a=this.a[:0]
    this.a=append(this.a,x)
    this.a=append(this.a,this.b ...)
    this.b=this.b[:0]
}


func (this *MyStack) Pop() int {
    x:=this.a[0]
    this.a=this.a[1:]
    return x
}


func (this *MyStack) Top() int {
    return this.a[0]
}


func (this *MyStack) Empty() bool {
    return len(this.a)==0
}

用一个队列实现栈的思路
用两个队列可以实现一个栈,那用一个队列能不能实现栈的功能呢?答案是可以的。队列里保存元素的顺序刚好和栈保存元素的顺序是相反的[队头的元素是最先加入的,队尾的元素是后加入的。栈顶的元素是后加入的,栈尾的元素是最先加入的],基于此我们可以在将元素入队之后重新出队再入队就可以使队中的元素反转一下,这样队头的元素就是栈顶的元素,队尾的元素就是栈尾的元素。[每次反转之前,可以先记录下当前队内的元素总数,避免将之前反转过的元素重复反转]

class MyStack {
public:
    MyStack() {

    }
    
    void push(int x) {
        int size=qu.size();
        qu.push(x);
        while(size--){
            qu.push(pop());
        }
    }
    
    int pop() {
        int x = top();
        qu.pop();
        return x;
    }
    
    int top() {
        return qu.front();
    }
    
    bool empty() {
        return qu.empty();
    }
    
private:
    queue<int>qu;
};


type MyStack struct {
    a []int
}


func Constructor() MyStack {
    return MyStack{
        a:[]int{},
    }
}


func (this *MyStack) Push(x int)  {
    size:=len(this.a)
    this.a=append(this.a,x)
    for size>0{
        this.a=append(this.a,this.Pop())
        size--
    }
}


func (this *MyStack) Pop() int {
    x:=this.Top()
    this.a=this.a[1:]
    return x
}


func (this *MyStack) Top() int {
    return this.a[0]
}


func (this *MyStack) Empty() bool {
    return len(this.a)==0
}



本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mfbz.cn/a/334301.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

Pytorch实战——3、数据加载与处理

&#x1f345; 写在前面 &#x1f468;‍&#x1f393; 博主介绍&#xff1a;大家好&#xff0c;这里是hyk写算法了吗&#xff0c;一枚致力于学习算法和人工智能领域的小菜鸟。 &#x1f50e;个人主页&#xff1a;主页链接&#xff08;欢迎各位大佬光临指导&#xff09; ⭐️近…

【音视频原理】图像相关概念 ③ ( RGB 色彩简介 | RGB 排列 | YUV 色彩简介 | YUV 编码好处 )

文章目录 一、RGB 色彩1、RGB 色彩简介2、RGB 排列 二、YUV 色彩1、YUV 色彩简介2、YUV 编码好处 一、RGB 色彩 1、RGB 色彩简介 RGB 是 计算机 中的 颜色编码方法 , 红 ( R ) / 绿 ( G ) / 蓝 ( B ) 三个颜色通道 可以设置不同的值 , 每个 通道 的 颜色值都可以取值 0 ~ 255 ,…

Python智能挖掘数据新秘器

大家好&#xff0c;本次分享一款在数据探索中表现出色的工具—Python Lux &#xff0c;通过自动化可视化和数据分析过程&#xff0c;使得数据探索变得更加快捷方便。 Lux的使用方法非常简单&#xff0c;只需在Jupyter notebook中输入dataframe&#xff0c;Lux就会智能推荐一组基…

Java项目:10 Springboot的电商书城管理系统

作者主页&#xff1a;舒克日记 简介&#xff1a;Java领域优质创作者、Java项目、学习资料、技术互助 文中获取源码 项目介绍 该系统分为前台展示和后台管理两大模块&#xff0c;前台主要是为消费者服务。该子系统实现了注册&#xff0c;登录&#xff0c;以及从浏览、下单到支付…

第三讲_ArkTS的初识

ArkTS的初识 1. ArkTS的基本组成2. ArkTS自定义组件 1. ArkTS的基本组成 装饰器&#xff1a; 用于装饰类、结构、方法以及变量&#xff0c;并赋予其特殊的含义。自定义组件&#xff1a;可复用的UI单元&#xff0c;可组合其他组件&#xff0c;图示中Component装饰的struct Hello…

Halcon 一维测量

文章目录 算子矩形算子弧形算子移动到新的参考点 Halcon 案例测量保险丝的宽度&#xff08;边缘对测量&#xff09;使用助手进行测量 halcon 案例获取芯片引脚的个数平均宽度距离&#xff0c;连续两个边缘的距离&#xff08;measure_pos &#xff09;halcon 定位测量Halcon 测量…

C#,水仙花数(Narcissistic number)的计算方法及源代码

一、水仙花数&#xff08;Narcissistic number&#xff09; 水仙花数&#xff08;Narcissistic number&#xff09;也被称为&#xff1a;超完全数字不变数&#xff08;pluperfect digital invariant, PPDI&#xff09;、自恋数、自幂数、阿姆斯壮数或阿姆斯特朗数&#xff08;A…

B站提示:“当前浏览器版本较低……”可行的解决方案(edge浏览器)

文章目录 问题研究和分析使用User-Agent Switcher for Chrome插件的解决方法使用userAgent switcher的解决方法 问题研究和分析 问题&#xff1a;使用最新版浏览器访问B站&#xff0c;首页总是有一条横幅提示&#xff1a;当前浏览器版本较低&#xff0c;为保证您的使用体验&am…

vscode设置terminal的最大行数

今天跑代码出现一个问题&#xff0c;就是整个程序跑完&#xff0c;整个程序的输出信息过多&#xff0c;最开始输出的信息已经被vscode的缓存冲掉了&#xff0c;只能看到最后的一部分&#xff0c;具体的原因是vscode的terminal默认只能保存1000行的信息&#xff0c;所以如果想保…

智慧仓储物流远程监控方案分析

智慧仓储物流远程监控方案分析 随着物联网、大数据、云计算等技术的快速发展&#xff0c;智慧仓储物流逐渐成为现代物流发展的重要方向。远程监控作为智慧仓储物流的重要组成部分&#xff0c;可以有效提高仓储物流的效率、准确性和安全性。 一、智慧仓储物流远程监控方案概述 …

llvm pass

pass们组合在一起&#xff0c;处理IR 而最后的目标代码生成阶段&#xff0c;会生成另一种MIR&#xff08;Machine IR&#xff09; PassManager管理这些pass pass处理IR之后会改变分析的情况&#xff0c;这些关于IR的信息由 AnalysisManager处理 1、pass &#xff08;1&…

在线多端口排课教务管理工具:教育机构管理的得力助手

在现代教育中&#xff0c;教务管理是一个复杂而重要的任务。为了简化这一过程&#xff0c;许多在线教务管理工具应运而生。今天&#xff0c;我将向大家介绍一款名为乔拓云的在线多端口排课教务管理工具。 首先&#xff0c;乔拓云是一个功能强大的教务管理系统。它不仅提供了小程…

Python创建线程

Python 提供了 _thread 和 threading 两个模块来支持多线程&#xff0c;其中 _thread 提供低级别的、原始的线程支持&#xff0c;以及一个简单的锁&#xff0c;正如它的名字所暗示的&#xff0c;一般编程不建议使用 thread 模块&#xff1b;而 threading 模块则提供了功能丰富的…

使用C#操作文件:一个实际案例——替换文件中的IP地址

标题&#xff1a; 使用C#操作文件&#xff1a;一个实际案例——替换文件中的IP地址 介绍&#xff1a; 欢迎阅读我的最新博客&#xff01;今天&#xff0c;我们将探讨如何使用C#来处理一个实际的编程挑战&#xff1a;读取一个配置文件并替换其中的IP地址。这是一个非常常见的…

vue写了debugger谷歌浏览器打开控制台没进断点

vue代码中打了断点&#xff0c;谷歌打开f12进不了断点解决方案如下 1、打开谷歌浏览器控制台&#xff0c;点击设置 2、在 Ignore List 中将“Enable Ignore Listing”勾选去掉&#xff0c;然后就可以正常使用debugger了

Es bulk批量导入数据(1w+以上)

最近在学习es的理论知识以及实际操作&#xff0c;随时更新~ 概要&#xff1a;首先你得有1w条数据的json&#xff0c;然后用java读取json文件导入 一. 创建Json数据 首先我生成1.5w条数据&#xff0c;是为了实践分页查询&#xff0c;用from-size和scroll翻页去实践 生成四个字段…

UG机械制图的基本常识

目前来说工程图就是传递产品信息的工具&#xff0c;所以图纸一定不能出错&#xff0c;因为所有的设计都要转化为生产的输入。 一张完整的工程图应由图框&#xff0c;图素&#xff0c;尺寸标注以及技术要求这四部分组成&#xff0c; 图框包括图纸幅面&#xff1a;A0,A1,A2,A3,…

(2024,VMamba,交叉扫描,线性复杂度,全局感受野,动态权重)视觉状态空间模型

VMamba: Visual State Space Model 公和众和号&#xff1a;EDPJ&#xff08;进 Q 交流群&#xff1a;922230617 或加 VX&#xff1a;CV_EDPJ 进 V 交流群&#xff09; 目录 0. 摘要 3. 方法 3.1 基础概念 3.2 2D 选择性扫描 3.3 VMamba 模型 3.3.1 整体架构 3.3.2 VSS…

nvm, node.js, npm, yarn 安装配置

文章目录 nvm 安装node.js 安装npm yarn 配置 nvm 安装 nvm 是一个 node.js 管理工具&#xff0c;可以快捷下载安装使用多个版本的node.js linux 命令行输入&#xff1a; curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.7/install.sh | bashwget -qO- https…