零。 全文概述
本文将介绍栈和队列的基本知识,如何模拟栈和队列;以及部分题目。由于本次考试不允许使用stl。因此本文将不介绍栈和队列的stl方法,具体解题也只会用到模拟的栈和队列
壹。 栈与队列的介绍——有限制的线性表
栈和队列相比于线性表其实并没有新的内容,只是此时对这个线性表能进行的操作有所限制而已。
a. 栈
1. 特点
i 仅可以在栈顶进行插入or删除操作
ii. 遵循后进先出原则(LIFO)
所以对出栈顺序是有要求的
eg:
iii. 主要实现的操作:入栈 出栈
2. 应用:
如果问题求解过程符合/需要“后进先出”——则要考虑栈
i. 生活中的例子: 子弹上膛 电池
ii. 编程中: 函数调用/递归调用 进制转换 括号匹配 表达式求值(见后文)
b. 队列
1. 特点:
i. 头删尾插:只能在表的一端进行插入操作,一端进行删除操作
ii. 遵循先进先出原则(FIFO)
iii. 主要实现的操作:与栈一样 就是入队和出队
2. 应用
如果问题求解过程符合/需要“先进先出”——则要考虑队列(如各种排队问题)
i. 生活中的例子: 排队 任务分配
ii. 编程中: 业务排队
贰. 模拟栈
我们知道,栈和队列其实也是线性表,只是是有限制的线性表;因此只用直接按照学习线性表的方法进行模拟,并且再对其操作进行一些限制即可
a. 用数组进行模拟(最简单 最通用)
去用数组模拟,并实现最基本的弹入 弹出 查询 判断是否为空
#include <cstdio>
#include <iostream>
using namespace std;
const int N = 100010;
//因为栈只用对栈顶进行操作,所以只需要一个tt指针去指向栈顶
int skt[N],tt;
//此时相当于初始化tt为0——所以tt为0时表示栈是空的
//弹入操作:
//相当于是两步:1.先让tt+1 让栈顶往上移动一位 2.再skt【tt】=x 即可让x进入到栈顶
void push(int x)
{
skt[++tt]=x;
}
//弹出操作:直接让tt减一(相当于把栈顶指针往下移动了一位 弹出了一个值)
void pop()
{
tt--;
}
//判断是否为空 就看栈顶指针是否为0即可
int empty()
{
if(tt) return 0;
else return 1;
}
//查询栈顶元素——所以只需要直接取栈顶指针的值即可
int query()
{
return skt[tt];
}
int main()
{
int n;
scanf("%d",&n);
//对于用字符去判断操作类型的题目,有两种处理方式:
//1。用一个char数组+scanf可以刚好用空格跳过字符;随后可以用op【0】去判断
//2。 用string字符串去读入
while(n--)
{
char op[5];
scanf("%s",op);
if(op[0]=='p'&&op[1]=='u')
{
int x;
cin>>x;
push(x);
}
if(op[0]=='p'&&op[1]=='o') pop();
if(op[0]=='q') printf("%d\n",query());
if(op[0]=='e')
{
if(empty()) cout<<"YES"<<endl;
else cout<<"NO"<<endl;
}
}
return 0;
}
此时相当于把一个数组去横过来看作一个栈,且由于栈只能对栈顶操作,因此我们也只需要有一个指针tt指向栈顶即可
可以看到,此时数组相当于一个栈,而tt指针所指的及以下部分即是我们可以用的;因此要实现入栈 出栈操作,也只需要把tt指针向上移动or向下移动即可。
图解:
要明确。用数组模拟栈时:关键在于要始终让tt指针指向栈顶的位置,而一切的操作也都是对tt指针做的(因为栈要求只能操作栈顶)
而虽然数组空间开的很大,但其实我们实际用到的栈的范围就是数组空间的0-tt(不包括0位置)
用数组模拟栈是十分简单高效的,并且通过函数可以让操作清晰明了;因此后续的题目操作都采用数组模拟的栈
b. 链栈模拟
此时的链表相当于一个受限的单链表。在链表一章中我们有说过识别一个链表的唯一标识就是链表头节点head了。在链表模拟栈中,我们让每个链表的头节点都指向栈顶。
因为插入和删除都只在栈顶进行,让链表的头节点直接指向栈顶也便于我们进行各种操作。
由图示可以看出,链表模拟和数组模拟其实是一样的。也都是只有一个指针指向栈顶,并在栈顶进行各种操作
#include <cstdio>
#include <iostream>
using namespace std;
struct sNode
{
int data; //存放数据
sNode * next; //存放指向下一个位置的指针
};
typedef sNode* stack; //后文的stack相当于就是结构体指针 指向链表所链的下一个位置
//初始化的时候 相当于构造一个空栈 并让栈顶指针指向NULL
void init( stack &s )
{
s = new sNode;
s = NULL; //在链表模拟中,当栈顶指针指向NULL时,我们认为栈是空的
return ;
}
//把值为x的插入到栈顶
void push( stack &s , int x )
{
stack p = new sNode;//生成新顶点p
p->data = x; //他的值就应该是传进来的新的值
p->next = s; //他下一个链接着的应该就是曾经的栈顶s
s = p; //此时栈顶应该变化为p
return ;
}
//取栈顶元素
int gettop( stack &s )
{
if( s!=NULL ) return s->data; //如果栈非空,则直接返回栈顶(指针)所指的那个值
else return -1; //若栈为空,则报错
}
//删除栈顶元素
int pop( stack &s )
{
if( s==NULL) return -1;
else
{
stack temp = s; //用一个临时指针指向原先栈顶s;再将他delete
s = s->next; //让栈顶指针往下移动一位即可
delete temp;
}
}
//判断栈是否为空(为空则返回1,否则返回0)
int empty( stack &s )
{
if( s==NULL) return 1;
else return 0;
}
int main()
{
//初始化
stack s;
init(s);
//赋值
push(s,3);
push(s,4);
push(s,5);
//判断是否为空
if(empty(s)) cout<<"为空"<<endl;
else cout<<"不为空"<<endl;
//不断输出栈顶元素直到为空
while(!empty(s))//栈不为空
{
int x = gettop(s);
cout<<x<<" ";
pop(s);
}
return 0;
}
叁。 模拟队列
类比模拟栈去理解记忆,此时也是给出两种方式模拟,一种是数组模拟,另一种是链表模拟
a. 用数组进行模拟
#include <cstdio>
#include <iostream>
using namespace std;
const int N = 100010;
//对队列进行初始化
int hh=0,tt=-1;//让队头一开始为0,队尾一开始为-1
int q[N];
//队尾插入
void push(int x)
{
q[++tt]=x;
//也相当于是两步操作:1.tt+1即让队尾往后面挪动一个位置 2.把x赋值给这个新挪动出来的位置
}
//队头删除
void pop()
{
hh++; //队头的指针是hh,让hh++就是删除一个队头元素
}
int query()
{
return q[hh]; //直接去取队头元素即可
}
bool empty()
{
if(hh<=tt) return false; //只要队尾大于等于队头 就说明这个队列里面是有东西的
else return true;
}
int main()
{
int n;
scanf("%d",&n);
while(n--)
{
string s;
cin>>s;
if(s[0]=='e')
{
if(empty()) cout<<"YES"<<endl;
else cout<<"NO"<<endl;
}
if(s[0]=='q') printf("%d\n",query());
if(s[0]=='p'&&s[1]=='o') pop();
if(s[0]=='p'&&s[1]=='u')
{
int x;
scanf("%d",&x);
push(x);
}
}
return 0;
}
与栈去类比理解:此时同样也是一个数组和指针。不同之处在于栈的时候我们只能对栈顶进行操作,因此我们只需要一个tt指针指向栈顶即可;但是在队列中,我们对队首(hh)和队尾(tt)都要进行操作,因此需要两个指针分别指向队首和队列
要注意的是:
1.队首指针hh初始化赋值为0,队尾指针tt初始化赋值为-1。当tt<hh时我们认为队列是空的;而当tt>=hh,即队尾指针大于等于队首指针时,我们认为队列中有元素。
2.由1可知,虽然队列的数组也开了很大,但其实真正有用的只有hh到tt的这部分(左右两边都要取到)
3.此时对两个指针hh和tt都是有限制的,hh队首只能删除,tt队尾只能加入
图解:
用数组模拟队列也是很简单方便的,因此后面我们都采取数组模拟队列
肆。 栈的例题
a. 进制转换
题目:把输入的十进制数转化成二进制数
分析:
对于把十进制数转换成二进制数,由进制之间转换的规则我们可以知道,只需要对其不断除2(作除法),再取余数,最后倒着将余数输出即可
11转换成二进制的图解
转换成代码:在第二步的操作中,我们发现是要将结果倒着输出,即LIFO——因此可以考虑使用栈
所以,只需要模拟出我们不断进行除法和取余数的操作(直到商为0退出循环),同时将得到的余数都放到栈中
代码实现:
#include <iostream>
#include <cstdio>
using namespace std;
const int N = 100010;
//初始化
int skt[N],tt;
//弹入操作
void push(int x)
{
skt[++tt]=x;
}
//弹出操作
void pop()
{
tt--;
}
//判断是否为空
int empty()
{
if(tt) return 0;
else return 1;
}
//查询栈顶元素
int query()
{
return skt[tt];
}
void Convert(int num,int n)
{
if (num == 0)
{ //特殊情况特判,当输入是0时,输出也是0
push(0);
}
else
{
while (num) //只有最后数字为0时,才会退出循环
{
push(num % n);//取余数并放入栈中
num /= n; //做除法
}
}
}
int main()
{
int num;
cin>>num;
Convert( num , 2);
// 把栈中的所有数都输出直到栈为空
while (!empty())
{
printf("%d", query());
pop();
}
printf("\n");
return 0;
}
本题的过程不算复杂,主要想借助本题说明一下
1.识别栈的使用:当出现需要LIFO的数据时应该考虑到使用栈去存储
2.由于数据结构考试不允许直接使用stl,因此我们只能使用数组模拟的栈
使用方法如下:
首先在程序的最前面加上数组模拟栈的相关代码(即初始化+各种操作)
const int N = 100010;
//初始化
int skt[N],tt;
//弹入操作
void push(int x)
{
skt[++tt]=x;
}
//弹出操作
void pop()
{
tt--;
}
//判断是否为空
int empty()
{
if(tt) return 0;
else return 1;
}
//查询栈顶元素
int query()
{
return skt[tt];
}
随后如果要使用栈,就直接调用skt数组(栈)以及各个操作函数即可
b. 括号匹配
题目:
分析:
此题要求我们进行括号的匹配,我们判断括号匹配时应该是在看到右括号时进行,在遇到某个右括号时去遍历过的左边找是否有与他匹配的括号。并且此时有多种括号,而正确的括号匹配中相邻的括号应该是一样的。综上,我们遇到一个新的右括号只用判断与他相邻的左括号是否匹配即可。
这种匹配规则是可以利用栈去实现的:即在遇到左括号时无脑将其入栈,遇到右括号时就去栈顶找栈顶的左括号是否与其相同(此时栈顶的左括号就是相邻最近的左括号);相同则出栈栈顶括号(相当于两个括号成功匹配相消了),不相同则匹配失败。要注意最后遍历完后,要额外判断一下栈是否为空(为空才表示所有的左括号都被消耗了)
画图模拟:
代码实现:
#include <iostream>
#include <stack>
#include <string>
using namespace std;
const int N = 100010;
//初始化
char skt[N];
int tt;
//弹入操作
void push(char x)
{
skt[++tt]=x;
}
//弹出操作
void pop()
{
tt--;
}
//判断是否为空
int empty()
{
if(tt) return 0;
else return 1;
}
//查询栈顶元素
char query()
{
return skt[tt];
}
//用一个函数去实现判断右括号与左括号是否匹配
bool isMatching(char opening, char closing) {
if (opening == '(' && closing == ')')
return true;
if (opening == '[' && closing == ']')
return true;
if (opening == '{' && closing == '}')
return true;
if (opening == '<' && closing == '>')
return true;
return false;
}
bool isBalanced(string str) {
for (char c : str)
//一种遍历的方法——即每次循环都输入一个字符c(这个字符c来自字符串str)直到字符串为空
{
if (c == '(' || c == '[' || c == '{' || c == '<') //左括号的情况
{
push(c);
}
else //右括号的情况
{
if (empty()||!isMatching(query(), c)) //如果匹配失败or栈此时为空 则直接返回失败,并跳出循环
{
return false;
}
else //如果匹配成功 则弹出一个栈顶元素
{
pop();
}
}
}
return empty();//循环完了额外判定下栈是否为空
}
int main()
{
string str;
cin >> str;
if (isBalanced(str)) {
cout << "yes" << endl;
} else {
cout << "no" << endl;
}
return 0;
}
注意:
1.要注意此时放入栈内的元素应该是字符类型,所以在初始化skt【】数组时类型应该是char才对
2.此时的匹配过程是要经过多次判断的,要实现许多功能,所以可以充分利用函数去分离这些功能
c. 表达式求值(数据结构作业02 -- 四则运算表达式计算)
题目:
表达式的求值原理操作 以及 前中后缀表达式 放到学习完树之后再进行介绍
因此本道题只给出算法和代码实现
算法流程:
E1:设立运算符栈和操作数栈;
E2:开始运算符#入栈,向表达式串尾添加结束运算符#;
E3:逐词读入表达式,并处理:
E31:若读入为操作数,则入栈;
E32:若读入为运算符,则与栈顶运算符相比较:
E321:若栈顶运算符优先级高于读入运算符:
弹出栈顶运算符和两个操作数,计算并将结果入栈,执行步骤E32;
E322:若栈顶运算符优先级低于读入运算符:
则将读入运算符入栈,执行步骤E3;
E323:若栈顶运算符优先级等于读入运算符:
若为#,计算结束,若为括号,则弹出运算符,执行步骤E3。
E4:检查栈状态,得到计算结果;
同样的也是按照规则进行匹配,只是此时的匹配规则更加复杂,同时要进行运算
代码实现:
#include<iostream>
#include<cstring>
#include<stack>
#include<unordered_map>
using namespace std;
//num里面存放着运算结果以及运算的数
//op里面存放着操作符
stack<int> num;
stack<char> op;
//eval()计算目前的表达式的值
void eval()
{
//注意因为用的是栈,所以先去出来的是b,其次是a,防止加减法出现负数
auto b = num.top(); num.pop();
auto a = num.top(); num.pop();
auto c = op.top(); op.pop();
int x;
if(c == '+') x = a + b;
else if(c == '-') x = a - b;
else if(c == '*') x = a * b;
else if(c=='/')
{
if(b==0)
{
cout<<"error"<<endl;
}
else x=a/b;
}
num.push(x);
}
int bracket(string str){//检查括号匹配情况
int mark=0;
for(int i=0;i<str.length();i++){
if(str[i]=='(') mark++;
if(str[i]==')') mark--;
}
return mark;
}
int main()
{
unordered_map<char,int> pr{{'+',1},{'-',1},{'*',2},{'/',2}};
qi:;
while(1)
{
string str;
cin>>str;
if(str[0]=='-')
{
cout<<"error"<<endl;
continue;
}
int tmp = bracket(str);
if(tmp!=0)
{
cout<<"error"<<endl;
continue;
}
for(int i = 0;i < str.size();i ++)
{
auto c = str[i];
//倘若这个字符是数字 且 连续出现数字,那么就需要将数字字符组合起来形成一个数
if(isdigit(c))
{
int x = 0, j = i;
while(isdigit(str[j]) && j < str.size())
x = x * 10 + str[j++] - '0';
i = j - 1;//更新i的值
num.push(x);
}
//倘若是左括号,那么就要入操作符
else if(c == '(') op.push(c);
//若是右操作符,那么直到做操作符里所有的数,都要进行一系列的运算
else if(c == ')')
{
while(op.top() != '(') eval();
op.pop();//且将左括号删除
}
//若操作的是+-/*操作符,则根据栈头与目前操作符比较大小,若栈头操作符大于等于目前操作符则进行计算
//且将当前运算符入栈
else
{
int tmp = i+1;
if(str[tmp]=='+' || str[tmp]=='-' || str[tmp]=='/' || str[tmp]=='*' )
{
cout<<"error"<<endl;
goto qi;
}
while(op.size() && pr[op.top()] >= pr[c]) eval();
op.push(c);
}
}
//将剩余的运算符进行计算
while(op.size()) eval();
cout<<num.top()<<endl;
}
return 0;
}
d. 栈的操作问题
题目:
代码实现:
#include<stdio.h>
#include<string.h>
#include<stdlib.h>
#include<malloc.h>
#include<math.h>
const int N = 100010;
//初始化
int skt[N],tt;// 定义一个数组skt,用于模拟栈
int nu[100001]; // 存放出栈序列
int head; // 入栈序列的当前值
//弹入操作
void push(int x)
{
skt[++tt]=x;
}
//弹出操作
void pop()
{
tt--;
}
//判断是否为空
int empty()
{
if(tt) return 0;
else return 1;
}
//查询栈顶元素
int query()
{
return skt[tt];
}
int main()
{
int n;//长度
while(~scanf("%d",&n)) // 循环读入输入
{
head=1; // 初始化入栈序列的当前值
tt = 0;
// 读入出栈序列
for( int i=0 ; i<n ; i++) scanf("%d",&nu[i]);
for( int i=0 ; i<n ; i++) //遍历出栈序列
{
int t=nu[i]; // 获取当前出栈序列的值
tt++; // 栈顶指针加1,模拟入栈操作
while(skt[tt-1]<t) // 如果栈顶元素小于当前出栈序列的值
{
skt[tt]=head; // 将当前入栈序列的值入栈
head++; // 入栈序列的当前值加1
tt++; // 栈顶指针加1,模拟入栈操作
printf("P"); // 输出入栈操作
}
tt--; // 栈顶指针减1,模拟出栈操作
if(skt[tt]==t){ // 如果栈顶元素等于当前出栈序列的值
printf("Q"); // 输出出栈操作
tt--; // 栈顶指针减1,模拟出栈操作
}
else if(skt[tt]>t){ // 如果栈顶元素大于当前出栈序列的值
printf(" error"); // 输出错误信息
break; // 跳出循环
}
}
memset(skt,0,sizeof(skt)); // 清空栈数组
memset(nu,0,sizeof(nu)); // 清空出栈序列数组
printf("\n"); // 输出换行符
}
return 0;
}
head和两个数组的作用:
我们的入栈序列的顺序一定是1 2 3 4......n。所以用一个head数组从1到n去表示入栈的序列。
nu【】数组的作用是存储出栈序列。
skt【】数组则是模拟栈,实现入栈出栈操作
循环的操作:
先取出栈序列中的一个值t想办法让其实现出栈操作。这里一定要注意到的是入栈序列是确定的1 2 3 4 n ; 且入栈序列具有单调性。所以我们可以将当前栈与在进入出栈操作的值t进行比较,有三种情况
先入栈
1.如果当前操作元素t>栈顶元素:这时候不满足出栈要求;又由于入栈序列是单调递增的,因此此时我们进行while循环不断让其入栈(P),直到操作元素t>=栈顶元素(如果一开始就有操作元素t>=栈顶元素。则不用进行此操作了)
再出栈,此时
2.如果当前操作元素t==栈顶元素:这时候满足了出栈要求,所以去进行出栈操作(Q),同时这个出栈序列nu【i】就操作完毕了,可以进入大的for循环的下一项去分析nu【i+1】
3.如果当前操作元素t<栈顶元素:又由于入栈序列一定是单调递增的,所以此时无论如何都不可能再满足出栈序列了,因此报错error
memet的操作:用于将数组清0
1.头文件:#include<string.h>
2.操作:memset(a,0,sizeof(a))——第一个参数是要进行清零操作的数组;第二个参数是0,表示要去赋值每一字节为0(结果就是全为0);第三个参数表示要操作的长度,一般是整个数组长度,所以用sizeof(数组a)去得
伍。 队列例题
a. 银行排队
题目:
代码实现:
#include <iostream>
#include <vector>
using namespace std;
// 定义客户信息的结构体
struct Client {
int arrival; // 到达时间
int duration; // 办理业务所需时间
};
int main()
{
int m, total, arrival, duration;
// 处理多组输入
while (cin >> m >> total)
{
vector<int> windows(m, 0); // 记录窗口下次空闲的时间
vector<Client> clients; // 存储客户信息
double totalWait = 0; // 总的等待时间
// 读取客户到达和办理业务所需时间
for (int i = 0; i < total; i++ )
{
cin >> arrival >> duration;
clients.push_back(Client{arrival, duration});
}
// 遍历每个客户,分配窗口并计算等待时间
for (const auto &client : clients)
{
int early = 0; // 可以最早服务客户的窗口索引
//通过循环所有的窗口一遍,找到最先空闲的窗口
for (int i = 1; i < m; i++ )
{
if (windows[i] < windows[early]) early = i;//若有更早空闲的窗口,则更新
}
// 客户在窗口的可用时间到达(有窗口正在空闲),直接办理业务
if (client.arrival >= windows[early])
{
windows[early] = client.arrival + client.duration;
}
else// 没有窗口正在等待,客户需要等待窗口
{
totalWait += windows[early] - client.arrival;
windows[early] += client.duration;
}
}
// 计算平均等待时间
double ans = totalWait / total;
printf("%.2f\n",ans);
}
return 0;
}
注:本题采用的是vector模拟
算法思路如下:
- 为每个窗口设置一个变量来跟踪其下一个可用时间。
- 对于每个客户,找到下一个可用时间最早的窗口。如果有多个窗口同时可用,选择序号最小的窗口。
- 更新该窗口的下一个可用时间为当前时间加上办理业务所需的时间。
- 计算客户的等待时间为窗口的下一个可用时间减去客户到达的时间。
- 累计所有客户的等待时间,并除以客户总数求平均值。
按照题目要求去把每个客户的情况都遍历一遍。(对于每个客户,都先去遍历一遍所有窗口找到最先结束的窗口)
注意:
由于我们在使用栈和队列时使用的都是数组模拟,因此其实操作会更加灵活。比如可以在队列的任意一端进行删除 插入操作