DS博客作业02--栈和队列
0.PTA得分截图
1.本周学习总结(0-4分)
1.1 总结栈和队列内容
一.栈
- 栈的定义
栈是一种只能在一端进行插入或删除操作的线性表,俗称:后进先出。表中允许进行插入、删除操作的一端称为栈顶。
- 栈的进栈出栈规则:
1.栈顶出栈->栈底最后出栈;
2.时进时出->元素未完全进栈时,即可出栈。
- 栈的分类:
1.顺序栈
利用一组地址连续的存储单元依次存放自栈底到栈顶的数据元素,同时附设指针 top 指示栈顶元素在顺序栈中的位置,附设指针 base 指示栈底的位置。
同样,应该采用可以动态增长存储容量的结构。如果栈已经空了,再继续出栈操作,则发生元素下溢,如果栈满了,再继续入栈操作,则发生元素上溢。
栈底指针 base 初始为空,说明栈不存在,栈顶指针 top 初始指向 base,则说明栈空,元素入栈,则 top++,元素出栈,则 top--,
故,栈顶指针指示的位置其实是栈顶元素的下一位(不是栈顶元素的位置)。
2.链栈
其实就是链表的特殊情形,一个链表,带头结点,栈顶在表头,插入和删除(出栈和入栈)都在表头进行,也就是头插法建表和头删除元素的算法。
显然,链栈插入删除的效率较高,且能共享存储空间。
-
栈的基本运算
-
InitStack(&s):初始化栈。构造一个空栈s。
-
DestroyStack(&s):销毁栈。释放栈s占用的存储空间。
-
StackEmpty(s):判断栈是否为空:若栈s为空,则返回真;否则返回假。
-
Push(&S,e):进栈。将元素e插入到栈s中作为栈顶元素。
-
Pop(&s,&e):出栈。从栈s中退出栈顶元素,并将其值赋给e。
-
GetTop(s,&e):取栈顶元素。返回当前的栈顶元素,并将其值赋给e。
-
-
顺序栈的功能操作代码实现
1.图像表示
2.结构体定义
typedef struct
{ ElemType data[MaxSize];
int top; //栈顶指针
} Stack;
typedef Stack *SqStack;
3.基本运算
<1>初始化栈initStack(&s)
void InitStack(SqStack &s)
{ s=new Stack;
s->top=-1;
}
<2>销毁栈ClearStack(&s)
void DestroyStack(SqStack &s)
{
delete s;
}
<3>判断栈是否为空StackEmpty(s)
bool StackEmpty(SqStack s)
{
return(s->top==-1);
}
<4>进栈Push(&s,e)
bool Push(SqStack &s,ElemType e)
{
if (s->top==MaxSize-1)
return false;
s->top++; //栈顶指针增1
s->data[s->top]=e;
return true;
}
<5>出栈Pop(&s,&e)
bool Pop(SqStack &s,ElemType &e)
{
if (s->top==-1) //栈为空的情况,栈下溢出
return false;
e=s->data[s->top];//取栈顶指针元素
s->top--; //栈顶指针减1
return true;
}
<6>取栈顶元素GetTop(s)
bool GetTop(SqStack *s,ElemType &e)
{
if (s->top==-1) //栈为空的情况
return false;
e=s->data[s->top];
return true;
}
4.顺序栈的四要素
栈空条件:top=-1
栈满条件:top=MaxSize-1
进栈e操作:top++; st->data[top]=e
退栈操作:e=st->data[top]; top--;
- 链栈的功能操作代码实现
1.图像表示
2.结构体定义
typedef int ElemType;
typedef struct linknode
{ ElemType data; //数据域
struct linknode *next; //指针域
} LiNode,*LiStack
3.基本运算
<1>初始化栈initStack(&s)
void InitStack(LiStack &s)
{ s=new LiNode;
s->next=NULL;
}
<2>销毁栈ClearStack(&s)
void DestroyStack(LiStack &s)
{ LiStack p;
while (s!=NULL)
{ p=s;
s=s->next;
free(p);
}
}
<3>判断栈是否为空StackEmpty(s)
bool StackEmpty(LiStack s)
{
return(s->next==NULL);
}
<4>进栈Push(&s,e)
void Push(LiStack &s,ElemType e)
{ LiStack p;
p=new LiNode;
p->data=e; //新建元素e对应的节点*p
p->next=s->next; //插入*p节点作为开始节点
s->next=p;
}
<5>出栈Pop(&s,&e)
bool Pop(LiStack &s,ElemType &e)
{ LiStack p;
if (s->next==NULL) //栈空的情况
return false;
p=s->next; //p指向开始节点
e=p->data;
s->next=p->next; //删除*p节点
free(p); //释放*p节点
return true;
}
<6>取栈顶元素GetTop(s,e)
bool GetTop(LiStack s,ElemType &e)
{ if (s->next==NULL) //栈空的情况
return false;
e=s->next->data;
return true;
}
4.链栈的四要素
栈空条件:s->next=NULL
栈满条件:不考虑
进栈e操作:结点插入到头结点后,链表头插法
退栈操作:取出头结点之后结点的元素并删除之
- 对于栈的C++模板类:stack
#include<stack>
1.stack<int> s:初始化栈,参数表示元素类型
2.s.push(t):入栈元素t
3.s.top():返回栈顶元素
4.s.pop():出栈操作只是删除栈顶元素,并不返回该元素
。
5.s1.empty(),当栈空时,返回true。
6.s1.size():访问栈中的元素个数
-
栈的应用
-
网络浏览器多会将用户最近访问过的网址组织为一个栈。
- 用户访问一个新页面,其地址会被存放至栈顶;而“后退”按钮,即可沿相反的次序访问此前刚访问过的页面。
-
递归算法
-
表达式求值--中缀表达式转后缀表达式
-
中缀表达式:运算符号位于两个运算数之间。
-
后缀表达式:运算符号位于两个运算数之后。
-
-
二.队列
- 队列的定义
只允许在表的一端进行插入,而在表的另一端进行删除的线性表。
- 队列的出队入队规则
1.限定在表的一端插入、另一端删除。 插入的那头就是队尾,删除的那头就是队头。也就是说只能在线性表的表头删除元素,在表尾插入元素。
2.先进先出。我们不能在表(队列)的中间操作元素,只能是在尾部进,在头部出去。
- 队列的分类
1.顺序队列
用一组连续的存储单元依次存放队列中的元素。
2.链队列
用链表表示的队列,限制仅在表头删除和表尾插入的单链表。
一个链队列由一个头指针和一个尾指针唯一确定。(因为仅有头指针不便于在表尾做插入操作)。
为了操作的方便,也给链队列添加一个头结点,因此,空队列的判定条件是:头指针和尾指针都指向头结点。
3.环形队列(循环队列)
把顺序队列首尾相连,把存储队列元素的表从逻辑上看成一个环,成为循环队列。
-
队列的基本运算
-
InitQueue(&Q):构造一个空队列Q。
-
DestroyQueue(&Q):销毁队列Q。
-
QueueEmpty(Q):判断队列Q是否为空。
-
QueueLength(Q):返回Q的元素个数,即队列的长度。
-
GetHead(Q, &e):用e返回Q的队头元素。
-
ClearQueue(&Q):将Q清为空队列。
-
EnQueue(&Q, e):插入元素e为Q的新的队尾元素。
-
DeQueue(&Q, &e):删除Q的队头元素,并用e返回其值。
-
-
顺序队列的功能操作代码实现
1.结构体定义
#define MAXQSIZE 100 //最大队列长度
typedef struct
{
QElemType *base; //初始化动态分配存储空间
int front, rear ; /*队首、队尾*/
}SqQueue;
SqQueue Q;
2.基本运算
<1>初始化队列InitQueue(&q)
void InitQueue(SqQueue *&q){
q=(SqQueue *)malloc(sizeof(SqQueue));
q->front=q->rear=-1;
}
<2>销毁队列DestroyQueue(&q)
void DestroyQueue(SqQueue *&q){
free(q);
}
<3>判断队列是否为空QueueEmpty(q)
void QueueEmpty(SqQueue *q){
return (q->front==q->rear);
}
<4>enQueue(&q,e)
bool enQueue(SqQueue *&q,ElemType e){
if(q->rear==MaxSize) return false; //队满上溢
q->rear++;
q->data[q->rear]=e;
return true;
}
<5>出队列deQueue(&q,&e)
bool deQueue(SqQueue *&q,ElemType &e){
if(q->front==q->rear) return false;
q->front++;
e=q->data[q->front];
return true;
}
3.顺序队列的四要素
空队列条件:Q.front==Q.rear
队列满:Q.rear-Q.front=m
入队: Q.base[rear++]=x;
出队:x=Q.base[front++];
- 链队列的功能操作代码实现
1.图像表示
2.结构体定义
typedef int QElemType;
typedef struct QNode {// 结点类型
QElemType data;
struct QNode *next;
} QNode, *QueuePtr;
typedef struct { // 链队列类
QueuePtr front; // 队头指针
QueuePtr rear; // 队尾指针
} LinkQueue;
3.基本运算
<1>创建结点
QueuePtr createNode(int data){
struct Node* newNode=(struct Node*)malloc(sizeof(struct Node));
newNode->next=NULL;
newNode->data=data;
return newNode;
};
<2>队列的初始化
QueuePtr createQueue(){
QueuePtr queue=new QNode;//分配内存空间
queue->front=queue->rear=NULL;//头指针和尾指针在一起为空
queue->queueSize=0;//队列大小为0
return queue;
}
<3>入队操作
void push(QueuePtr queue,int data){
QueuePtr newNode=createNode(data);
if(queue->queueSize==0)
queue->front=newNode;
else
queue->rear->next=newNode;
queue->rear=newNode;
queue->queueSize++;
}
<4>获取对头元素
int queryFront(QueuePtr queue) {
if(queue->queueSize==0){
printf("队列为空无法获取对头元素");
printf("\n");
return -1;
}
return queue->front->data;
}
<5>判断队列是否为空
int empty(QueuePtr queue){
if(queue->queueSize==0)
return 0;
else
return 1;
}
<6>出队操作
void pop (QueuePtr queue){
if(queue->queueSize==0){
printf("队列为空不能出队");
exit(0);
}else{
QueuePtr newFrontNode=queue->front->next;
free(queue->front);
queue->front=newFrontNode;
queue->queueSize--;
}
}
- 循环队列
1.图像表示
2.循环队列的产生
当使用顺序队列时,会遇到明明队中还有位置却不能入队的情况
这是因为采用rear==MaxSize-1作为队满条件的缺陷。
当队满条件为真时,队中可能还有若干空位置。这种溢出并不是真的溢出,称为假溢出。
为了解决这种情况下造成的假溢出
把数组的前端和后端连接起来,形成一个环形的顺序表,即把存储队列元素的表从逻辑上看成一个环,称为环形队列或循环队列。
3.循环队列的入/出队列运算(利用“模运算”)
-
入队:Q.rear=(Q.rear+1)%m
-
出队:Q.front=(Q.front+1)%m
4.循环队列的四要素
队空条件:front=rear
队满条件:(rear+1)%MaxSize=front
进队e操作:rear=(rear+1)%MaxSize;将e放在rear处;
出队操作:front=(front+1)%MaxSize;取出front处元素e;
- 对于队列的C++模板类:queue
1.push():将x入队,时间复杂度为O(1)
2.front()back():分别可以获得队首元素和队尾元素,时间复杂度为O(1)
3.pop():令队首元素出队,时间复杂度为O(1)
4.empty():检查queue是否为空,返回true则空,返回false则非空,时间复杂度为O(1)
5.size():返回queue内元素的个数,时间复杂度为O(1)
-
队列的应用
-
重复密钥
-
凯撒加密
- 通过将字母按顺序推后3位起到加密作用
-
图的宽度优先搜索法
-
操作系统的作业调度,用于缓冲区,网络中的路由缓冲包区。
-
1.2.谈谈你对栈和队列的认识及学习体会。
栈(Stack)是限定仅在表尾进行插入和删除操作的线性表。
队列(Queue)是允许在一端进行插入,一端进行删除操作的线性表。
它们都可以用线性表的顺序存储结构来实现,但都存在着顺序存储的一些弊端。
对于栈来说,如果是两个相同数据类型的栈,则可以用数组的两端作栈底的方法来让两个栈共享数据,这就可以最大化利用数组的空间。
对于队列来说,为了避免插入删除需要移动数据,于是就引入了循环队列,使得队头和队尾可以在数组中循环变化。
解决了移动数据的时间损耗,使得本来插入和删除是O(n)的时间复杂度变成O(1)。
在学习了栈和队列的相关内容之后,在场景的应用方面又得到了一定的拓展,面对之前无法解决的情况也得到了新的方法。
在接下来对存储结构操作的不断学习中,应付不同问题的手法也将越来越熟练。
2.PTA实验作业(0-2分)
2.1.题目1:7-5 表达式转换 (25分)
2.1.1代码截图
2.1.2本题PTA提交列表说明。
Q1:没有考虑到当表达式头或左括号后为+-符号时对应的操作,应加上该情况下对应的操作
Q2:在当经过在表达式头或左括号后为+-号后,没有将flag1、2的值赋为1,从而导致非指定条件下进入错误的判断
2.2 题目2:7-7 银行业务队列简单模拟 (25分)
2.2.1代码截图
2.2.2本题PTA提交列表说明。
Q1:没有判断输出空格的条件,导致末尾留有空格而格式错误
Q2:在输出空格的判断条件中忽略了A、B至少有一者非空就要输出空格的整体性
3.阅读代码(0--4分)
3.1 题目及解题代码
题目
解题代码
class Solution {
public:
bool validateStackSequences(vector<int>& pushed, vector<int>& popped) {
stack<int> st;
int n = popped.size();
int j = 0;
for (int i = 0; i < pushed.size(); ++i){
st.push(pushed[i]);
while(!st.empty() && j < n && st.top() == popped[j]){
st.pop();
++j;
}
}
return st.empty();
}
};
3.1.1 该题的设计思路
时间复杂度:O(N)
空间复杂度:O(N)
3.1.2 该题的伪代码
初始化栈 stack,j = 0;
遍历 pushed 中的元素 x;
当 j < popped.size() 且栈顶元素等于 popped[j]:
弹出栈顶元素;
j += 1;
如果栈为空,返回 True,否则返回 False。
3.1.3 运行结果
符合情况
不符情况
3.1.4分析该题目解题优势及难点。
解题优势:
思路很简单,尝试按照 popped 中的顺序模拟一下出栈操作,如果符合则返回 true,否则返回 false。
这里用到的贪心法则是如果栈顶元素等于 popped 序列中下一个要 pop 的值,则应立刻将该值 pop 出来。
难点:
使用一个栈 st 来模拟该操作。
将 pushed 数组中的每个数依次入栈,在入栈的同时需要判断这个数是不是 popped 数组中下一个要 pop 的值。
如果是就把它 pop 出来。
最后检查栈是否为空。
3.2 题目及解题代码
题目
解题代码
using PII = pair<int, int>;
class Solution {
public:
bool canMeasureWater(int x, int y, int z) {
stack<PII> stk;
stk.emplace(0, 0);
auto hash_function = [](const PII& o) {return hash<int>()(o.first) ^ hash<int>()(o.second);};
unordered_set<PII, decltype(hash_function)> seen(0, hash_function);
while (!stk.empty()) {
if (seen.count(stk.top())) {
stk.pop();
continue;
}
seen.emplace(stk.top());
auto [remain_x, remain_y] = stk.top();
stk.pop();
if (remain_x == z || remain_y == z || remain_x + remain_y == z) {
return true;
}
stk.emplace(x, remain_y);
stk.emplace(remain_x, y);
stk.emplace(0, remain_y);
stk.emplace(remain_x, 0);
stk.emplace(remain_x - min(remain_x, y - remain_y), remain_y + min(remain_x, y - remain_y));
stk.emplace(remain_x + min(remain_y, x - remain_x), remain_y - min(remain_y, x - remain_x));
}
return false;
}
};
3.2.1 该题的设计思路
时间复杂度:O(xy)
空间复杂度:O(xy)
3.2.2 该题的伪代码
stack<PII> stk;
stk.emplace(0, 0);
以 remain_x, remain_y 作为状态,表示 X 壶和 Y 壶中的水量。
HashSet存储所有已经搜索过的 remain_x, remain_y 状态,
保证每个状态至多只被搜索一次。
while (!stk.empty())
把 X 壶灌满。
把 Y 壶灌满。
把 X 壶倒空。
把 Y 壶倒空。
把 X 壶的水灌进 Y 壶,直至灌满或倒空。
把 Y 壶的水灌进 X 壶,直至灌满或倒空。
end while
返回flase
3.2.3 运行结果
符合情况:
不符情况:
3.2.4分析该题目解题优势及难点。
解题优势:
在实际的代码编写中,由于深度优先搜索导致的递归远远超过了 Python 的默认递归层数,
因此下面的代码使用栈来模拟递归,避免了真正使用递归而导致的问题。
难点:
在每一步搜索时,会依次尝试所有的操作,递归地搜索下去。这可能会导致陷入无止境的递归。