数据结构 栈与队列

数据结构 栈与队列

栈与队列(优先队列、双端队列)是一类特殊的线性表。

顺序栈和链式栈

栈的存储方式有两种,基于数组和基于链表,分别被称为顺序栈和链式栈。

栈与括号匹配

括号匹配问题,某个字符串中的三类括号能否匹配,只需将左括号入栈,每次比较当前字符和栈顶字符是否匹配即可。

#include <iostream>
#include <stack>

using namespace std;

int main(){
    string in;
    cin>>in;
    stack<char> s;
    
    for(char c : in){
        // 假如是左括号,那么入栈
        if(c=='(' || c=='[' || c=='{'){
            s.push(c);
        }// 假如是右括号,并且栈空
        else if(s.empty() && (c==')' || c==']' || c=='}')){
            cout<<"No"<<endl;
            return 0;
        }// 假如是右括号,并且匹配
        else if(c==')' && s.top()=='(' || c==']' && s.top()=='[' || c=='}' && s.top()=='{'){
            s.pop();
        }else{
            cout<<"No"<<endl;
            return 0;
        }
    }

    if(s.empty()) cout<<"Yes"<<endl;
    else cout<<"No"<<endl;
}

表达式计算

计算机中的表达式有操作数、操作符和分界符组成,通常,算数表达式有三种表示:

中缀表示:<操作数><操作符><操作数>

前缀表示:<操作符><操作数><操作数>

后缀表示:<操作数><操作数><操作符>

类似树的前中后序遍历(表达式本身也是一棵树,树的层级刚好就是表达式的优先级)

中缀表示中,需要根据每个表达式的优先级来确定计算顺序,还有括号运算符,因此和前缀一样,都需要两个栈来实现;后缀则只需要一个。

计算后缀表达式的值

后缀表达式,又称为逆波兰(RPN)表达式,算法如下:

  1. 自左至右顺序扫描表达式的每一项,根据其类型做如下操作:
  2. 如果是该项是操作数,那么直接入栈
  3. 如果该项是操作符,那么栈顶的两个元素出栈,与操作符进行计算,结果入栈
  4. 最终扫描完成后,栈顶的元素就是表达式的值
    int evalRPN(vector<string>& tokens) {
        stack<string> s;
        // 自左向右读取
        for(string str : tokens){
          	// 操作符
            if(str.size() == 1 && (str[0] == '+'||str[0] == '-'||str[0] == '*'||str[0] == '/')){
                int a = stoi(s.top());
                s.pop();
                int b = stoi(s.top());
                s.pop();
                if(str[0] == '+'){
                    s.push(to_string(a+b));
                }
                if(str[0] == '-'){
                    s.push(to_string(b-a));
                }
                if(str[0] == '*'){
                    s.push(to_string(a*b));
                }
                if(str[0] == '/'){
                    s.push(to_string(b/a));
                }
            // 操作数
            }else{
                s.push(str);
            }
        }
        return stoi(s.top());
    }

将中缀表达式转化为后缀表达式

在中缀表达式中,操作符的优先级和括号使得求解的过程被复杂化,把它转化为后缀表达式后,求解变得简单。为了实现中缀向猴嘴的转化,需要考虑各个操作符的优先级。

在本算法中,有两种优先级,栈内优先级isp和栈外优先级icp,其值如下:

操作符ch#(*,/,%+,-)
Isp06531
Icp01426

可以看到,一般操作符的栈内优先级比栈外优先级高1,这说明同样优先级的操作符满足左结合律。括号的优先级在内外分别是最低和最高,说明其不重要,为分割符,主角是操作符与操作数。内外优先级相等的情况仅发生在括号匹配或者结束符#处,前者将使得字符继续向下读入,后者将结束本次转化。

算法如下:

  1. 初始化。栈内为开始结束符#,当前读入的字符ch为中缀的第一个字符

  2. 自左至右读取中缀表达式ch

    1. 若为操作数,直接输出

    2. 若为操作符,比较当前读取的中缀字符ch的栈外优先级icp(ch)和栈顶操作符op的栈内优先级isp(op)

      1. 若icp(ch) > isp(op) ,ch入栈

      2. 若icp(ch) < isp(op) ,依次输出所有isp大于icp(ch)的栈顶元素,然后判断栈顶元素和icp(ch)关系

        1. 若icp(ch) > isp(栈顶元素),说明ch为正常运算符,入栈

        2. 若icp(ch) == isp(栈顶元素),说明ch为括号或者开始结束符,栈顶元素出栈

      3. 若icp(ch) == isp(op) ,op出栈但不输出(同理这时有两种,括号和结束符,都不需要输出)

最终输出的序列就是后缀表达式。

也可以按照当前中缀序列元素的类型来进行操作:

  1. 若为操作数,直接输出
  2. 若为操作符,比较与栈顶元素的优先级,大于则入栈,小于则出栈并输出直到大于或左括号或结束符,然后入栈
  3. 若为左括号,入栈
  4. 若为右括号,一直出栈并输出直到左括号,然后左括号出栈但不输出
  5. 若为结束符,一直输出到栈顶为结束符,然后结束
#include <iostream>
#include <string>
#include <map>
#include <stack>

using namespace std;

int main(){
    // 这是输入的中缀表达式序列,注意前面没有开始结束符"#",否则第一步就挂了
    string in[] = {"A","+","B","*","(","C","-","D",")","-","E","/","F","#"};
    // 事先规定的各个符号的优先级
    map<char,int> isp,icp;
    isp['#'] = 0;isp['('] = 1;isp['*'] = isp['/'] = isp['*'] = 5;isp['+'] = isp['-'] = 3;isp[')'] = 6;
    icp['#'] = 0;icp['('] = 6;icp['*'] = icp['/'] = icp['*'] = 4;icp['+'] = icp['-'] = 2;icp[')'] = 1;
    
    // 用一个栈来完成转换,栈内初始化为1个开始结束符"#"
    stack<string> s;
    s.push("#");
    
    // 读取中缀表达式序列,转化为后缀表达式并输出
    for(string ch : in){
        // 假如是操作符,这里判断可用map的find函数,该函数返回迭代器
        if(ch.size() == 1 && isp.find(ch[0])!=isp.end()){
            // 栈顶操作符op
            string op = s.top();
            
            // 假如外优先级大于内,那么外来的新操作符入栈
            if(icp[ch[0]] > isp[op[0]]){
                s.push(ch);
            }
            // 假如外优先级小于内,那么栈顶操作符输出并出栈,直到外优先级d不小于内
            else if(icp[ch[0]] < isp[op[0]]){
                while(isp[s.top()[0]] > icp[ch[0]]){
                    cout<<s.top();
                    s.pop();
                }
                // 此时若外优先级大于内,说明外来的新操作符非括号与结束符,那么入栈
                if(icp[ch[0]] > isp[s.top()[0]]){
                    s.push(ch);
                }
                // 否则,为括号或者结束符,依据匹配的原则,还要将栈顶元素出栈
                else{
                    s.pop();
                }
            }
            // 假如内外优先级相等,说明二者是互相匹配的括号或者开始结束符,出栈即可
            else{
                s.pop();
            }
        }
        // 假如是操作数
        else{
            cout<<ch;
        }
    }
}

栈与递归

栈和递归密切相关,递归函数在调用时有外部调用和内部调用之分。外部调用结束后,返回调用该过程的主程序;内部调用结束后,将返回到递归过程中本次内部调用的下一个语句处。因此在每一次内部调用中,都要为函数的形参传递、函数返回位置的记录等预留空间,,这些内容在每次递归函数调用时被打包加在某个栈的栈顶,称为递归工作栈

利用栈将递归改为非递归

为了减少函数调用开销,可以用一个栈来记录递归的过程,将递归转化为非递归,例如树的前中后序遍历,都可以用栈来代替递归。

利用迭代求解递归

一些递归问题常常可以用迭代的方式解决,例如递归语句发生在函数最后一行的尾递归,或者是从大到小单调递归的单向递归

递归法求解汉诺塔问题

汉诺塔是递归的经典问题。A,B,C三个柱子,起初A柱子上有n个上小下大排列的盘子,问全部移到C柱子上需要移动多少次,其中所有盘子只能小的在大的上面。

#include <iostream>
#include <string>

using namespace std;

// 每个问题我们都认为是把A柱子上的n个盘子借助B柱子移动到C柱子,第一个参数是起始柱,第二个是中转柱,第三个是目标柱
void hanoi(int n, string A, string B, string C){
    if(n == 1){
        cout<<A<<"->"<<C<<endl;
    }else{
        hanoi(n-1, A, C, B);// 前n-1个先借助C柱转移到B柱
        cout<<A<<"->"<<C<<endl;// 然后将最下面的一个转移到C柱
        hanoi(n-1, B, A, C);// 然后将那n-1个转移到C柱
    }
}

int main(){
    hanoi(3, "A", "B", "C");
}

队列、循环队列与链式队列

队列利用数组实现最基本的顺序队列,但是这可能有一些溢出和空间利用的不充分;利用模运算可以将数组的空间充分利用,也就是实现上的循环队列;利用链表实现的队列就是链式队列了。

打印杨辉三角

利用队列可以打印杨辉三角的前n行,其中杨辉三角的第i行就是多项式(a+b)i的系数

#include <iostream>
#include <queue>

using namespace std;

void YANGVI(int n){
    
    // 队列初始化为第一行
    queue<int> q;
    q.push(1);q.push(1);
    
    // 逐行处理
    for(int i = 1;i <= n;i++){
        cout<<endl;
        // 读到第一个1,说明上一行已经被读完了,本行已经全部入队,加一个本行结束的1
        if(q.front() == 1){
            cout<<1<<" ";
            q.push(1);
            q.pop();
            q.push(1+q.front());
        }
        while(q.front() != 1){
            cout<<q.front()<<" ";
            int tmp = q.front();
            q.pop();
            q.push(tmp+q.front());
        }
        // 读到第二个1,说明这一行已经被读完了,下一行已经全部入队,加一个下一行开始的1
        cout<<q.front()<<" ";
        q.push(1);
        q.pop();
    }
}

int main(){
    YANGVI(5);
}

一般逐行处理的情况都少不了用队列作为其辅助工具。

优先级队列

优先级队列每次从队列中取出的应该是具有最高优先权的元素。

STL优先队列priority_queue

#include <iostream>
#include <queue>

using namespace std;

// 基本类型的堆
priority_queue<int> pq1;                         // 默认为大顶堆
priority_queue<int,vector<int>,greater<int>> pq2;// 这样写就是小顶堆


// 对象的堆
// 第一种方式,重载<运算符
struct node1{
    int n;
    bool operator < (const node1 &a) const {
        return this->n > a.n;// 这样写就是小顶堆,和前面的greater一样,这是一个思维的反常点
    }
};

priority_queue<node1> pq3;

// 第二种方式,重写仿函数,即重载运算符()
struct node2{
    int n;
    bool operator () (node2 &a,node2 &b) const {
        return a.n > b.n;// 这样写就是小顶堆,和前面的greater一样,这是一个思维的反常点
    }
};

priority_queue<node2> pq4;

int main(){
    // 获取堆顶元素
    pq1.top();
    
    // 插入元素
    pq1.push(1);
    
    // 删除元素
    pq1.pop();
}

双端队列

双端队列可以在队列的两端进行插入和删除。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值