数据结构---栈和队列

news/2024/11/24 19:15:15/

数据结构---栈和队列

    • 1,栈是什么?
    • 2,栈的相关术语
    • 3,栈的基本操作
    • 4,栈的出栈顺序问题
    • 5,栈的顺序存储
      • 5.1,顺序栈的初始化和判空操作
      • 5.2,顺序栈的进栈操作
      • 5.3,顺序栈的出栈操作
      • 5.3,顺序栈读取栈顶元素
      • 5.4,另一种设计方式
      • 5.5,共享栈
    • 6,栈的链式存储
    • 7,队列是什么?
    • 8,队列的相关术语
    • 10,队列的基本操作
    • 11,队列的顺序存储
      • 11.1,顺序队列的初始化和判空操作
      • 11.2,顺序队列的入队和出队操作
      • 11.3,顺序队列获取队头元素的值
      • 11.4,计算队列元素个数
      • 11.5,另一种实现方式
    • 12,队列的链式存储
      • 12.1,链式队列的初始化和判空操作
      • 12.2,链式队列的入队和出队操作
    • 13,双端队列

1,栈是什么?

上节介绍了线性表。栈(Stack)也是一种操作受限的线性表。

只允许在一端进行插入和删除操作线性表即为栈。

栈类似理解为盘子或烤串。取放盘子时只能在一摞盘子顶部进行取放。烤肉时肯定是从签子的顶部串肉,吃串的时候也是从顶部开吃。
在这里插入图片描述


2,栈的相关术语

  • 空栈:即栈里没有存任何元素;
  • 栈顶:允许插入的一端;
  • 栈底:不允许插入和删除的一端;

在这里插入图片描述


3,栈的基本操作

  • InitStack(&S):初始化栈。构造一个空栈S,分配内存空间;
  • DestoryStack(&S):销毁栈。销毁并释放栈S所占用的内存空间;
  • Push(&S,x):进栈,若栈S未满,则将x加入使之成为新栈顶;
  • Pop(&S,&x):若栈S非空,则弹出栈顶元素,并用x返回;(删除);
  • GetTop(S,&x):读栈顶元素。若栈S非空,则用x返回栈顶元素。(只读取不删除);
  • StackEmpty(S):判断一个栈S是否为空。若S为空,则返回true,否则返回false;

4,栈的出栈顺序问题

进栈顺序:a->b->c->d->e。
有哪些合法的出栈顺序?

最简单的情况是依次进栈,所有元素都进栈之后,进行出栈操作,此时出栈的顺序即为:e,d,c,b,a

如果是进栈出栈穿插进行,会有很多种情况。如ab先入栈,然后执行一次出栈操作,继续使cde入栈,然后执行出栈操作:顺序为:b,e,d,c,a

在这里插入图片描述


5,栈的顺序存储

顺序栈使用顺序存储方式实现,与顺序表类似。

顺序栈的定义(代码实现):

#define MaxSize 10           //定义栈中元素最大个数
typedef int ElemType;        //此处代码示例使用int类型
typedef struct{  ElemType data [MaxSize];  //使用静态数组存放栈中元素int top;               //栈顶指针,一般指向栈顶元素
}SqStack;void testStack(){SqStack S;         //声明一个顺序栈//***后续操作***
}

上述栈的顺序实现方式,给各个数据元素分配连续的存储空间。大小为:MaxSize*Sizeof(ElemType)


5.1,顺序栈的初始化和判空操作

初始化空栈时,一般将栈顶指针赋值为-1,判空同理。

//初始化栈
void InitStack(SqStack &S){S.top=-1;    //初始化栈顶指针
}//判断栈空
bool StackEmpty(SqStack S){if(S.top==-1){  //栈空return true;}else{      //不空return false;}
}

5.2,顺序栈的进栈操作

进栈操作相当于增删改查中的增,顺序栈进栈操作的基本思路如下:

  • 判断栈是否满,栈满则报错;
  • 栈顶指针加1;
  • 新元素入栈;
  • 返回true;

代码实现如下:

//新元素入栈(注意操作顺序)
bool Push(SqStack &S,ElemType x){if(S.top==MaxSize-1){  //栈满return false;}S.top=S.top+1;   //指针先加1S.data[S.top]=x;  //新元素入栈return true;
}

指针加1和新元素入栈操作也可以简化为一步:

S.data[++S.top]=x;  //运行顺序为:先++,再赋值

5.3,顺序栈的出栈操作

出栈操作相当于增删改查中的删,顺序栈出栈操作的基本思路如下:

  • 先判断栈是否为空,栈空则报错;
  • 栈顶元素出栈;
  • 栈顶指针减1;
  • 返回true;

代码实现如下:

//出栈
bool Pop(SqStack &S,ElemType &x){if(S.top==-1){    //栈空报错return false;}x=S.data[S.top];  //栈顶元素先出栈S.top=S.top-1;    //指针再减一return true;
}

新元素出栈和指针减一操作也可以简化为一步:

x=S.data[S.top--]; //运行顺序为:先赋值,再--

注意:出栈之后数据还残留在内存中,只是逻辑上被删除了。


5.3,顺序栈读取栈顶元素

读取栈顶元素和出栈操作的区别在于,出栈会删除元素,读取栈顶元素不会,因此读取栈顶元素代码实现如下:

//读取栈顶元素
bool GetTop(SqStack S,ElemType &x){if(S.top==-1){    //栈空,报错return false;}   x=S.data[S.top];  //x记录栈顶元素return true;
}

5.4,另一种设计方式

顺序栈的栈顶指针初始化时也可以指向0,即top指针可以指向下一个可以插入元素的位置,这种情况下,进栈操作和出栈操作也会有所变化,如下:

S.data[S.top++]=x;   //此为进栈操作。先赋值,再自增
x=S.data[--S.top];   //此即为出栈操作。先自减,再赋值

5.5,共享栈

由于使用了静态数组,不难发现顺序栈的一个缺点:栈的大小不可变。

如何解决顺序栈的大小不可变的问题呢?

  • 方案一:使用栈的链式存储方式实现;
  • 方案二:一开始就给顺序栈分配大量存储空间,这必然导致内存的浪费,因此可以考虑使用共享栈,提高内存空间的使用率。

共享栈的特点:

  • 逻辑上是两个栈,物理上二者共享同一片空间;
  • 设置两个栈顶指针,一个赋值为-1,一个赋值为MaxSize;
  • 放入数据元素时,两个栈一个往上,一个往下,两个栈都往中间增长;

共享栈的定义和初始化:

//共享栈的定义
typedef struct{ElemType data[MaxSize];int top0;int top1;
}ShStack;void InitStack(ShStack &S){S.top0=-1;   //初始化0号栈顶指针S.top1=MaxSize;   //初始化1号栈顶指针}

根据共享栈结构,共享栈栈满的条件为 top0+1==top1;如下图即为共享栈栈满的情形:
在这里插入图片描述


知识结构梳理:
在这里插入图片描述


6,栈的链式存储

回顾头插法建立单链表代码

//头插法建立单链表(带头结点)
LinkList List_HeadInsert(LinkList &L){//①初始化一个空的单链表L=(LinkList)malloc(sizeof(LNode));L->next=NULL;//②输入结点的值int x;scanf("%d",x);while(x!=9999){   //③对头结点进行后插LNode *s=(LNode *)malloc(sizeof(LNode));s->data=x;s->next=L->next;L->next=s;scanf("%d",&x);    //继续输入下一个待插入值}return L;
}

上述头插法建立单链表即每次都对头结点进行后插操作,建立起一个单链表。而此操作正好只在链头的一端进行,符合进栈操作的特性。同理,如果规定单链表只能在此端删除,此受限的单链表就和链栈等同了起来。此即为单链表的链式存储,该类型描述为以下代码。

typedef struct Linknode{ ElemType data;             //数据域struct Linknode *next;     //指针域
}*LiStack;                     //链栈类型定义

7,队列是什么?

队列也是一种操作受限的线性表。

队列(Queue):只允许在一端进行插入操作,另一端进行删除操作线性表即为栈。

可以形象理解为,排队打饭的场景、排队过收费站的场景。都是队尾加入元素,队头删除元素。
在这里插入图片描述


8,队列的相关术语

  • 队头:删除数据元素的一端;
  • 队尾:插入数据元素的一端;
  • 空队列:即队列中无任何元素;

在这里插入图片描述


10,队列的基本操作

  • InitQUeue(&Q):初始化队列,构造一个空队列Q;
  • DestroyQueue(&Q):销毁队列。销毁并释放队列Q所占用的内存空间;
  • EnQueue(&Q,x):入队,若队列Q未满,将下加入,使之成为新的队尾;
  • DeQueue(&Q,&x):出队,若队列Q非空,删除队头元素,并用x返回;
  • GetHead(Q,&x):读队头元素,若队列Q非空,则将队头元素赋值给x;
  • QueueEmpty(Q):判断队列是否为空,返回true或false;

11,队列的顺序存储

队列采用顺序存储方式时,可以使用静态数组存放队列中的元素。

队列(顺序存储方式)的定义代码实现:

#define MaxSize 10            //定义队列最大元素个数
typedef int ElemType;
typedef struct{ElemType data[MaxSize];    //用静态数组存放队列元素int front,rear;            //队头指针和队尾指针
}SqQueue;void testQueue(){SqQueue Q;       //声明一个队列//后续操作。。。
}

注意,由于队列操作受限,队头删除,队尾插入,因此需要两个指针分别指向队头和队尾。

上述队列的顺序实现方式,给各个数据元素分配连续的存储空间。大小为:MaxSize*Sizeof(ElemType)


11.1,顺序队列的初始化和判空操作

InitQUeue(&Q):初始化队列,构造一个空队列Q;
QueueEmpty(Q):判断队列是否为空,返回true或false;

一般规定,front队头指针指向队头元素,rear队尾指针指向队尾元素的后一个位置(即下一个应该插入元素的位置)。如下图所示:
在这里插入图片描述
顺序队列初始化的时候需要让队尾指针和队头指针同时指向0。同时这也是判断顺序队列是否为空的依据。 代码实现如下:

//初始化队列
void InitQueue(SqQueue &Q){//初始化时,队头队尾指针均指向0Q.front=Q.rear=0;
}//判断队列是否为空
bool QueueEmpty(SqQueue Q){if(Q.rear==Q.front){   //队空条件return true;}else{return false;}
}

11.2,顺序队列的入队和出队操作

EnQueue(&Q,x):入队,若队列Q未满,将下加入,使之成为新的队尾;
DeQueue(&Q,&x):出队,若队列Q非空,删除队头元素,并用x返回;

①入队操作,即向队尾添加元素,具体思路如下:

  • 先判断队列是否为满,若队满则报错;
  • 将新元素插入队尾;
  • 队尾指针后移;

思路很简单,但需要思考队列满的条件是什么?

首先,使用rear==MaxSize并不能表达队列存满。因为此时若有队头元素出队,队头指针会后移,静态数组出现空闲空间,如果此时有新元素入队,要能插入到队列前面空闲的位置。因此需思考新元素插入队尾后,如何让队尾指针重新指向队列中下一个空闲位置。

可以通过取余操作使队尾指针重新指向队列前面的空闲部分,代码语句是:Q.rear=(Q.rear+1)%MaxSize;

比如,假设静态数组最大容量为10,在rear为9时,插入元素,之后执行此语句使队尾指针指向下一个待插入位置,则此时 (9+1)%10=0,就指向数组下表为0的位置,使用模运算将存储空间在逻辑上变为了环状。


因此队满情形,即队尾指针的下一个位置是队头,代码语句为:(Q.rear+1)%MaxSize==Q.front; 。使用模运算将存储空间在逻辑上变为了环状,队满的图示如下:
在这里插入图片描述
需要牺牲一个存储单元,队满情形下,不可让rear和front指向同一个位置,因为rear==front是判断队列是否为空的条件。

因此顺序队列完整的入队操作的代码如下:

//入队操作
bool EnQueue(SqQueue &Q,ElemType x){if((Q.rear+1)%MaxSize==Q.front){       //队满情形return false;    //报错}Q.data[Q.rear]=x;    //将x插入队尾Q.rear=(Q.rear+1)%MaxSize;     //队尾指针后移指向下一个待插入位置return true;
}

②队列的出队操作即从队头删除元素并用变量x返回其值,具体思路如下:

  • 先判断队列是否为空,队空则报错;
  • 不空,则将队头指针指向的数据元素赋值给变量x;
  • front指针后移一位;

出队操作代码实现如下:

//出队操作
bool DeQueue(SqQueue &Q,ElemType &x){if(Q.rear==Q.front){return false;   //判断队空则报错}x=Q.data[Q.front];Q.front=(Q.front+1)%MaxSize;  //队头指针后移(转圈移动)return true;
}

11.3,顺序队列获取队头元素的值

GetHead(Q,&x):读队头元素,若队列Q非空,则将队头元素赋值给x;

代码实现如下:

//获取队头元素的值
bool GetHead(SqQueue Q,ElemType &x){if(Q.rear==Q.front){  //队空则报错return false;}x=Q.data[Q.front];return true;
}

11.4,计算队列元素个数

通过以上学习,了解到队满和队空的条件为:

  • 队满:(Q.rear+1)%MaxSize==Q.front;
  • 队空:Q.front==Q.rear;

根据队头指针和队尾指针的值,我们可以很方便地计算出队列内元素的个数:

  • 队列内元素个数为:(rear+MaxSize-front)%MaxSize 个;

在这里插入图片描述
上图例子中,rear为2,front为3,则当前队列内元素个数为(2+10-3)%10 = 9个。


11.5,另一种实现方式

在上述队列的顺序实现方式,我们牺牲了一个存储空间来区别队满和队空两种状态。但有些算法题中可能会要求不准浪费这一个单位的存储空间。那我们应当如何设计来保证队列的性能呢?

方案一:在队列结构内定义变量size,记录队列此时存放的数据元素个数。

代码定义如下:

//另一种实现方式
#define MaxSize 10
typedef struct{ElemType data[MaxSize];int front,rear;int size;   //记录队列当前长度
}SqQueue;
  • 初始化时,rear=front=0; size=0;
  • 插入成功size++;
  • 删除成功size–;

虽然此时队满和队空时,队头指针和队尾指针都是指向同一个位置,但可通过size的值进行判断队满还是队空

  • 队满条件:size==MaxSize;
  • 队空条件:size==0;

方案二:在队列结构内定义变量tag,用来标识最近进行的是插入操作还是删除操作(因为只有删除操作才会导致队空,只有插入操作才会导致队满)

代码定义如下:

//方案二
#define MaxSize 10
typedef struct{ElemType data[MaxSize];int front,rear;int tag;  //标识位,最近执行的是删除操作tag=0,插入操作tag=1;
}SqQueue;
  • 初始化时,rear=front=0; tag=0;
  • 插入成功,tag设置为1;
  • 删除成功,tag设置为0;

此时,可以搭配tag值判断队满还是队空:

  • 队满: front==rear且tag为1;
  • 队空:front==rear且tag为0;

12,队列的链式存储

链式队列和普通的单链表相比,无非是链式队列额外要求元素删除只能在队头,元素插入只能在队尾。

队列链式存储代码声明如下:

//定义链式队列内的结点
typedef struct LinkNode{ElemType data;struct LinkNode *next;
}LinkNode;//定义链式队列
typedef struct{LinkNode *front,*rear; //队列的头指针和尾指针/*如果开发场景内经常用到队列长度,此处可以补充声明一个int型的length变量*/
}LinkQueue;

在这里插入图片描述


12.1,链式队列的初始化和判空操作

链式队列分为带头结点和不带头结点。初始化时,如下图所示:

在这里插入图片描述
在这里插入图片描述

链式队列的代码实现

①链式队列的初始化和判空操作(带头结点)

//初始化链式队列(带头结点)
void InitQueue(LinkQueue &Q){//初始化时front和rear都指向头结点Q.front=Q.rear=(LinkNode *)malloc(sizeof(LinkNode));Q.front->next=NULL;
}//判空操作(带头结点)
bool IsEmpty(LinkQueue Q){if(Q.front==Q.rear){return true;}else{return false;}
}

②链式队列的初始化和判空操作(不带头结点)

//初始化链式队列(不带头结点)
void InitQueue(LinkQueue &Q){//初始化时front和rear都指向NULLQ.front=NULL;Q.rear=NULL;
}//判空操作(不带头结点)
bool IsEmpty(LinkQueue Q){if(Q.front==NULL){return true;}else{return false;}
}

12.2,链式队列的入队和出队操作

EnQueue(&Q,x):入队,若队列Q未满,将下加入,使之成为新的队尾;
DeQueue(&Q,&x):出队,若队列Q非空,删除队头元素,并用x返回;

①链式队列入队操作的基本思想:

  • 使用malloc函数申请新结点;
  • 数据域赋值;
  • 新结点插入到rear之后;
  • 修改表尾指针;

代码实现如下:

//新元素入队(带头结点)
void EnQueue(LinkQueue &Q,ElemType x){LinkNode *s=(LinkNode *)malloc(sizeof(LinkNode));s->data=x;s->next=NULL;Q.rear->next=s; //新结点插入到rear之后Q.rear=s;       //修改表尾指针
}
//新元素入队(不带头结点)
void EnQueue(LinkQueue &Q,ElemType x){//申请新结点LinkNode *s=(LinkNode *)malloc(sizeof(LinkNode));s->data=x;s->next=NULL;if(Q.front==NULL){   //不带头结点情况下,空队列中插入第一个元素需要特殊处理Q.front=s;Q.rear=s;}else{Q.rear->next=s;   //新结点插入到rear结点之后Q.rear=s;         //修改rear指针}
}

②链式队列的出队操作基本思想:

  • 队列若为空,出队失败,返回false;
  • 声明一指针p,指向待删除结点;
  • 使用变量x,将删除的结点的数据域返回;
  • 修改头结点的后向指针;
  • 如果删除的是队列内最后一个结点,需要特殊处理;
  • 释放指针p

代码实现如下:

//队头元素出队(带头结点)
bool DeQueue(LinkQueue &Q,ElemType &x){if(Q.front==Q.rear){  //空队return false;}LinkNode *p=Q.front->next;x=p->data;     //使用x返回队头元素Q.front->next=p->next;  //修改头结点next指针if(Q.rear==p){  //最后一个出队的元素,需修改rear指针Q.rear=Q.front;}free(p);return true;
}
//队头元素出队(不带头结点)
bool DeQueue(LinkQueue &Q,ElemType &x){if(Q.front==NULL){  //空队return false;}LinkNode *p=Q.front;x=p->data;     //使用x返回队头元素Q.front=p->next;  //修改front指针if(Q.rear==p){  //最后一个出队的元素,需修改rear指针Q.rear=NULL;Q.front=NULL;}free(p);return true;
}

注:链式存储队列一般不会存满,除非内存不足。


13,双端队列

双端队列也是一种操作受限的线性表。它允许从两端插入,两端删除。

在这里插入图片描述
特殊的双端队列有:

  • 输入受限的双端队列:允许从一端输入,两端删除的线性表。
  • 输出受限的双端队列:允许从两端插入,一段删除的线性表。
    在这里插入图片描述
    可以根据栈或双端队列性质判断输出序列的合法性。并且栈中合法的序列,双端队列内一定也合法。

http://www.ppmy.cn/news/819434.html

相关文章

高德地图通过图层layer实现对海量点的可视化渲染

一、可视化海量点应用场景 在正文开始之前我先说说我为啥会使用这个技术来实现数据的可视化。 事情是这样的,我接手了一个项目,里面有个需求是在地图上标记出他们公司的产品的使用分布。我接手的时候呢,我前面的那位大哥是使用marker点覆盖物…

猎头推荐转行大数据分析师骗局

今天中午,朋友问我的工作,然后就闲聊了几句,唠了一些最近怎么样啊之类的话,最后才进入今天这篇博客的主题,大数据分析师骗局。 我朋友是土木工程毕业的,毕业了大概四年左右,由于工作不太稳定&am…

猎头职场:合格的职场人

猎头职场:合格的职场人,作为职场新人,在进入职场时,要做的第一件事就是要让自己尽快成为一名合格的“职场人”。而要成为一名合格的职场人,你就需要了解职场的规则、适应职场的环境、养成良好的职场习惯。 大方待人 …

傻逼猎头

最近年前又接了几个猎头电话 这些猎头借着一个电话 套我的工作收入信息 真的很无聊 我想到那些租房中介 这些猎头跟这些就像是一群抢骨头的狗没啥区别 感觉好像我对用户隐私、社交工程这些一无所知似的 垃圾的猎头 其实就是那些恶心公司的走狗帮凶 在帮公司老板挖人才…

猎头职场:职场站稳得这样

猎头职场:职场站稳得这样,不论是什么人,如果没有一技之长傍身的话,在职场中就不能够站稳脚跟,在社会中更加不可能立足、做出自己的事业。所以,想要变得有出息的话,一定要有真才实学。 理解能力…

自从做了猎头,日子过得越来越像段子。。。

自从做了猎头,日子过得越来越像段子。。。 1.人选A,上午HR反馈技术没通过,我刚告诉人选,HR又给换了一个项目,又面试,结果又没通过,现在我都不敢给人选反馈了。。。 2.人选B,面试好…

猎头日记 每日一篇

2017.2.15 工作计划,早晨上班时候,先搜了一下猎头网,看到一个大数据架构师的人选,成功联系推荐,终于有了本周第一份推荐简历,随时邮件正文没写好,不过,以后会记得的,一定…

猎头职场:职场也有忌讳

猎头职场:职场也有忌讳,现在职场难免会得罪人,就算是人精、老油条,这一些职场上的精英也会在不经意之间就得罪领导或者是同事,但是每次他们做了之后,想明白原来有这样问题,做过一次就不再做了&a…