DS博客作业02--栈和队列
0.PTA得分截图
栈和队列题目集总得分,请截图,截图中必须有自己名字。题目至少完成2/3(不包括选择题),否则本次作业最高分5分。
1.本周学习总结(0-4分)
1.1 总结栈和队列内容
1.1.1 栈
当你用浏览器上网时,不管什么浏览器都有一个“后退”键,你点击后可以按访问顺序的逆序加载浏览过的网页。即使你从一个网页开始,连续点了几十个链接跳转,你点“后退” 时,还是可以像历史倒退一样,回到之前浏览过的某个页面。又或者你在word里面快乐地写论文的时候,一不小心将一整段内容删除了,这个时候不要慌,我们可以点Ctrl+z,撤销之前的删除操作。这就是栈在现实生活之中的应用栈是限定仅在表尾进行插入和删除操作的线性表。我们把允许插入和删除的一端称为栈顶(top),另一端称为栈底(bottom),不含任何数据元素的找称为空栈。栈是一种后进先出的线性表。
(1) 顺序栈
既然栈是线性表的特例,那么栈的顺序存储其实也是线性表顺序存储的简化,我们将其简称为顺序栈。
顺序栈的结构体定义:
#include "stdio.h"
/* 存储空间初始分配量 */
#define MAXSIZE 20
/* SElemType类型根据实际情况而定,这里假设为int */
typedef int SElemType;
/* 顺序栈结构 */
typedef struct
{
SElemType data[MAXSIZE];
int top; /* 用于栈顶指针 */
}SqStack;
顺序栈的操作
由于顺序栈属于后进先出的结构,所以可以将它想象成一种容器。若现在有一个栈Sta,StackSize是5,则栈普通情况、空栈和栈满的情况示意图如下。
(1)顺序栈的进栈操作:
a.栈顶指针 S->top 先自增1,给需要进栈的元素腾出内存空间。
b.再赋值。就是给对应的数组元素赋值:S->data[S->top]=e
/* 插入元素e为新的栈顶元素 */
Status Push(SqStack *S,SElemType e)
{
if(S->top == MAXSIZE -1) /* 栈满 */
{
return ERROR;
}
S->top++; /* 栈顶指针增加一 */
S->data[S->top]=e; /* 将新插入元素赋值给栈顶空间 */
return OK;
}
(2)顺序栈的出栈操作:
进栈是先自增再赋值,出栈则反过来。先把要出栈的元素获取到,然后再指针自减,把空间释放出来。
/* 若栈不空,则删除S的栈顶元素,用e返回其值,并返回OK;否则返回ERROR */
Status Pop(SqStack *S,SElemType *e)
{
if(S->top==-1)
return ERROR;
*e=S->data[S->top]; /* 将要删除的栈顶元素赋值给e */
S->top--; /* 栈顶指针减一 */
return OK;
}
(2)顺序栈的其他操作:
/* 构造一个空栈S */
Status InitStack(SqStack *S)
{
/* S.data=(SElemType *)malloc(MAXSIZE*sizeof(SElemType)); */
S->top=-1;
return OK;
}
/* 顺序栈的遍历:从栈底到栈顶依次对栈中每个元素显示 */
Status StackTraverse(SqStack S)
{
int i;
i=0;
while(i<=S.top)
{
visit(S.data[i++]);
}
printf("\n");
return OK;
}
Status visit(SElemType c)
{
printf("%d ",c);
return OK;
}
/* 在栈不空的情况下返回栈顶元素并出栈 */
Status Pop(SqStack *S,SElemType *e)
{
if(S->top==-1)
return ERROR;
*e=S->data[S->top]; /* 将要删除的栈顶元素赋值给e */
S->top--; /* 栈顶指针减一 */
return OK;
}
(2)链栈:
链栈是没有附加头结点的运算受限的单链表。栈顶指针就是链表的头指针。对于链栈来说,一般情况下基本不存在栈满的情况。链栈为空的判断的条件是top=NULL;链栈的结构体定义如下:
typedef int Status;
/* SElemType类型根据实际情况而定,这里假设为int */
typedef int SElemType;
/* 链栈结构 */
typedef struct StackNode
{
SElemType data;
struct StackNode *next;
} StackNode,*LinkStackPtr;
typedef struct
{
LinkStackPtr top;
int count;
} LinkStack;
链栈的操作与对链表的操作基本相同。
/* 链栈的进栈操作 */
Status Push(LinkStack *S,SElemType e)
{
LinkStackPtr s=(LinkStackPtr)malloc(sizeof(StackNode));
s->data=e;
s->next=S->top; /* 把当前的栈顶元素赋值给新结点的直接后继,见图中① */
S->top=s; /* 将新的结点s赋值给栈顶指针,见图中② */
S->count++;
return OK;
}
/* 出栈操作 */
Status Pop(LinkStack *S,SElemType *e)
{
LinkStackPtr p;
if(StackEmpty(*S))
return ERROR;
*e=S->top->data;
p=S->top; /* 将栈顶结点赋值给p,见图中① */
S->top=S->top->next; /* 使得栈顶指针下移一位,指向后一结点,见图中② */
free(p); /* 释放结点p */
S->count--;
return OK;
}
1.1.2 队列:
在日常生活中,我们使用电脑的时候。机器有时会处于疑似死机的状态,鼠标怎么点都没用,双击任何快捷方式都不动弹。就当你失去耐心,打算重启时,突然它能动了,把你刚才点击的所有操作全部都按顺序执行了一遍。这其实是因为操作系统中的多个程序因需要通过一个通道输出,而按先后次序排队等待造成的。这就是队列。队列是只允许在一端进行插入操作,而在另一端进行删除操作的线性表。
(1)顺序队列
线性表分为顺序存储和链式存储,栈是线性表,所以也有这两种存储方式。同样,队列作为一种特殊的线性表,也同样存在这两种存储方式。我们一直都是用数组来实现顺序存储的,顺序队列也不例外。所以我们可以定义一个数组 int data[MAXSIZE] 来存储队列的元素。另外,我们还需要两个指针,来标记队头和队尾,所以定义如下:
* QElemType类型根据实际情况而定,这里假设为int */
typedef int QElemType;
/* 循环队列的顺序存储结构 */
typedef struct
{
QElemType data[MAXSIZE];
int front; /* 头指针 */
int rear; /* 尾指针,若队列不空,指向队列尾元素的下一个位置 */
}SqQueue;
如果队不满,我们就可以入队了。入队的思路就是,先给队尾元素赋值,然后再将队尾指针向后移动一位。比如从空队列开始,此时 Q->front == Q->rear,这个时候插入元素的话,其实就是给 data[Q->rear] 赋值 e,然后队尾指针 Q->rear 向后移动一位重新赋值。虽然我们在定义队列的时候没这个长度变量,但是我们可以通过模计算来取得我们需要的。将队尾指针向后移动一位?很简单,Q->rear = (Q->rear+1)%MAXSIZE; 即可。
/* 入队操作 */
Status EnQueue(SqQueue *Q,QElemType e)
{
if ((Q->rear+1)%MAXSIZE == Q->front) /* 队列满的判断 */
return ERROR;
Q->data[Q->rear]=e; /* 将元素e赋值给队尾 */
Q->rear=(Q->rear+1)%MAXSIZE;/* rear指针向后移一位置, */
/* 若到最后则转到数组头部 */
return OK;
}
/* 出队操作*/
Status DeQueue(SqQueue *Q,QElemType *e)
{
if (Q->front == Q->rear) /* 队列空的判断 */
return ERROR;
*e=Q->data[Q->front]; /* 将队头元素赋值给e */
Q->front=(Q->front+1)%MAXSIZE; /* front指针向后移一位置, */
/* 若到最后则转到数组头部 */
return OK;
}
(2)链队:
队列的链式存储结构,其实就是线性表的单链表,只不过它只能尾进头出而已,我们把它简称为链队列。
/* QElemType类型根据实际情况而定,这里假设为int */
typedef int QElemType;
typedef struct QNode /* 结点结构 */
{
QElemType data;
struct QNode *next;
} QNode,*QueuePtr;
typedef struct /* 队列的链表结构 */
{
QueuePtr front,rear; /* 队头、队尾指针 */
} LinkQueue;
链队的操作,入队出队,大致思路:
(1)我们先创建一个结点s,QueuePtr s=(QueuePtr)malloc(sizeof(QNode));
(2)然后给s的data域赋值e,指针域next赋值null。s->data=e;s->next=NULL; 目的就是让它成为新任队尾元素。
(3)前任队尾元素直接让它的指针域指向s就行了。Q->rear->next=s;
(4)别忘了把队尾指针重新指向新任队尾s。Q->rear=s;
代码如下:
/* 插入元素e为Q的新的队尾元素 */
Status EnQueue(LinkQueue *Q,QElemType e)
{
QueuePtr s=(QueuePtr)malloc(sizeof(QNode));
if(!s) /* 存储分配失败 */
exit(OVERFLOW);
s->data=e;
s->next=NULL;
Q->rear->next=s; /* 把拥有元素e的新结点s赋值给原队尾结点的后继,见图中① */
Q->rear=s; /* 把当前的s设置为队尾结点,rear指向s,见图中② */
return OK;
}
出队操作时,就是头结点的后继结点出队,将头结点的后继改为它后面的结点,若链表除头结点外只剩一个元素时,则需将rear指向头结点。具体步骤如下:
(1)如图中,要删除掉a1结点,思路很简单,就是让头结点Q->front的后继next直接指向a2。但是a2如何标识呢?
(2)假设a1结点为p结点,那么a2就是p->next了。如何让a1结点存到p呢?
(3)直接让头结点的后继指向p就行,p=Q->front->next;
(4)假如队尾已经是p结点的话(Q->rear==p),队尾指针需要指向头结点Q->rear=Q->front;
(5)最后别忘了把p free掉。
/* 若队列不空,删除Q的队头元素,用e返回其值,并返回OK,否则返回ERROR */
Status DeQueue(LinkQueue *Q,QElemType *e)
{
QueuePtr p;
if(Q->front==Q->rear)
return ERROR;
p=Q->front->next; /* 将欲删除的队头结点暂存给p */
*e=p->data; /* 将欲删除的队头结点的值赋值给e */
Q->front->next=p->next;/* 将原队头结点的后继p->next赋值给头结点后继 */
if(Q->rear==p) /* 若队头就是队尾,则删除后将rear指向头结点 */
Q->rear=Q->front;
free(p);
return OK;
}
1.2.谈谈你对栈和队列的认识及学习体会。
学习栈与链表的时候感觉这两个概念好懂,但是真正做题的时候又十分地抽象,题目通常很长,需要利用栈和容器地性质才能很好的解决问题。所以,这一部分的习题做的不是太好。开学已经近6周了,总是觉得自己的状态不怎么良好。线上教学确实能比现实中上课听得清楚,但是自己需要抵制各种诱惑。还有,最近代码量有点少,所以看见题目会有一种无从下手的感觉。在家确实没有学校氛围好,自己会懈怠的。总而言之,还是看自己,不能步步都跟不上不是吗?努力吧!
2.PTA实验作业(0-2分)
选2道PTA题目,不写伪代码,只贴代码截图,并记录代码提交碰到问题及解决方法。不得选栈或队列操作(选,则为0分)选择难度越高题目得分越高。
2.1.题目1:7-3 jmu-ds-符号配对
2.1.1代码截图(注意,截图,截图,截图。不要粘贴博客上。)
2.1.2本题PTA提交列表说明。
Q:多种错误;
A:开始按着自己的思路写了下来,输出时的格式不正确,导致错误;
Q部分正确:
A:这次提交过了两个测试点。由于我没有调用函数,直接在main()里面实现,所以代码重复的地方多;当输入的字符串是匹配的时候,可以输出”yes”;但是当栈内仍然存在元素(如测试点”[{“),也会输出”yes”;所以另外加了一个判断条件,判断循环结束时栈是否为空;
Q:部分正确;
A:起初先判断flag值是否进行了变通,再判断栈是否为空。这样子做,如果flag值被改变,栈为空,那么即会输出yes,也会输出no。所以,将栈空判断放在前面就会解决这个问题。
2.2题目2:7-6 jmu-报数游戏
2.2.1代码截图(注意,截图,截图,截图。不要粘贴博客上。)
2.2.2本题PTA提交列表说明。
Q:答案错误;
A:开始的时候没看懂题目的意思,以为m%3==1的情况下才进行输出,所以第一次答案错误。在调试中发现了这个问题,所以没有提交;
Q:部分正确;
A:格式不符合导致的错误。题目要求每一个数字之间要有空格隔开,结尾不能留空格。所以要控制一下输出格式。因为我用的是链队,所以不方便对队尾数字进行操作,所以对第一个数字进行格式控制。刚开始改的时候没有注意第一个数字也要出队,导致输出的数据多了一个。不要忘记判断m与n的大小,m>n时要直接报错。
3.阅读代码(0--4分)
3.1 题目及解题代码
可截图,或复制代码,需要用代码符号渲染。题目截图后一定要清晰。
/*官方解题代码*/
void hanota(vector<int>& A, vector<int>& B, vector<int>& C) {
int n = A.size();
move(n, A, B, C);
}
void move(int n, vector<int>& A, vector<int>& B, vector<int>& C){
if (n == 1){
C.push_back(A.back());
A.pop_back();
return;
}
move(n-1, A, C, B); // 将A上面n-1个通过C移到B
C.push_back(A.back()); // 将A最后一个移到C
A.pop_back(); // 这时,A空了
move(n-1, B, A, C); // 将B上面n-1个通过空的A移到C
}
};
3.1.1 该题的设计思路
解题思路:递归与分治
首先我们来回顾一下这个问题。有 A,B,C 三根柱子,A 上面有 n 个盘子,我们想把 A 上面的盘子移动到 C 上,但是要满足以下三个条件:
- 每次只能移动一个盘子;
- 盘子只能从柱子顶端滑出移到下一根柱子;
- 盘子只能叠在比它大的盘子上。
假设 n = 1,只有一个盘子,很简单,直接把它从 A 中拿出来,移到 C 上;
如果 n = 2 呢?这时候我们就要借助 B 了,因为小盘子必须时刻都在大盘子上面,共需要 4 步。
如果 n > 2 思路和上面是一样的,我们把 n 个盘子也看成两个部分,一部分有 1 个盘子,另一部分有 n - 1 个盘子。
当你在思考这个问题的时候,就将最初的 n 个盘子从 A 移到 C 的问题,转化成了将 n - 1 个盘子从 A 移到 C 的问题, 依次类推,直至转化成 1 个盘子的问题时,问题也就解决了。这就是分治的思想。
时间复杂度:O(2^n-1)。空间复杂度:O(1)。
3.1.2 该题的伪代码
定义三个容器ABC,分别当ABC柱;
move(int n, vector<int>& A, vector<int>& B, vector<int>& C)
{
if n = 1 直接把盘子从 A 移到 C;
if n > 1
move(n - 1, A, C, B); 将A上面n - 1个通过C移到B
A柱元素出栈并压入C栈中;
move(n - 1, B, A, C);将B上面n-1个通过空的A移到C
}
3.1.3 运行结果
网上题解给的答案不一定能跑,请把代码复制自己运行完成,并截图。
3.1.4分析该题目解题优势及难点。
(1)这是一道递归方法的经典题目,乍一想还挺难理清头绪的,所以要从简单的例子进入手。加入只有一个方块的时候可以直接移动,加入有两个方块需要借用B柱暂存上方方块,将最底层方块进行移动,再把上方方块移动,那么,当方块书大于2时,可以将整个方块组看成底层方块与上方方块两个部分。使用递归解决问题,思路清晰,代码少。但是在主流高级语言中(如C语言、Pascal语言等)使用递归算法要耗用更多的栈空间,所以在堆栈尺寸受限制时,应避免采用。所有的递归算法都可以改写成与之等价的非递归算法。
(2)自己在刚开始解决递归问题的时候,总是会去纠结这一层函数做了什么,它调用自身后的下一层函数又做了什么…然后就会觉得实现一个递归解法十分复杂,根本就无从下手。既然递归是一个反复调用自身的过程,这就说明它每一级的功能都是一样的,因此我们只需要关注一级递归的解决过程即可。所以,总结一下,解决递归问题的三步走:
a.找整个递归的终止条件:递归应该在什么时候结束?
b.找返回值:应该给上一级返回什么信息?
c.本级递归应该做什么:在这一级递归中,应该完成什么任务?
3.2 题目及解题代码
可截图,或复制代码,需要用代码符号渲染。题目截图后一定要清晰。
题解代码:
vector<pair<int, int>> reconstructQueue(vector<pair<int, int>>& people) {
int len = people.size();
vector<pair<int, int>> ans;
//1. 按身高 h 从大到小排列,身高相等按 k 从小到大排序
sort(people.begin(), people.end(), cmp);
//2. 重建队列
for( int i = 0; i < len; i++ ){
ans.insert(ans.begin() + people[i].second, people[i]);
}
return ans;
}
//按身高 h 从大到小排列,身高相等按 k 从小到大排序
static bool cmp(const pair<int, int>& data1, const pair<int, int>& data2){
return data1.first != data2.first ? data1.first > data2.first : data1.second < data2.second;
}
3.1.1 该题的设计思路
- 按身高 h 从大到小排列,身高相等按 k 从小到大排序
- 重建队列
时间复杂度:O(n)。空间复杂度:O(n)。
3.1.2 该题的伪代码
vector<pair<int, int>> reconstructQueue(vector<pair<int, int>>& people) {
设整型变量len = people.size();
vector<pair<int, int>> ans;
按身高 h 从大到小排列,身高相等按 k 从小到大排序 sort(people.begin(), people.end(), cmp);
for i = 0 to i<len
{
ans.insert(ans.begin() + people[i].second, people[i]);//完成重构队列;
}
return ans;
}
3.1.3 运行结果
3.1.4分析该题目解题优势及难点。
函数中涉及到的c++知识
(1)pair<int, int> 可以理解为包含两个元素的结构体
(2)vector<pair<int, int>> 是个长度可变的结构体数组,c++里面称为容器
(3)ret_func_type func(vector<pair<int, int>>& name) 中的name是vector<pair<int, int>>容器的引用,可以理解为传入一个指针
(4)sort(g.begin(), g.end(), cmp) 对容器g的结构体按照cmp的排序规则排序,容器的起始数据的指针是 g.begin(),容器的末尾数据的指针是g.end()