栈和队列
上一期链接【数据结构】树与二叉树的概念与基本操作代码实现
下一期链接【数据结构】数组(稀疏矩阵、特殊矩阵压缩、矩阵存储、稀疏矩阵压缩、稀疏矩阵的快速转置、十字链表)
栈(stack)
什么是栈
栈是限制在表的一端(表尾)进行插入和删除的线性表(插入和删除受限制的线性结构)
特点:先入后出
进栈和出栈演示:
栈的术语说明
- 栈顶:允许进行插入和进行删除操作的一段成为栈顶
- 栈底:表的另一端称为栈底 (第一个元素进入的位置)
- 压栈(入栈、进栈):在栈顶位置插入元素的操作叫做压栈,或入栈、进栈
- 出栈(弹栈、退栈):删除栈顶元素的操作叫做出栈,也叫作弹栈,或者退栈
- 空栈:不含元素的空表
- 栈溢出:当栈满的时候,如果再有元素压栈,则发生上溢,当栈空的时候,再出栈则发生下溢
栈的主要操作:
- 初始化空栈
- 入栈
push
- 出栈
pull
- 判断栈是否为空栈
- 取栈顶数据元素
栈的存储结构
1. 顺序存储结构 —— 顺序栈
顺序栈是用一组地址连续的存储单元依次存储栈中的每一个数据元素。[用数组实现]
栈的主要操作都是在栈顶进行的,因此必须保存栈顶的位置以便操作进行。
- 实际栈顶位置–栈顶元素在数组中的位置(对应的数组下标)
- 下一次入栈的位置–即下一次插入到数组中的位置
保存栈顶位置的变量称为栈顶指针.
栈顶指针的使用方法:
方法1:栈顶指针指示栈顶的实际位置
方法2:栈顶指针指向下一次入栈的位置
基本操作的代码实现:
//顺序栈的存储结构
#define MAXSIZE 5
typedef struct{
int data[MAXSIZE];
int top;
}SeqStack;
SeqStack s;
栈顶指针指示实际栈顶的位置
栈初始化:
s.top=-1;//初始化
栈空:
s.top=-1;//栈空
栈满:
s.top=MAXSIZE-1;
栈顶:
s.data[s.top];
入栈:
//入栈 :输入示例
if(s.top<MAXSIZE-1){
s.data[++s.top]=x;
}
//数组没有空间,溢出
else cout<<"overflow"<<endl;
- 出栈:
//出栈: 只要 s.top>=0 栈中就还有元素
if(s.top>=0){
cout<<s.data[s.top--]<<endl;
}
2. 链式存储结构 —— 链栈
采用不带表头结点的单链表存放栈,每个数据元素对应结点空间的指针存放该数据元素的直接前驱
基本操作的代码实现:
//也就是链表的存储结构
typedef struct node{
int data;
struct node *next;//指向该节点的直接前驱
}Node,*LinkList;
//相关操作
LinkList top;//栈顶指针
//出栈操作
if(top){
//栈是否为空的判断条件
p=top;
top=top->next;//栈顶指针下移
free(p);//释放空间
}
//入栈操作
s=(LinkList)malloc(sizeof(Node));//开辟新空间
cin>>s->data;//输入插入的值
s->next=top;//入栈
top=s;//栈顶指针上移
双栈共享存储空间
两个栈共用一个一维数组v[M],栈底分别设在数组的两端,各自向中间伸展,第一个栈自顶向下伸展,第二个栈自底向上伸展。两个栈共享存储空间,可互补空缺,使得某个栈实际可利用的空间大于M/2
队列(queue)
什么是队列
队列:限制插入在表一端(表尾)进行,而删除在表的另一端(表头)进行。(插入和删除操作受限制的线性结构)
特点:先进先出
队列的主要操作:入队、出队、初始化空队列、求队长
说明:队列的主要操作在队首和队尾处完成 ,要保留队头(front)、队尾(back)两个位置方便操作实现
双端队列:
双端队列又名(double ended queue),简称deque,双端队列没有队列和栈这样的限制级,它允许两端进行入队和出队操作,也就是说元素可以从队头出队和入队,也可以从队尾出队和入队。
队列的存储方式
1. 顺序存储结构——顺序队列
队列的顺序存储结构
//队列的顺序存储结构
define MAXSIZE 5
typedef struct{
int data[MAXSIZE];
int f,r;//队首和队尾指针:指数组元素的下标
}SeQueue;
//f为队首指针 队首数组元素下标 r为队尾指针 队尾元素下标
SeQueue q;
初始化:q.f=0;q.r=0;
入队: 只要有空间
if(q.r<MAXSIZE){
q.data[q.r]=x;
q.r++;//队尾指针上移(数组下标)
}
else printf(“overflow”);
出队:
if(q.f!=q.r)//if(q.f<q.r)
q.f++;//对手指针上移(数组下标)
假溢出以及解决方案
假溢出:针对上述代码,5次入队操作,5次出队操作后,此时队空,但是再次插入数据元素却判断:没有空间—明明有空间却判断出没有空间,这种现象称为假溢出
!!!注意:队列的限制就是两个指针q.r和q.f都是只能单方向移动的,后面会有讲到循环队列。
队列的顺序存储结构–假溢出
解决方案:
方法1:每删除一个数据元素,余下的所有数据元素顺次移动一个位置(数据量大的时候相当复杂)
方法2:循环队列,将顺序队列data[0… MAXSIZE -1]看成头尾相接的循环结构。
循环队列
通过求余运算可以实现数组首位的相连
//队列中元素的个数
//漏洞 但q.r=q.f时 可能队列已满,可能队列未满此时的结果为0
num = (q.r-q.f+MAXSIZE)%MAXSIZE;
//如果num=0 说明队列长度为0
//队列为空
if(!num){
cout<<"队列为空"<<endl;
}
else{
cout<<"队列不为空"<<endl;
}
初始化:q.f=0;q.r=0
入队:
//队未满
//condition表示对未满的条件
if(condition){//队未满
q.data[q.r]=x;
q.r=(q.r+1)%MAXSIZE;
}
else
printf("overflow");
出队:
//condition表示对不空的条件
if(condition){//队不空
q.f=(q.f+1)%MAXSIZE;
}
判断循环队列是空还是满!!!
队空和队满时的条件均为:q.f==q.r,无法根据队首和队尾指针的相对位置判断队列是处于“空”还是处于“满”的状态。
解决方案:
方法一:为区分队空、队满,牺牲一个存储位置,
当(q.r+1)%MAXSIZE==q.f
时认为队满了,q.r==q.f
为队空
方法二:设一计数器,初始化时计数器清0,入队时,计数器+1,出队时计数器-1
上述方法即可,优先使用方法一。
1. 顺序队列如何解决假溢出?
顺序队列的“假溢出”:
顺序队列是一种使用数组实现的队列。
当我们不断地进行入队操作时,队列的尾指针(rear)会不断向后移动,但是出队操作会释放掉前面的空间。然而,即使前面的空间被释放,指针仍然会继续向后移动,直到达到系统预留给程序的内存上界。这就导致了“假溢出”问题。
解决方法是使用循环队列。循环队列将存储队列的数组头尾相接,从而避免了“假溢出”。
初始化时,我们直接创建两个游标指针,分别指向头结点和尾结点。入队操作时,我们将尾指针向后移动,但要注意判断:如果尾指针达到队列的空间上限,需要从头节点继续开始移动.
2. 循环队列如何判断队满和队空?
循环队列的队满和队空判断:
循环队列的判断条件相对简单: 队空:当队头指针(front)等于队尾指针(rear)时,队列为空。
队满:当队尾指针加1后等于队头指针时,队列为满。
需要注意的是,由于循环队列中有一个位置是不存储元素的,因此队列中最多只能存储n-1个元素,其中n为队列的长度.
2. 链式存储结构——链队
■ 定义:采用链式存储结构存放
■ LinkList f,r;
说明: f为队首指针,指示链队的队首位置; r为队尾指针,指示链队的队尾位置
入队就是 单链表创建时的尾插法
出队就是 单链表按顺序从头删除元素
双端队列
双端队列:是一种允许同时从前端和后端添加和删除元素的特殊队列,它是队列和栈的结合体。
- 输入受限双端队列:队列的一端允许插入删除,另一端只允许删除的双端队列。
- 输出受限双端队列:队列的一端允许插入删除,另一端只允许插入的双端队列。
感谢您的阅读!
上一期链接【数据结构】树与二叉树的概念与基本操作代码实现
下一期链接【数据结构】数组(稀疏矩阵、特殊矩阵压缩、矩阵存储、稀疏矩阵压缩、稀疏矩阵的快速转置、十字链表)