数据结构---栈和队列
- 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,双端队列
双端队列也是一种操作受限的线性表。它允许从两端插入,两端删除。
特殊的双端队列有:
- 输入受限的双端队列:允许从一端输入,两端删除的线性表。
- 输出受限的双端队列:允许从两端插入,一段删除的线性表。
可以根据栈或双端队列性质判断输出序列的合法性。并且栈中合法的序列,双端队列内一定也合法。