数据结构系列第二篇栈和队列,本文依旧会以伪代码和真实代码进行示例的编写。
为什么称栈和队列是运算受限制的线性表呢,就是因为这俩哥们和普通的线性表不太一样。那么具体不一致的点在哪呢?
我们都知道不管是顺序表还是链表都是可以在其任意位置进行插入和删除操作的(前提是在其索引范围内),插入和删除操作时栈和队列都支持的,但是不同就不同在这个任意位置上。
一、栈--后入先出表
我们先看栈的定义:栈-->是只能在表的一端(栈尾)进行【插入】和【删除】操作的线性表,其中允许允许【删除】和【插入】的一端称为栈顶(top),另一端称为栈底。
栈这家伙比较流氓,他的思想是先进入的数据反而后被操作(因为越后入,越靠近栈顶),所以叫后入先出表。举个例子,就是你医院看病,明明是最早进行排队的,反而却是最后才见到医生。
实例,比如现在有一组数 :S = (1,2,3,4,5),那么它入栈的顺序就是如下图所示这样!
聪明的同学应该已经发现了,这种思想其实和递归是一样的(将大问题不断的拆解成可执行的小问题,然后先执行小问题,最后执行大问题)
既然线性表是分为顺序表和链表的,那么栈也能这样分么?答案是肯定的,接下来我们就看伪代码实现过程。
1.1-栈的顺序表实现
1.1.1-创建结构体
int maxsize; //定义可存储的最大元素个数
typedef struct seqstack{
DataType data[maxsize];
int top; // 栈顶top
}SeqStk;
SeqStk *stk; // 实例化栈结构 s
1.1.2-判空
int EmptyStack(SeqStk *stk){
if(s->top==0){ // 判断栈空,就看栈顶这个索引是不是0,如果是0,则代表没元素
error("栈空");
return 1;
}else{
return 0;
};
};
1.1.3-进栈
int Push(SeqStk *stk,DataType x){ // x:进栈的值
if(stk->top==maxsize-1){ // 对于顺序表操作,进行插入操作时一定要校验上溢情况
error("栈满");
return 0;
}else{ // 栈未满情况
stk->top++; // top先增加,占据新栈顶位置
stk->data[stk->top] = x; // 将值赋值给新栈顶位置
return 1;
};
};
1.1.4-出栈
int Pop(SeqStk *stk){
if(stk->top == 0){ // 对于出栈,一定要校验下溢情况,不能出现栈已经空了,还要进行出栈操作
error("栈空!");
}else{
stk->top--; // 对于出栈操作,只需要将top值自减即可,相当于原top值不在栈里了
return 1;
};
};
1.1.5-获取栈顶元素
DataType GetTop(SeqStk *stk){
if(stk->top==0){ // 栈空情况就表示没有栈顶元素
return NULL;
}else{
return stk->data[stk->top]; // 直接返回top对应的元素值即可
};
};
1.2-栈的链式实现
定义:栈的链式实现-链栈是一个操作受限的单链表,受限行为表现在插入和删除只能在链表表头进行,头指针就是栈顶top指针。
1.2.1-结构体实现(单链表结构体怎么实现,链栈结构体就怎么实现)
typedef struct Node{
DataType data; // 数据域
Node *next; // 指针域
}LKstk;
1.2.2-初始化
void InitStack(LKstk *LS){
LS = (LKstk*)malloc(sizeof(LKstk)); // 申请内存空间,强不强转有没有都行,具体看场景
LS->next = NULL; // 这是一个空表,栈顶指针域指向NULL
};
1.2.3-判空
int EmptyStack(LKstk *Lstk){
if(Lstk->next == NULL){ // 链栈的判空条件就看头指针的next域有没有节点
error(“栈空”);
return 1;
}else{
return 0;
};
};
1.2.4-进栈
int Push(LKstk *Lstk,DataType x){
// 对于链栈,进栈操作不需要判断上溢(可参考链表-不向顺序表有空间限制)
temp = malloc(siazeof(LKstk)); // 创建新结点
temp->data = x; // 将值赋给新节点数据域
temp->next =Lstk->next; // 对于链表插入,一定是先搭上后面,再连接前面节点
Lstk->next = temp;
return 1;
};
1.2.5-出栈
int Pop(LKstk *Lstk){
if(Lstk->next == NULL){
error("栈空"); // 出栈校验下溢
return 0;
}else{
LKstk *temp; // 创建新结点用于存储原栈顶结点信息
temp = Lstk->next; // 存储原栈顶信息
Lstk->next = temp->next; // 将头指针指向新的栈顶(原栈顶的next域)
free(temp); // 是否原栈顶内存空间
retun 1;
};
};
1.2.6-取栈顶值
DataType GetTop(LKstk *Lstk){
if(Lstk->next == NULL){ // 判断下溢
error("空栈");
return NULLData;
}else{
return Lstk->next->data; // 直接返回头指针指向的数据
};
};
二、队列--先进先出表
队列也是运算受限的线性表,但是这家伙不像栈那样流氓,它呢比较提倡公平,提倡先进先出,就像一根水泥管子,从一端进入,再从另一端出去。
所以这就是队尾(real)进行新增(插入)数据,队头(front)进行取数据(删除)。
思考🤔:既然出现了队尾和队头的概念,那么这俩家伙有什么作用呢?
从两个问题来看:
1.空队列怎么判断?
2.满队列怎么判断?
第一个问题,对于顺序队列来说,当队尾real和队头front指向同一个位置时,就意味着该队列为空。
第二个问题,对于顺序队列来说,当队尾插入一个新数据时,real+1(自增的目的是为了让real始终指向未存放数据的新位置);当队头存在数据出队列时,front+1(front自增的目的是为了让front始终指向新的对头位置)
具体过程可参考图一图二
real和front进行的都是自增操作,但是顺序表又有一个特点就是存储空间是有限的,这就必然会带来一个如图三的问题--假溢出
为了解决假溢出问题,特引出循环队列概念(前提:还是要提前确定maxsize)
循环队列入队列出队列过程如图四所示。
1).队列为空时,real = front(指向同一位置)
2).a1、a2、a3入队列时,real不断进行自增操作(目的:real指向空闲空间位置)
3.)出队列时,front进行自增(目的:front指向新栈顶元素)
4).栈满的条件,又图可知,应该是real+1 == front,但实际我们知道这个等式是不可能成立的(毕竟4+1!=0恒成立)
所以此地用模运算更合适--使用 (real+1)%maxsize==front 来校验栈是否满
2.1-队列的顺序表实现
2.1.1-创建结构体
int maxsize;
typedef struct SeqQueue{
DataType data[maxsize];
int real,front;
}SeqQue;
SeqQue sq;
2.1.2-初始化
void creatLkQueue(SeqQue*sq){
sq->real = 0; // 默认是个空队列,real、front都指向0位置
sq->front = 0;
};
2.1.3-判空
int EmptyQueue(SeqQue *sq){
if(sq->real ==sq->front){ // Lk->real == Lk->front 表示空队列
printf("栈空");
return 1;
}else{
return 0;
};
};
2.1.4-入队列
int EnterQue(SeqQue *sq,DataType x){
if((sq->real+1)%maxsize == sq->front){
printf("队列满");
return 0;
}else{
sq->data[sq->real] = x; // 将real指向的位置存入新值
sq->real = (sq->real+1)%maxsize; // real自增,执行新地址
return 1;
};
};
2.1.5-出队列
int OutQueue(SeqQue *sq){
if(sq->real == sq->front){
printf("空队列");
return 0;
}else{
sq->front = (sq->front+1)%maxsize; // 出队列直接将front指向下一个元素即可
return 1;
};
};
2.1.6-获取队首元素
DataType GetQueue(SeqQue *sq){
if(sq->real == sq->front){
printf("空队列");
}else{
x = sq->data[sq->front];
return x;
};
};
链式队列-使用单链表表示队列,但是单链表只有一个指针,无法即指向对首又指向队尾,所以我们设想原指针执行对首,新增一个指针用于指向队尾。
上溢:链表结构没必要考虑上溢
下溢:当尾指针(real)==头指针(front)时,即为空队列(除此之外还有另一个判断条件:就是当front->next==NULL时,也是空队列)
2.2-队列的链式实现
2.2.1-构造结构体
typedef struct LinkListQueueNode{ // 结点
DataType data; // 数据域
LinkListQueueNode *next; // 指针域
}LKQueNode;
typedef struct LkQueue{ // 链表
LKQueNode *real,*front; // 创建头尾指针
}LkQue;
LKQueNode Lk;
LkQue Lq;
2.2.2-初始化
int InitLinkQueue(LkQue *Lq){
LKQueNode *temp; // 创建结点
temp = (LKQueNode*)malloc(sizeof(LKQueNode));
Lq->front = temp; // 新创建的队列是个空队列,front和real均指向头结点即可
LQ->real = temp;
(LQ->front)->next = NULL;
};
2.2.3-判空
int EnptyLkQueue(LkQue *Lq){
if(Lq->real==Lq->front){ // 头指针和尾指针相等,表明空队列
printf("空队列");
return 1;
}else{
return 0;
};
};
2.2.4-入队列
// 链表进行插入操作时,一定要先搭上目标后驱,再连接目标前驱
int insertLkQueue(LkQue *Lq,DataType x){
// 链表插入新结点 不需要考虑上溢情况
LKQueNode *temp; // 创建新结点,用于存储新数据
temp = (LKQueNode *)malloc(sizeof(LKQueNode)); // 申请存储空间
temp->data = x; // 将值赋给新结点data域
temp->next = NULL; // 尾部新插入的结点是最后一个,其next指向null
(Lq->real)->next = temp; // 将原最后一个结点的next指向新插入的结点
Lq->real = temp; // 将real指针下移,指向新插入的结点(表示新插入的结点是最后结点)
};
2.2.5-出队列
int outLkQueue(LkQue *Lq){
if(Lq->real==Lq->front){ // 出队列校验下溢
printf("空队列");
return 0;
}else{
LKQueNode *temp;
temp = (Lq->front)->next; // temp用于存放要出队列的结点信息
(Lq->front)->next = temp->next; //将front指向新的队首结点
if(temp->next==NULL){ // 这里要校验队列是不是空
Lq->real = Lq.front; // 如果是,尾指针和头指针相等
};
free(temp); // 释放temp
return 1;
};
};
2.2.6-获取对首元素
DataType GetLkQueue(LkQue *Lq){
if(Lq->real==Lq->front){ // 出队列校验下溢
printf("空队列");
return NULL;
}else{
LkQueNode *temp; // 新建结点
temp = (Lq->front)->next;
return temp->data;
};
未完待续~