DS博客作业02

第二次DS博客作业


Q0.展示PTA总分


Q1.本章学习总结

1.1 学习内容总结

    • 什么是栈?
      同有序表、链表一样,栈也是用来存储逻辑关系为 "一对一" 数据的线性存储结构。但是栈的存储结构与它们不同,无论是存数据还是取数据,遵循“先进后出”的原则,即最先进栈的元素最后出栈(Last In First Out)。一个简单的例子,向一个栈中依次放入1、2、3,再进行出栈,则会得到结果3、2、1。
      总结来看,栈是一种只能从表的一端存取数据且遵循 "先进后出" 原则的线性存储结构。 栈分为顺序栈和链栈,即分别为数组、链表实现栈。
      对于栈,为了方便理解,我们可以将它看作是一个“桶”,我们要向桶里拿东西、放东西,都只能放在桶的顶部,也就是栈顶,而不能直接拿、放中间的某个部分。
      例如下图,我们要取元素只能取到栈顶的3,而不能直接拿到1或者2;要存元素,我们只能将其放在3的上面,而不能塞到2与3中间。

    • 栈的定义与初始化
      这里分别给出一个顺序栈和链栈的定义,后文的举例会用到它们。
      顺序栈的初始化,只需定义top的值为-1,代表栈为空
      链栈初始化只需新建一个空链表,设置头结点next结点为空即可,我们采用带头结点的单链表

typedef struct SeStack          //顺序栈
{	
    int data[MaxSize];	        //MaxSize代表栈的元素最大数量
    int top;         	        //指向栈顶
};
       
typedef struct LiStack          //链栈
{	
    int data;	                //链栈则不用担心栈的大小了
    struct LiStack* next;       //后继节点指针	        
};
  • 判断栈是否为空
    在上文初始化的基础上,我们很容易就能推出栈空的条件
    顺序栈的栈空条件即为top的值为-1,链栈的栈空条件为头结点的next结点为空(使用带头结点的链表)
bool StackEmpty1(SeStack* S)    //判断顺序栈是否为空,为空返回true,不空返回false
{
    if (S->top == -1)       //顺序栈为空的条件
       return true;
    return false;
}
      
bool StackEmpty2(LiStack* S)    //判断链栈是否为空,为空返回true,不空返回false
{
    if (S->next == NULL)      //链栈为空的条件
       return true;
    return false;
}
  • 入栈
    对于顺序栈,在入栈前我们要先判断栈是否已满,即top是否等于maxsize-1(因为我们的top从-1开始),然后再进行入栈,并且要让top值+1
    而链栈就不用考虑栈满的情况,我们直接采用头插法来实现入栈操作,这样方便我们出栈等操作
bool Push1(SeStack *S, int x)    //顺序栈入栈元素x
{
    if (S->top == MaxSize - 1)   //栈满
    {
        return false;
    }
    //注意要先修改栈顶元素的下标,再进行赋值
    S->top++;
    S->data[S->top] = x;
    return true;
}
        
bool Push2(LiStack &S, int x)    //链栈入栈
{
    LiStack *temp = new LiStack;
    temp->data = x;
    temp->next = S->next;
    S->next = temp;             //就是链表的头插法
    return true;
}
  • 出栈
    不论是顺序栈还是链栈,在出栈前都要先判断栈是否为空!
    不能忘记顺序栈要在出栈后将top的值-1,链栈的出栈要释放内存空间
bool Pop1(SeStack *S, int &e)   //删除栈顶元素,并将原栈顶的值给e
{
    if(EmptyStack1(S) == true)    //栈空,不能出栈
    {
        return false;
    }
    e = S->data[S->top];       //记录栈顶的值
    S->top--;                  //top值减1
    return true;
}
     
bool Pop2(LiStack &S, int &e)   //链栈的情况
{
    LiStack *temp = new LiStack;
    if(EmptyStack2(S) == true)    //栈空,不能出栈
    {
        return false;
    }
    temp = S->next;              //相当于删除链表头结点的后一个结点
    e = temp->data;              //记录被删除的栈顶的值
    S->next = temp->next;
    delete temp;
    return true;
}
  • 关于stack库

    • c++中的stack库可以有效、简易的帮我们做成一个栈,以下是他的一些函数
    • 定义: stack<栈中数据类型>栈的名字;
    • size():返回栈中元素个数
    • top():返回栈顶的元素
    • pop():删除栈顶元素
    • push(e):向栈中添加元素e
    • empty():栈为空时返回true(等同于栈的size()==0)
  • 栈的应用

    • 符号的配对:我们在使用VS编写程序时,当左右括号((、[、{、/*)不匹配时就会跳出错误提示,我们可以利用栈来模仿实现这一功能
      • 简单思路如下:有序读入一段字符串,遇到左括号则将它们入栈,遇到右括号则判断栈是否为空且栈顶元素是否匹配,两者条件有一不符合即不配对,且在读完所有字符后,还要判断栈中是否还有元素剩余,若有剩余也是不匹配
    • 中缀表达式转后缀表达式
      • 主要有几点需要注意:
        • 1.根据运算符的优先级考虑进栈和出栈,特别注意右括号,它会进行出栈操作直到遇到左括号(并将其左括号也出栈)
        • 2.小数:伴着数字,只需要一起输出就可以
        • 3.正负号:也有分情况,第一个情况,数是正负数时是没有括号的(如-3+1),第二种情况,算式中间的正负数(如(-1)、(+3)),且注意正数不需要输出正号
        • 4.有可能算式遍历完,栈中还有剩余符号,也要输出出来
      • 下面以2+3*(7-4)+8/4作为例子讲解
        • 1.直接输出2,因为栈空,加号直接入栈,此时栈中元素为 + ,输出结果为2
        • 2.输出3,乘号优先级大于栈顶的加号,所以入栈,左括号优先级大于栈顶的乘号,也入栈,此时栈中元素为 + * ( ,输出结果为2 3
        • 3.输出7,遇到减号,虽然优先级低于左括号,但因为此时栈顶即为左括号,所以入栈,此时栈种元素为 + * ( - ,输出结果为2 3 7
        • 4.输出4,此时遇到右括号,出栈直到栈顶为左括号,并出栈左括号,此时栈中元素为 + * ,输出结果为2 3 7 4 -
        • 5.遇到加号,此时栈顶为乘号,优先级高于加号,则出栈乘号,接下来栈顶为加号,优先级相等,出栈加号后自己入栈,此时栈中元素为 + ,输出结果为2 3 7 4 - * +
        • 6.输出8,遇到除号,除号优先级高于栈顶的加号,所以直接进栈,此时栈中元素为 + / ,输出结果为2 3 7 4 - * + 8
        • 7.输出4,字符串已全部读完,栈中还有元素剩余,将它们全部出栈,得到最终结果2 3 7 4 - * + 8 4 / +
      • 下面给出伪代码思路
	初始化一个map,例如加减号为1,乘除为2,左括号为3
	While 遍历字符串s do
	     if 运算数是正负数(s[0]即为+ -或左括号后即为+ -号) then
	        直接输出符号(+号不输出)和后面的数字,左右括号忽略跳过
	     End if
	     If s[i]为数字或.号 then
	        输出数字或点号直到s[i]不是数字
	     End if
	     If s[i]为运算符 then
	         If 栈为空或栈顶为左括号 then
	             S[i]入栈
	         Else if s[i]不为右括号 then
	            输出栈顶符号并出栈直到栈空栈顶符号优先级小于s[i]或栈顶为左括号
	             S[i]入栈
	         Else if s[i]为右括号 then
	             输出栈顶符号并出栈直到栈顶为左括号,并出栈左括号
	     End if
	End while
	如果循环结束后栈不空,依次输出栈中剩余元素
  • 队列
    • 什么是队列?
      队列和栈一样,也是一种对数据的"存"和"取"有严格要求的线性存储结构。
      与栈结构不同的是,队列的两端都"开口",要求数据只能从一端进,从另一端出。通常称进数据的一端为 "队尾",出数据的一端为 "队头"。
      队列中数据的进出要遵循 “先进先出” 的原则(First In First Out),即最先进队列的数据元素,同样要最先出队列(如入队1、2、3,再进行出队,得到结果仍是1、2、3)
      与现实中的“排队”基本一致,较易理解
    • 普通队列的问题
      对于一个普通的队列,它有一个非常致命的问题:容易造成空间浪费
      假设有一个长度为6的队列,我们先入队6个元素,再出队2个元素

      这时我想再插入1个元素,很明显队列中还有2个空间剩余,但是因为rear已经到达极限,我们不能再往队列里插入元素了
      此时我们就需要一个循环队列,来解决这个问题,我们在这里定义rear指向队列尾部的后一点
    • 为什么rear要往后定义一点?
      如果仅仅是原来的定义方法,我们又会遇到一个新问题:我们无法判断队满与队空了(因为队满和队空的条件都为front==rear)
      所以我们选择牺牲掉数组中的一个存储空间,此时判断队满的条件是:尾指针的下一个位置和头指针相遇,就说明队列满了,队空即为front==rear
    • 定义、初始化(顺序队直接以循环队列为例)
      对于顺序循环队列,我们对它的初始化需要使它的front、rear值都置为0
      对于链队,需要创建一个新链表,front为头结点,初始化read都指向头结点,头结点的next为空(采用带头结点的链表)
      初始化的代码较简单,不赘述
typedef struct SeQueue          //顺序循环队列
{	
    int data[MaxSize];	        //MaxSize代表队的元素最大数量
    int front, rear;         	//指向队首、队尾后一点
};
       
typedef struct                //链队内部
{	
    int data;	                
    struct queue* next;       //后继节点指针	        
} queue;
typedef struct LiQueue        //保存链队头尾指针
{
    queue *front;
    queue *rear;
}
  • 判断队列是否为空
    顺序循环队列的队空条件在上文已有提及,即头尾相同
    链队的队空条件仍为头结点的next结点为空(因为使用的时带头结点的链表)
bool QueueEmpty1(SeQueue* Q)    //判断顺序循环队列是否为空,为空返回true,不空返回false
{
    if (Q->front == Q->rear)       //顺序栈为空的条件
       return true;
    return false;
}
    
bool QueueEmpty2(LiQueue* Q)    //判断链队是否为空,为空返回true,不空返回false
{
    if (Q->front->next == NULL)      //链队为空的条件
       return true;
    return false;
}
  • 判断队列是否已满
    顺序循环队列的队满条件较为特殊,需要留意一下
    链队不会有队满的问题,故跳过
bool QueueFull(LiQueue* Q)        //判断顺序循环队列是否满了,为满返回true,不满返回false
{
    if ((Q->rear + 1) % MaxSize == Q->front)       //队满条件,特别注意取余
       return true;
    return false;
}

  • 入队
    同样的,对顺序循环队列首先判断队列是否为满,然后再插入,且要注意rear值不只是简单+1,略有不同
    链队采用尾插法实现入队即可

bool enQueue1(SeQueue *Q, int x)    //入队元素x
{
    if (QueueFull(Q) == true)       //队满
    {
        return false;
    }
    Q->data[Q->rear] = x;      //先入队,再更改rear
    Q->rear = (Q->rear + 1) % MaxSize;   //特别注意这里,这是为了达成循环的效果
    return true;
}
     
bool enQueue2(LiQueue &Q, int x)    //链队
{
    queue *temp = new queue;  //临时节点
    temp->data = x;
    temp->next = NULL;
    Q->rear->next = temp;       //尾插法
    Q->rear = temp;             //队尾指向新的结点
    return true;
}
  • 出队
    和栈一样,都要先判断队是否为空再进行出列
    循环队列的front变化方式需要注意,而链队要注意只有一个元素时删除的情况
bool deQueue(SeQueue *Q, int &e)   //出队,并将原队首的值给e
{
    if(EmptyQueue1(Q) == true)    //队空
    {
        return false;
    }
    e = Q->data[Q->front];       //记录队首的值
    Q->front = (Q->front + 1) % MaxSize;   //实现循环
    return true;
}
    
bool deQueue2(LiQueue &Q, int &e)   //链队,直接取
{
    queue *temp = new queue;
    if(EmptyQueue2(Q) == true)    //队空
    {
        return false;
    }
    e = Q->front->next->data;    //队首的值
    temp = Q->front->next;       //保存队首结点
    Q->front->next = temp->next;
    if (temp == Q->rear)         //队列中只有一个元素
        Q->front = Q->rear;      //队列置空
    delete temp;                 //释放结点内存
    return true;
}
  • queue库
    • push(e):向队尾插入元素e
    • pop():删除队首元素
    • size():返回队列的大小
    • empty():若队列为空返回true,否则返回false
    • front():返回队首元素
    • back():返回队尾元素
  • 队列的应用
    • 寻找最短迷宫路径 PTA7-8
      • 求解过程简单的伪代码(初始化过程跳过)
while 队列不空 do
   {  
     qu.front++;  //出队  
     读取队首坐标
     if 队首坐标即为终点坐标 then
        找到出口,输出答案
     end if
     for I=0 to 4 do  
       根据i为1、2、3、4,分别记往上下左右走后的坐标
       if 走后的坐标没有走过 then  
         将该方块坐标入队,且它的前驱为qu.front
         记录该方块走过  
       end if  
     end for  
   }

  • 图片说明
    • 用图片说明7-8中例图的部分情况,其中该图中,同一行的步数是相同的(有点像树)
    • 可以看的出来,使用队列解决迷宫问题,即使遇到岔路,它也会同时往下走 ,一边岔路走一格,一边岔路走一格这样
    • 也就可以知道,首先遇到的终点的一端,倒推回去一定就是最短的路线(例如路线A三十步到达终点,B路线五十步也能到达终点,那么路线A遇到终点的下标一定小于B)
  • 和栈走迷宫有什么区别?
    • 栈走迷宫使用的是深度优先搜索(DFS),队列走迷宫使用的是广度优先搜索(BFS)
    • 说的简单点:栈走迷宫,会一条路走到黑,走到无路可走,才会返回去找其他路
    • 而队列则是一层一层向外找可以走的点,同时在多条岔路上找路的感觉
    • 例如例题中的图,使用栈就会停在第一个路口,先在右上角打转,直到全部路都走不了了才往下走

1.2 学习体会

  • 进入了栈与队列的学习,虽然仍在线性表的范围内,但能明显的感觉到题目难度的变化,相信在接下来的章节中这个变化会更加明显
  • 发现了库函数真的很好用!不过还是要注意不能忘了原始的写法
  • 对栈和队列没有很特别的认识,拿桶和排队举例子就很容易理解了
  • 代码质量有所下降,还不能非常熟练的运用各种库函数,这点有反映在PTA题目中,是接下来要重视一下的点

Q2.PTA实验作业

2.1 表达式转换

2.1.1 代码截图

2.1.2 PTA提交列表及说明

  • 1.18分部分正确:嵌套括号、运算数前正负号答案错误,一开始在除了右括号以外的运算符,要根据运算符优先级入栈出栈时,没有考虑到栈顶元素为左括号时的状况,导致将左括号也进行了出栈操作以至于产生了错误,后来添加了对栈顶元素是否为左括号的判断
  • 2.18分部分正确:对于(-1)这种负数,没有处理好右括号,导致其会一直出栈到栈空,后来添加了在输出正负数后立马处理右括号的代码
  • 3.23分部分正确:运算数前有正负号答案错误,对正负数的判断只写了左括号后为加减号的可能性,没有考虑到如果第一个数就为正负数的话就没有括号的情况,单独增添了对第一个符号是否时正负号的判断
  • 4.23分部分正确:遇到正数将加号也一并输出了(如(+3)输出+3),询问后得知正号不需要输出,于是将该部分代码修改为只输出负数的负号

2.2 银行排队问题之单窗口“夹塞”版

2.2.1 代码截图

2.2.2 PTA提交列表及说明

  • 1.编译错误:PTA莫名其妙会自己把编译器从C++换成C
  • 2.部分正确:一开始仅前2个测试点、一名顾客测试点通过,大规模数据测试点超时
    朋友来时队首正好完成/朋友来时原来队首朋友走了的测试点未通过,起初认为这两个测试点都需要重新排队,将中间循环>改为>=后通过
  • 3.部分正确:窗口空余一段时间等待下一位测试点,一开始没有考虑这种情况,时间直接从0开始,时间只是工作时间叠加,到达时间大于等待时间就不需要等待等等错误。后来修改了一个变量nowTime用于记录当前时间,开始时为第一个顾客的到达时间,当nowTime小于顾客到达时间时就重置为顾客到达时间,顾客的等待时间即为nowTime减去到达时间,经过这些更改后2名顾客的测试点也一并通过了
  • 4.部分正确:大规模数据超时:一开始采用两层循环,时间复杂度为O(n^2),数据多就超时了,后来新添加了一个map,以顾客的名字为key记录顾客在队列中是第几位,并将其运用到代码中,将时间复杂度减少到了O(n),将上条问题解决后一并通过了该测试点,通过了本题
  • 5.其他问题:没有用上queue库,做法写作队列读作数组……代码还可以简化

Q3.阅读代码

3.1 栈排序

3.1.1 设计思路

本题设计思路较简单,需要保持栈的栈顶元素为栈中最小的元素
假设已有一个符合要求的栈(1,3)现在想要插入数据2
发现2大于栈顶元素1-->建立辅助栈,1入辅助栈,出主栈-->2小于栈顶元素3,2进入辅助栈-->将辅助栈中元素再进入主栈,即得到答案
图示如下

时间复杂度为O(n),空间复杂度为O(n)

3.1.2 伪代码

        //本题其他函数代码较简单,这里就只放入栈的代码了
        void push(int val) {
        if 主栈非空且主栈栈顶元素小于插入元素 then
            while 主栈非空且主栈栈顶元素小于插入元素 do
                记录主栈栈顶元素
                主栈出栈、辅助栈入栈
            end while
            将插入元素也放入辅助栈
            while 辅助栈非空 do
                记录辅助栈栈顶元素
                辅助栈出栈、主栈入栈
            end while
        else 栈顶元素大于插入元素
            插入元素直接入栈
        end if
    }

3.1.3 运行结果

3.1.4 分析

  • 本题要求只能用额外的一个辅助栈来达到目的,可能是本题要求稍高一点的地方,但也没有特别困难
  • 巧妙运用两个栈来回入栈、出栈的特性,可以使原来正向排列的栈变成反向
  • 在题解中还有看到使用vector库函数的做法,但是它运用到了sort函数,导致时间复杂度上不如该做法

3.2 最大频率栈

3.2.1 设计思路

本题需改变出栈操作,移除栈中出现最频繁的数,若多个数字出现次数相同,则移出离栈顶最近的元素
假设有栈(5,7,5,7,4,5,从左到右为栈底到栈顶)
第一次pop:5出现次数最多,移除5,此时栈(5,7,5,7,4)
第二次pop:5、7出现次数一样多,但7离栈顶近,移出7,此时栈(5,7,5,4)
第三次pop:5出现次数最多,移除5,此时栈(5,7,4)
第四次pop:5、7、4出现次数一样多,4为栈顶,即移除4
第五、六次pop移除7、5

该题解使用了两个map,一个map是数值对应出现次数,一个map是出现次数对应栈,对题解代码的说明在下面
时间复杂度为O(1),空间复杂度为O(n)

3.2.2 伪代码

        void push(int x) {
        数字x出现次数+1
        在最大出现次数和数字x的出现次数中取最大
        向第 (x的出现次数) 个栈入栈元素x
    }
    
        int pop() {
        取x为第 (最大出现次数) 个栈的栈顶元素
        对应栈出栈
        数字x的出现次数减1
        if(对应出现次数的栈已空) 最大出现次数减1;
        return x;
    }

3.2.3 运行结果

3.2.4 分析

  • 本题的第二个map我觉得是使用的最巧妙的,非常完美的空间换时间做法
  • 首先是第一个map,freq,单纯的记录某个数字的出现次数,很正常
  • 最难也最巧妙的是第二个map,group,其映射值是一个栈,它将所有出现次数相同的数字又放到一个栈中,完美解决了“出现次数相同时,输出离栈最近的元素”的要求
  • 举个栗子,有数字1,2,1,2,3入栈(栈底->栈顶),其中1和2的出现次数一样多,但是因为第二个1更早出现,所以1先进入了group[2]的栈
    之后又遇到了第二个2,2在1之后进入了group[2]的栈,所以group[2]的栈从栈顶到栈底存着2、1
    此时出栈,就得到2,出现次数为2的栈还剩下1,跟着后出栈,此时已有答案2 1
    这时出现次数为2的栈已空,最大出现次数减1得到1
    出现次数为1的栈按照栈底到栈顶的排序为1、2、3,所以接着输出为3、2、1
    所以得到最终答案2 1 3 2 1
  • 本题题解作者对map函数与栈的结合使用十分精彩,我认为是这篇题解代码最优秀的地方
原文地址:https://www.cnblogs.com/silverash/p/12548578.html