0.PTA得分截图
1.1 总结栈和队列内容
栈的存储结构及操作:栈的本质其实还是线性表:限定仅在表尾进行插入或删除操作
同样的,栈也分为顺序和链式两大类。其实和线性表大同小异,只不过限制在表尾进行操作的线性表的特殊表现形式
- 1、顺序栈:利用一组地址连续的存储单元依次存放自栈底到栈顶的数据元素,同时附设指针 top 指示栈顶元素在顺序栈中的位置,附设指针 base 指示栈底的位置。
- 2、链栈:其实就是链表的特殊情形,一个链表,带头结点,栈顶在表头,插入和删除(出栈和入栈)都在表头进行,也就是头插法建表和头删除元素的算法。显然,链栈还是插入删除的效率较高,且能共享存储空间。
栈的操作:
栈只能在栈顶进行操作,链栈的插入可理解为头插法建链,即按(a,b,c)插入栈,栈顶为a
代码表示
顺序栈
void Push(Stack *s,int x)
{
if(!IsFull(*s))
{
s->data[s->top]=x;
s->top++;
}
else
printf("栈溢出");
}
链栈
void Push(LinkStack *top,int x)
{
LinkStack *s;
s=new Linkstack*
s->data=x;
s->next=top;
top=s;
}
栈的删除(出栈):
栈的删除要遵循进栈顺序,同时也只能在栈顶操作
代码表示
顺序栈
int Pop(Stack *s)
{
if(!IsEmpty(*s))
{
s->top--;
return s->data[s-top];
}
else
{
printf("栈空");
return 0;
}
}
链栈
int Pop(LinkStack *top)
{
LinkStack *s;
int data;
if(!IsEmpty(*top))
{
s=*top;
*top=(*top)->next;
data=s->data;
free(s);
return data;
}
else
{
printf("栈空");
return 0;
}
}
取栈顶元素:因为栈的操作之在栈顶进行,所以对于栈顶元素的操作格外重要;取栈顶元素主要在于此时栈中是否存在元素,若栈空则取栈顶元素无效
顺序栈
int GetTop(Stack s)
{
if(!IsEmpty(s))
return s.data[s.top-1]
else
{
printf("栈空");
returnERROR;
}
}
链栈
int GetTop(LinkStack *top)
{
if(!IsEmpty(s))
return top->data;
else
{
printf("栈空");
returnERROR;
}
}
判断栈空:栈的删除,取栈顶操作基于栈是否空,若栈空则两个操作都无效
顺序栈
bool IsEmpty(Stack s)
{
if(s.top==0)
return true;
else
return false;
}
链栈
bool IsEmpty(LinkStack *top)
{
if(top==NULL)
return true;
else
return false;
}
C++ STL之stack栈容器
在c++中,因为栈的一系列操作过于麻烦所以我们有一个stack容器
- 一、STL:
1)标准模版库,提供了通用的模版库和函数。如:向量、链表、队列、栈。
2)核心组建包括:容器(Containers)、算法(Algorithms)、迭代器(Iterators) - 二、Stack栈容器:
1)容器适配器,遵循先进后出(FILO)数据结构。
2)头文件:#include
3)成员函数
empty()
测试栈是否为空,为空返回true,否则返回false。
size()
返回栈中元素的个数
top()
返回栈顶元素(最后push进来的那个)的引用。
push(val)
压一个值到栈中,其值将被初始化为 val
pop()
将栈顶元素弹出,注意这个函数无返回值,如果需要获取栈顶元素,应先调用top(),再pop()
swap()
swap将两个 stack的内容交换。这两个 stack的模板参数 T和 Container必须都相同。
栈的应用:
栈是一个重要的数据结构,其特性简而言之就是“后进先出”,这种特性在计算机中有着广泛的运用。其实程序员无时无刻不在使用者栈,函数的调用是我们间接使用栈的最好的例子,但是栈在实际中的运用远不止这些,比较经典的应用还包括括号匹配、逆波兰表达式的求值等.
括号匹配:
在PTA题目练习中我们遇到过两道题目,都是符号配对,而符号配对就是栈的经典应用
符号配对中我们主要在乎四个问题
(1)左右括号匹配正确
(2)左右括号匹配错误
(3)左括号多于右括号
(4)右括号多于左括号
其中我们将左括号放入栈之后取栈顶来匹配,如一组数据({[]})
问题解决:通过遍历栈与右括号配对来匹配
代码展示
#include
#include
#include
#include
逆波兰表达式:通俗说就是中缀表达式转后缀表达式
其核心在于分离数字与符号时判断符号的优先级,而优先级决定了出栈与入栈的顺序
如数据2+3*(7-4)+8/4
代码展示
#include
#include
#include
#include
using namespace std;
stack st;
int main()
{
string str;
cin >> str;
int flag = 0;
int temp = 0;
for (int i = 0; str[i]!=0; i++)
{
if (str[i] >= '0' && str[i] <= '9')
{
if (flag == 0 && temp == 0)
{
cout << str[i];
flag = 1;
}
else if (str[i - 1] >= '0' && str[i - 1] <= '9')
{
cout << str[i];
}
else if (str[i - 1] == '.')
{
cout << "." << str[i];
}
else
{
cout << " " << str[i];
}
}
else if (str[i] == '+' || str[i] == '-')
{
if (str[i-1]=='('||i==0)
{
if (str[i] == '-')
{
if (i == 0)
{
cout << str[i] << str[i + 1];
temp = 1;
}
else
cout << " " << str[i] << str[i + 1];
i++;
}
else
continue;
}
else if (st.empty())
{
st.push(str[i]);
}
else
{
while (1)
{
if (!st.empty() && st.top() != '(')
{
cout << " " << st.top();
st.pop();
}
else
{
st.push(str[i]);
break;
}
}
}
}
else if (str[i] == '/' || str[i] == '*')
{
if (st.empty())
{
st.push(str[i]);
}
else if (st.top() == '/' || st.top() == '*')
{
while (1)
{
if (st.top() == '/' || st.top() == '*' || !st.top() == '(')
{
cout << " " << st.top();
st.pop();
}
else
{
st.push(str[i]);
break;
}
}
}
else
{
st.push(str[i]);
}
}
else if (str[i] == '(')
{
st.push(str[i]);
}
else if (str[i] == ')')
{
while (1)
{
if (st.top() != '(')
{
cout << " " << st.top();
st.pop();
}
else
{
st.pop();
break;
}
}
}
}
while (!st.empty())
{
if (st.top() == '(')
st.pop();
else
{
cout << " " << st.top();
st.pop();
}
}
return 0;
}
队列的存储结构及操作
队列:队列是一种特殊的线性表,特殊之处在于它只允许在表的前端(front)进行删除操作,而在表的后端(rear)进行插入操作,和栈一样,队列是一种操作受限制的线性表。进行插入操作的端称为队尾,进行删除操作的端称为队头。
因为队列判断队满的条件,有时队还有空时就会给出队满的判断,这就是假溢出,为了解决假溢出我们引入了新的概念‘循环队列’
循环队列:将向量空间想象为一个首尾相接的圆环,并称这种向量为循环向量。存储在其中的队列称为循环队列(Circular Queue)。循环队列是把顺序队列首尾相连,把存储队列元素的表从逻辑上看成一个环,成为循环队列。
队列的存储结构
顺序队列:创建顺序存储结构,我们必须动态申请或静态地分配一块存储位置,设置两个指针来进行管理,分别称为对头front、队尾rear,当队头与队尾相等时
我们称为队空
入队
void EnQueue(Queue *q,int x)
{
if((q->rear+1)%maxsize==q->front)
exit(0);
else
{
q->rear=(q->rear+1)%maxsize;
q->data[q->rear]=x;
}
}
出队
void DeQueue(Queue *q,int &x)
{
if(QueueEmpty(*q))//判断队空
exit(0);
else
{
q->front=(q->front+1)%maxsize;
x=q->data[q->front];
}
}
取队首
voidGetFront(Queue *q,int x)
{
if(QueueEmpty(q))
exit(0);
else
x=q.data[q.front+1]%maxsize;
}
链式队列:链式存储结构其实就是线性表的单链表,只是只能在对头出元素,队尾进元素
代码展示
入队
void EnQueue(Queue *q,int x)
{
q-rear-next=new LinkQueue;
q->rear=q->rear->next;
q->rear->data=x;
q->rear->next=NULL;
}
出队
void DeQueue(Queue *q)
{
LinkQueue *p;
int data;
if(!IsEmpty(*q))//判断队空
{
p=q->front->next;
q->front->next=p->next;
if(p->next==NULL)
q-rear=q->front;
data=p->data;
free(p);
return data;
}
队列应用
迷宫问题
迷宫问题在前面栈的应用中有所实现但是迷宫问题有时候不单单是找到出路,还有可能是找到最短路径,队列实现迷宫问题采用广度优先遍历,采取树叉衍生通过最快得到的路径得到最短路径,也就是说,队列找到的一定是最短路径
相当于栈,队列迷宫问题应用更广泛
代码展示
#include
#define MaxSize 100
#define M 8
#define N 8
typedef struct
{
int i,j;
//方块在迷宫中的坐标位置(i,j)
int pre; //本路径中上一方块在队列中的下标
} SqQueue;
SqQueue Qu[MaxSize]; //定义顺序非循环队列
int front=0,rear=0;
int mg[M+2][N+2]={
{1,1,1,1,1,1,1,1,1,1},
{1,0,0,1,0,0,0,1,0,1},
{1,0,0,1,0,0,0,1,0,1},
{1,0,0,0,0,1,1,0,0,1},
{1,0,1,1,1,0,0,0,0,1},
{1,0,0,0,1,0,0,0,0,1},
{1,0,1,0,0,0,1,0,0,1},
{1,0,1,1,1,0,1,1,0,1},
{1,1,0,0,0,0,0,0,0,1},
{1,1,1,1,1,1,1,1,1,1}
};
void print(SqQueue Qu[],int front)
{
int k=0;
for(int i=front;i>0;i=Qu[i].pre)
{
printf("(%d,%d) ",Qu[i].i,Qu[i].j);
k++;
if(k%5==0) //每输出每5个方块后换一行
printf("\n");
}
}
bool mgpath1(int xi,int yi,int xe,int ye)
{ int i, j, di, i1, j1;
rear++;
Qu[rear].i=xi;
Qu[rear].j=yi;
Qu[rear].pre=-1; //(xi,yi)进队
mg[xi][yi]=-1; //将其赋值-1,以避免回过来重复搜索
while(front!=rear)
//队不空循环
{
front++;
i=Qu[front].i;
j=Qu[front].j; //出队
if (i==xe && j==ye) //找到了出口,输出路径
{
print(Qu, front); //调用print函数输出路径
return true; //找到一条路径时返回真
}
for (di=0;di<4;di++) //循环扫描每个方位
{
switch(di)
{
case 0:i1=i-1;
j1=j; break;
case 1:i1=i; j1=j+1; break;
case 2:i1=i+1; j1=j; break;
case 3:i1=i; j1=j-1; break;
}
if (mg[i1][j1]==0)
{
rear++;
Qu[rear].i=i1;
Qu[rear].j=j1;
Qu[rear].pre=front; //(i1,j1)方块进队
mg[i1][j1]=-1; //将其赋值-1
} }
}
return false;
}
int main()
{
if (!mgpath1(M,N,1,1))
printf("该迷宫问题没有解!");
return 1;
}
银行问题:我们熟知的银行排队就是队列的一种形式,先来先进,边进边出,同时多窗口问题也是多队列的实现
代码
#include
#include
#include
using namespace std;
int main()
{
int N;
cin >> N;
queueque0;
queueque1;
for (int i = 0; i < N; i++)
{
int num;
cin >> num;
if (num % 2 != 0) //分成奇偶队列
que1.push(num);
else que0.push(num);
}
int flag = 0; //用来控制第一个输出的编号前面不带空格
for (int i = 1;; i++)
{
if (que0.empty() && que1.empty())
break;
if (!que1.empty())
{
if (flag == 0)
cout << que1.front();
else cout << " " << que1.front();
que1.pop();
flag = 1;
}
if (i % 2 == 0 && !que0.empty()) {
if (flag == 0)
cout << que0.front();
else cout << " " << que0.front();
que0.pop(); flag = 1;
}
}
cout << endl;
return 0;
}
相同的,为了便于操作C++中也存在队列的容器Queue容器
queue模版类的定义在 头文件中。
queue与stack模版非常类似,queue模版也需要定义两个模版参数,一个是元素类型,一个是容器类型,元素类型是必要的,容器类型是可选的,默认为dqueue类型。
定义queue对象的示例代码如下:
queue q1;
queue q2;
queue的基本操作有:
1.入队:如q.push(x):将x元素接到队列的末端;
2.出队:如q.pop() 弹出队列的第一个元素,并不会返回元素的值;
3,访问队首元素:如q.front()
4,访问队尾元素,如q.back();
5,访问队中的元素个数,如q.size();
1.2.谈谈你对栈和队列的认识及学习体会
- 栈与队列都遵循先来先进的原则,不同的是栈只能一边操作不是进就是出,二者不能同时进行,而队列却可以进的同时另一端出;同时栈和队列操作在时间复杂度上都是O(1);但是栈和队列不同在于,栈遵循先进后出,队列遵循先进先出;
- 栈与队列的学习在两个容器使用上方便了很多,所以对于二者我们主要在于对逻辑的判断以及应用的灵活,在迷宫问题与银行问题中,理解并不是难事,难点主要在于去实现,同时将栈或队列的的规则熟记
2.PTA实验作业
2.1.题目1:7-5 表达式转换
2.1.1代码截图
2.1.2本题PTA提交列表说明。
部分正确:考虑时将负数为第一个与在括号内两种情况合并讨论||写成&&
部分正确:注意到两种情况没有分开但是忘记删去了为括号前的i==0
2.2 题目2:7-7 银行业务队列简单模拟
2.2.1代码截图
2.2.2本题PTA提交列表说明
段错误:输出时奇偶队列一直在输出奇队列导致访问出错
本题的两个队列对应AB窗口,其中将AB时间控制改为A输出两个B输出一个即i要为2的倍数B才输出,而A一直输出
3.阅读代码(0--4分)
3.1 题目及解题代码
class MinStack {
public:
/** initialize your data structure here. */
MinStack() {
}
void push(int x) {
main_stk.push(x);
if(help_stk.empty()) help_stk.push(x);
else {
if(x < help_stk.top()){
help_stk.push(x);
}else{
int temp = help_stk.top();
help_stk.push(temp);
}
}
}
void pop() {
main_stk.pop();
help_stk.pop();
}
int top() {
return main_stk.top();
}
int getMin() {
return help_stk.top();
}
private:
stack main_stk;
stack help_stk;
};
3.1.1 该题的设计思路
使用一个辅助栈来存当前最小值
在每一次push的时候,主栈正常push,压入数据;副栈执行一次判断,如果当前值小于顶点值,最小值还是之前的值反之则当前值是最小值
3.1.2 该题的伪代码
void push(int x) {
进主栈
if(栈空)
直接进辅助栈
else {
if(值小于当前辅助栈顶值){
进栈
}else{
将栈顶值再次入栈
}
}
}
void pop() {
出栈
}
int top() {
返回主栈栈顶元素
}
int getMin() {
返回辅助栈栈顶值
}
private:
定义主栈,及辅助栈
};
3.1.3 运行结果
3.1.4分析该题目解题优势及难点。
解题优势:一般按思路我会将栈元素进行排序,这样出栈的就是我要的最小值,但是这样题目时间复杂度可能不符合要求,该解题方式通过构建辅助栈将排序环节直接通过栈顶元素判断解决,避免了更多的步骤,同时对于二次取栈顶也方便许多
难点:该方式适用于该题却不适用于延伸题目,二次取最小可以适用但是如果多次取就会出现问题
3.2 题目及解题代码
class Solution {
public:
int minIncrementForUnique(vector& A) {
sort(A.begin(), A.end());
int dup = 0; // 重复数个数
int dup_sum = 0; // 重复数下标和
int sit_sum = 0; // 空位下标和
for (int i = 1; i < A.size(); ++i) {
if (A[i] == A[i - 1]) {
++dup;
dup_sum += A[i];
} else {
int t = A[i - 1] + 1;
// 有空位且还有未放置的重复数,则逐一放置
while (t < A[i] && dup > 0) {
sit_sum += t;
--dup;
++t;
}
}
}
if (dup > 0) {
int t = A.back() + 1;
sit_sum += (2 * t + dup - 1) * dup / 2;
}
return sit_sum - dup_sum;
}
};
3.2.1 该题的设计思路
用vector函数建立数组
运用sort函数将所有数排序再进行判断
通过未出现数与重复出现数来判断move次数
3.2.2 该题的伪代码
sort函数排序
定义变量dup存数组有重复元素个数
定义dup_sum为 重复数下标和
定义 sit_sum 为未出现相邻值之和
for (int i to A.size()) {
if (相邻两数相等) {
++dup;
下标大得数赋值给dup_sum
} else {
定义t为元素相邻数
while (t < A[i] && dup > 0) {
t累加
重复数减一
t自增
}
end while
}
}
if (此时重复数大于0) //最后一个数与前面一个数之间并不相邻
{
将数组最后一个数自增后赋值给t
未出现数之和sit_sum += (2 * t + dup - 1) * dup / 2;
}
返回未出现数之和与重复出现数之差即要自增多少次才能出现唯一数组
}
};
3.2.3 运行结果
3.2.4分析该题目解题优势及难点。
题解优势:sort函数的应用使排序更简单,同时排序后更便于计算move,未出现数的累计成了解题的关键,将未出现数与重复出现数相比将数组元素变为相邻数,同时二者只差就已经解决了move
难点:改题目的思路是亮点也是难点,未出现数的累计和就是难点,特别是最后的dup判断,以及返回数的确定是题目省心的关键但是若没有明确思路就很难解决