数据结构---栈和队列

数据结构---栈和队列

    • 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)也是一种操作受限的线性表。

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

栈类似理解为盘子或烤串。取放盘子时只能在一摞盘子顶部进行取放。烤肉时肯定是从签子的顶部串肉,吃串的时候也是从顶部开吃。
数据结构---栈和队列_第1张图片


2,栈的相关术语

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

数据结构---栈和队列_第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

数据结构---栈和队列_第3张图片


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;   //指针先加1
	S.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;如下图即为共享栈栈满的情形:
数据结构---栈和队列_第4张图片


知识结构梳理:
数据结构---栈和队列_第5张图片


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):只允许在一端进行插入操作,另一端进行删除操作线性表即为栈。

可以形象理解为,排队打饭的场景、排队过收费站的场景。都是队尾加入元素,队头删除元素。
数据结构---栈和队列_第6张图片


8,队列的相关术语

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

数据结构---栈和队列_第7张图片


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队尾指针指向队尾元素的后一个位置(即下一个应该插入元素的位置)。如下图所示:
数据结构---栈和队列_第8张图片
顺序队列初始化的时候需要让队尾指针和队头指针同时指向0。同时这也是判断顺序队列是否为空的依据。 代码实现如下:

//初始化队列
void InitQueue(SqQueue &Q){
	//初始化时,队头队尾指针均指向0
	Q.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; 。使用模运算将存储空间在逻辑上变为了环状,队满的图示如下:
数据结构---栈和队列_第9张图片
需要牺牲一个存储单元,队满情形下,不可让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 个;

数据结构---栈和队列_第10张图片
上图例子中,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;

数据结构---栈和队列_第11张图片


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

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

数据结构---栈和队列_第12张图片
数据结构---栈和队列_第13张图片

链式队列的代码实现

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

//初始化链式队列(带头结点)
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都指向NULL
	Q.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,双端队列

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

数据结构---栈和队列_第14张图片
特殊的双端队列有:

  • 输入受限的双端队列:允许从一端输入,两端删除的线性表。
  • 输出受限的双端队列:允许从两端插入,一段删除的线性表。
    数据结构---栈和队列_第15张图片
    可以根据栈或双端队列性质判断输出序列的合法性。并且栈中合法的序列,双端队列内一定也合法。

你可能感兴趣的:(数据结构,考研,算法,栈,队列,c++)