DS博客作业02--栈和队列

0. PTA得分截图

1. 本周学习总结

1.1 总结栈和队列内容

1.1.1 栈

  • 栈的定义

栈是一种只能在一端进行插入或删除的线性表,主要特点是先进后出

栈的几个概念

允许进行插入、删除操作的一端称为栈顶
表的另一端称为栈底
当栈中没有数据元素时,称为空栈
栈的插入操作通常称为进栈入栈
栈的删除操作通常称为退栈出栈

  • 顺序栈

假设栈的元素个数最大不超过正整数MaxSize,所有的元素都具有同一数据类型ElemType,则可用下列方式来定义顺序栈类型SqStack:

typedef struct 
{      ElemType data[MaxSize]; 
        int top;//栈顶指针
}  SqStack;

假设MaxSize=5

top指向栈顶元素,初始值为-1

当top=MaxSize-1时不能再进栈——即栈满

进栈时top+1,出栈时top-1

顺序栈4要素

栈空条件:top=-1

栈满条件:top=MaxSize-1

e进栈操作:top++;将e放在top处;

e出栈操作:从top处取出元素e;top--;栈内元素没有被删除

  • 顺序栈的基本运算算法

初始化栈

  • 建立一个新的空栈s,实际上是将栈顶指针指向-1即可

具体代码示例

void InitStack(SqStack &s)
{  
    s=new Stack;  
    s->top=-1;
} 

销毁栈

  • 释放栈s占用的存储空间

具体代码示例

void DestroyStack(SqStack &s)
{
  delete s;
}

判断栈是否为空

  • 栈为空的条件是s->top=-1

具体代码示例

bool StackEmpty(SqStack s)
{
  return(s->top==-1);
}

进栈

  • 在栈不满的条件下,先将栈指针+1,然后在该位置上插入元素e 一定要考虑栈满的情况!

具体代码示例

bool Push(SqStack &s,ElemType e)
{
  if (s->top==MaxSize-1) //栈满的情况,即栈上溢出 
	return false;
   s->top++; //栈顶指针增1
   s->data[s->top]=e; //元素e放在栈顶指针的地方	  
   return true;
}

出栈

  • 在栈不空的情况下,先将栈顶元素赋给e,然后将栈顶指针-1

具体代码示例

bool Pop(SqStack &s,ElemType &e)
{
   if (s->top==-1) //栈为空的情况,即栈下溢出
	return false;
   e=s->data[s->top]; //取栈顶指针元素     
   s->top--; //栈顶指针减1
   return true;
}

取栈顶元素

  • 在栈不空的情况下,将栈顶元素赋给e

具体代码示例

bool GetTop(SqStack s,ElemType &e)
{	
   if (s->top==-1) //栈为空的情况,即栈下溢出 	   
            return false;
      e=s->data[s->top]; //取栈顶指针元素的元素
      return true;
}
  • 共享栈

如果需要用到两个相同类型的栈,可以用一个数组data[0..MaxSize-1来实现这两个栈,这称为共享栈。

  • 链栈

链栈中数据节点的类型LiStack定义如下:

typedef int ElemType;
typedef struct linknode
{  ElemType data; //数据域
   struct linknode *next; //指针域
} LiNode,*LiStack;

链栈4要素

栈空条件:s->next=NULL

栈满条件:不考虑

进栈e操作:结点插入到头结点后,链表头插法

退栈操作:取出头结点之后结点的元素并删除

  • 链栈的基本运算算法

初始化栈

void InitStack(LiStack &s)
{  
   s=new LiNode;
   s->next=NULL;
}

销毁栈

  • 释放栈s占用的全部存储空间,这里栈内元素是被删除的了
void DestroyStack(LiStack &s)
{ 
   LiStack p;
   while (s!=NULL)
   {	  
       p=s; 	
       s=s->next;
       free(p); 
   }
}

取栈顶元素

  • 同样要在栈不为空的条件下操作
bool GetTop(LiStack s,ElemType &e)
{  
   if (s->next==NULL)	//栈空的情况
	return false;
   e=s->next->data;
   return true;
}

判断栈是否为空

  • 栈为空的条件是s->next==NULL
bool StackEmpty(LiStack s)
{
   return(s->next==NULL);
}

进栈

  • 链栈与顺序栈不同,不需要考虑栈满的情况
void Push(LiStack &s,ElemType e)
{  
   LiStack p;
   p=new LiNode;
   p->data=e; //新建元素e对应的节点p
   p->next=s->next; //插入p节点作为开始节点
   s->next=p;
}

出栈

  • 与顺序栈相同,也要考虑栈空的情况,不过链栈是将栈内数据物理删除,而顺序栈并没有删除栈内元素
bool Pop(LiStack &s,ElemType &e)
{  
   LiStack p;
   if (s->next==NULL) //栈空的情况
	return false;
   p=s->next;
   e=p->data;
   s->next=p->next;
   free(p); //释放p节点
   return true;
}
  • c++模板类:stack

#include<stack> 头文件

1.stacks:初始化栈,参数表示元素类型

2.s.push(t):入栈元素t

3.s.top():返回栈顶元素

4.s.pop():出栈操作只是删除栈顶元素,并不返回该元素 物理删除,数据在栈内不存在

5.s1.empty():当栈空时,返回true

6.s1.size():访问栈中的元素个数

  • 栈的应用

1.输入之后逆序输出

2.语法检查:括号匹配

每当扫描到大中小的括号后,令其进栈,当扫描到右括号时,则检查栈顶是否为相应的左括号,若是,则出栈处理,若不是,则出现了语法错误。当扫描到文件结尾,若栈为空则表明没有发现括号配对错误。

3.数制转换

十进制转八进制。例如(1348)十进制= (2504)八进制,N不断的除8,每次的余数就是结果的其中一个因子,注意先出来的因子是低位的数,可以考虑用栈来保存每次取余的结果,那么出栈的顺序就是实际的结果顺序。

N N/8 N%8
1348 168 4
168 21 0
21 2 5
2 0 2

4.中缀和后缀表达式的转换及计算

1.中缀表达式转换成后缀表达式的转化思路

从头到尾扫描中缀表达式,若遇到数字则直接写入后缀表达式,若遇到运算符,则比较栈顶元素和该运算符的优先级,当该运算符的优先级大于栈顶元素的时候,表明该运算符的后一个运算对象还没有进入后缀表达式,应该把该运算符暂存于运算符栈中,然后把它的后一个运算对象写入到后缀表达式中,再令其出栈并写入后缀表达式中;若遇到的运算符优先级小于等于栈顶元素的优先级,表明栈顶运算符的两个运算对象已经被写入后缀表达式,应将栈顶元素出栈并写入后缀表达式,对于新的栈顶元素仍进行比较和处理,直到栈顶元素的优先级小于当前等待处理的运算符的优先级为止,然后令该运算符进栈即可。

对于左括号直接进栈,右括号则使左右两个括号内的运算符都出栈

2.后缀表达式求值

后缀表达式求值也需要一个栈,其元素类型为操作数的类型,此栈存储后缀表达式中的操作数、计算过程的中间结果及最后结果。

计算过程:扫描后缀表达式,若遇到操作数则进栈,若遇到操作符则弹出两个操作数进行计算,然后将结果压进栈,直到最后扫描完毕,栈中应该保存着最终结果。

1.1.2 队列

  • 队列的定义

队列只能选取一个端点进行插入操作,另一个端点进行删除操作
队列的主要特点是先进先出

队列的几个概念

把进行插入的一端称做队尾(rear)
进行删除的一端称做队首或队头(front)
向队列中插入新元素称为进队或入队,新元素进队后就成为新的队尾元素
从队列中删除元素称为出队或离队,元素出队后,其后继元素就成为队首元素

  • 顺序队

定义顺序队:

typedef struct 
{     
    ElemType data[MaxSize]; 
    int front,rear;//队首和队尾指针
}SqQueue;

因为队列的两端都在变化,所以需要两个指针来表示队列的状态

假设MaxSize=5

rear总是指向队尾元素

元素进队时,rear+1

front指向当前队中队头元素的前一个位置

元素出队时,front+1

当rear=MaxSize-1时不能再进队

  • 队列的基本运算

初始化队列

  • 构造一个空队列q,将front和rear指针均设置成初始状态即-1
void InitQueue(SqQueue *&q)
{   
    q=new SqQueue;
    q->front=q->rear=-1;
}

销毁队列

void DestroyQueue(SqQueue *&q)
{
    free(q);
}

判断队列是否为空

  • 当q->front==q->rear时队列为空
bool QueueEmpty(SqQueue *q)
{
       return(q->front==q->rear);
}

进队列

  • 在队列不满的条件下,先将队尾指针rear循环增1,然后将元素添加到该位置
bool enQueue(SqQueue *&q,ElemType e)
{        
    if (q->rear==MaxSize-1)	//队满上溢出
        return false;
    q->rear++;
    q->data[q->rear]=e;
    return true;
}

出队列

  • 在队列q不为空的条件下,将队首指针front循环增1,并将该位置的元素值赋给e
bool deQueue(SqQueue *&q,ElemType &e)
{      
    if (q->front==q->rear) //队空下溢出
	return false;
    q->front++;
    e=q->data[q->front];
    return true;
}
  • 循环队列

当采用rear==MaxSize-1作为队满条件时,当其为真,队中可能还有若干空位置,这种溢出并不是真正的溢出,称为假溢出。

解决假溢出的问题,就需要把数组的前端和后端连接起来,形成一个环形的顺序表,即把存储队列元素的表从逻辑上看成一个环,称为环形队列或循环队列。

for example

环形队列4要素

队空条件:front==rear

队满条件:(rear+1)%MaxSize==front

e进队操作:rear=(rear+1)%MaxSize //将e放在rear处

e出队操作:front=(front+1)%MaxSize //取出front处的元素e

已知front、rear,求count?
count=(rear-front+MaxSize)%MaxSize
已知front、count,求rear?
rear=(front+count)%MaxSize
已知rear、count,求front?
front=(rear-count+MaxSize)%MaxSize

  • 链队列

优点
1.相比普通的队列,元素出队时无需移动大量元素,只需移动头指针。
2.可动态分配空间,不需要预先分配大量存储空间。
3.适合处理用户排队等待的情况。

入队

Status EnQueue(LinkQueue *Q, ElemType e) 
{
    // 给新节点分配空间
    QueuePtr s =new QNode;

    // 分配空间失败,结束程序
    if (!s) 
    {
        exit(OVERFLOW);
    }

    s->data = e; // 将值赋值给新节点
    s->next = NULL; // 新节点指向NULL
    Q->rear->next = s; // 队尾指针的下一个元素指向新节点
    Q->rear = s; // 队尾指针指向新节点(新节点成为队尾指针的指向的节点)
    return OK;
}

出队

Status DeQueue(LinkQueue *Q, ElemType *e) 
{
    QueuePtr p; // 用于指向被删除节点

    // 队列为空,出队失败
    if (Q->front == Q->rear) 
    {
        return ERROR;
    }

    p = Q->front->next; // p指向队列的第一个元素
    *e = p->data; // 将队列头节点的值赋值给元素e
    Q->front->next = p->next; // 头指针的下一个节点指向下下个节点(跳过头节点)

    // 如果被删除节点是队尾指针指向的节点(删除后队列为空)
    if (Q->rear == p) 
    {
        Q->rear = Q->front; // 队尾指针指向队头指针
    }
    free(p); // 释放队头节点
    return OK;
}
  • c++容器:queue

#include<queue>

q1.push(x):将x接到队列的末端

q1.pop():弹出队列的第一个元素 并不会返回被弹出元素的值,要先取队头再pop

q1.front():即最先被压入栈中的元素

q1.back():即最后被压入栈中的元素

q1.empty():当队列为空时返回true

q1.size():访问队列中的元素个数

  • 队列的应用

1.输入之后正序输出

2.迷宫寻找最短路径

因为队列是深度优先搜索,可以比栈更快的找出最短路径

3.排队问题

比如售票机取票先排到的人就先取票 or 银行三兄弟经典问题 😄

1.1.3 栈与队列的异同

相同点

  • 都是线性结构
  • 插入操作都是在表尾进行
  • 插入和删除的时间复杂度都是O(1),在空间复杂度上也相同
  • 都可以通过顺序结构和链表实现

不同点

  • 删除数据元素的位置不同,栈在表尾进行,队列在表头进行
  • 顺序栈能够实现多栈空间共享,而顺序队列不能
  • 应用场景不同;常见栈的应用场景包括括号问题的求解,表达式的转换和求值,函数调用和递归实现,深度优先搜索遍历等;常见的队列的应用场景包括银行排队和广度优先搜索遍历等

1.2 谈谈你对栈和队列的认识及学习体会

栈和队列从概念上相对比较好理解。一开始学习是通过数组的方式存储数据。接着后学习了用链表的方式存储数据。我们可以通过栈或队列,完成表达式转换,符号配对,迷宫等需要存储后在逆序或正序输出的程序功能。

目前接触到了c++容器stack(栈)和queue(队列),这两种模板很实用,但第一次使用还是容易出错:在调用模板的函数时没有加括号,如要对栈s调用pop函数,写成了s.pop,导致程序崩溃

还有就是在写程序的过程中要时刻记得判断是否为空,否则程序也会报错(自己写程序就容易忘记 害

2. PTA实验作业

2.1 报数游戏

报数游戏是这样的:有n个人围成一圈,按顺序从1到n编好号。从第一个人开始报数,报到m(m<n)的人退出圈子;下一个人从1开始报数,报到m的人退出圈子。如此下去,直到留下最后一个人。其中n是初始人数;m是游戏规定的退出位次(保证为小于n的正整数)。要求用队列结构完成。输出数字间以空格分隔,但结尾不能有多余空格。

2.1.1 代码截图

2.1.2 本题PTA提交列表说明

第一个编译错误是因为没有转换c++的编译器

第二个编译错误是因为在看代码的时候发现忘记加return 0了 结果在敲的时候把分号用中文输入法输入就错了

还有一个是在vs编译中,我一开始将编号入队的时候是用push(i),结果输出的数字都跟答案差1,然后经过调试发现应该要push(i+1)

2.2 符号配对

请编写程序检查C语言源程序中下列符号是否配对:/**/()[]{}

2.2.1 代码截图






2.2.2 本题PTA提交列表说明

我觉得这道浙大的符号配对跟7-3比起来难度提升了不少,一开始是没什么头绪,没明白题目的意思。第一个不配对的字符我一开始以为是从左往右数的第一个,就一直搞不懂怎么判断。后来是参考CSDN的代码,才知道是从中间往两边数的第一个不配对字符。

在参考代码的同时我也学会了用<>来代替/**/,这样的做法会比直接判断注释符是否配对来的简单耶

然后提交了好几次代码,最后一个测试点老是过不了,也不太理解最后一个测试点的意思。

我一开始是以为我的数组太小了,但是后来改大了之后也还是过不了。问了同学之后才知道是我没有把第一个不配对的字符及时pop出去。

3. 阅读代码

3.1 题目及解题代码

  • 题目:

  • 代码:

class Solution {
public:
    bool canMeasureWater(int x, int y, int z) {
        if (x + y < z) return false;
        if (x == 0 || y == 0) return z == 0 || x + y == z;
        return z % gcd(x, y) == 0;
    }
};

3.1.1 设计思路

  • 对于示例1的理解:

  • 思路及算法

预备知识:贝祖定理

我们认为,每次操作只会让桶里的水总量增加 x,增加 y,减少 x,或者减少 y。
因此,我们可以认为每次操作只会给水的总量带来 x 或者 y 的变化量。因此我们的目标可以改写成:找到一对整数a,b,使得ax+by=z
而只要满足z≤x+y,且这样的a,b 存在,那么我们的目标就是可以达成的。这是因为:

若a≥0,b≥0,那么显然可以达成目标。

若a<0,那么可以进行以下操作:
往 y 壶倒水;
把 y 壶的水倒入 x 壶;
如果 y 壶不为空,那么 x 壶肯定是满的,把 x 壶倒空,然后再把 y 壶的水倒入 x 壶。
重复以上操作直至某一步时 x 壶进行了 a 次倒空操作,y 壶进行了 b 次倒水操作。

若b<0,方法同上,x 与 y 互换。

而贝祖定理告诉我们,ax+by=z 有解当且仅当 z 是 x,y 的最大公约数的倍数。因此我们只需要找到 x,y 的最大公约数并判断 z 是否是它的倍数即可。

时间复杂度:O(log(min(x,y)))
空间复杂度:O(1)

3.1.2 伪代码

if(所需要z的水量大于x+y的水壶可以盛水的总量)return false;
if(x或y为0) return z==0||x+y==z;
if(z是x、y最大公约数的倍数) return true;
else return false;

3.1.3 运行结果

3.1.4 分析该题目解题优势及难点

优点
1.这题运用数学原理来解题真的超快,一开始看题目连题目都没太看懂,然后看了官方解答,震惊于怎么会有这么机智的解法
2.与单纯的c++解法相比,时间复杂度和空间复杂度都小了很多

难点
1.这个数学原理虽然真的把代码简化了非常多,但是吧,对于没有了解过这个原理的人(比如我)来说,是真的难搞,数学思想真的有一定难度

3.2 题目及解题代码

  • 题目:

  • 代码:

class Solution {
public:
    vector<vector<int>> reconstructQueue(vector<vector<int>>& people) {
        // 先排序
        // [7,0], [7,1], [6,1], [5,0], [5,2], [4,4]
        
        // 再一个一个插入。
        // [7,0]
        // [7,0], [7,1]
        // [7,0], [6,1], [7,1]
        // [5,0], [7,0], [6,1], [7,1]
        // [5,0], [7,0], [5,2], [6,1], [7,1]
        // [5,0], [7,0], [5,2], [6,1], [4,4], [7,1]
        sort(people.begin(), people.end(), [](const vector<int>& a, const vector<int>& b) {
            if (a[0] > b[0]) return true;
            if (a[0] == b[0] && a[1] < b[1]) return true;
            return false;
        });
        
        vector<vector<int>> res;
        for (auto& e : people) {
            res.insert(res.begin() + e[1], e);
        }
        return res;
    }
};

3.2.1 设计思路






时间复杂度:O(N²) 排序使用了O(NlogN) 的时间,每个人插入到输出队列中需要O(k) 的时间,其中 k 是当前输出队列的元素个数
空间复杂度:O(N) 输出队列使用的空间

3.2.2 伪代码

按高度 h 降序排列;
if(h相同)
    按 k 值的升序排列;

for(读取people)
{
    根据k找到相应插入位置插入list;
}

return 输出队列;

3.2.3 运行结果

3.2.4 分析该题目解题优势及难点

优点
1.代码运用了各种c++容器,使得代码量变得很少,理解起来也很方便
2.运用贪心算法,思路简洁明了

难点
1.题目又是难懂,一开始看题目没明白是怎么回事,说的是身高应该降序排列为什么[5,0]会在[7,0]的前面,然后看了题解用的贪心算法,粗略看一遍之后也还是没明白是怎么个算法,然后再看一遍才明白过来,题目确实有点难想
2.对于C++的容器以及函数的应用比较多

posted @ 2020-03-22 19:07  陈璧君  阅读(384)  评论(0编辑  收藏  举报