408数据结构-栈和队列的基本概念 自学知识点整理

(Stack)是只允许在一端进行插入或删除操作的线性表。首先栈是一种线性表,但限定这种线性表只能在某一端进行插入和删除操作。
栈的操作特性可以明显地概括为 后进先出(Last In First Out, LIFO)。

栈的数学性质:当 n n n个不同元素进栈时,出栈元素不同排列的个数为 1 n + 1 C 2 n n \frac{1}{n+1}C_{2n}^{n} n+11C2nn。这个公式被称为 卡特兰数(Catalan)公式,可采用数学归纳法证明。

与线性表类似,栈也有两种存储方式。
顺序栈:采用顺序存储的栈称为 顺序栈,它利用一组地址连续的存储单元存放自栈底到栈顶的数据元素,同时附设一个指针(top)指示当前栈顶元素的位置。
顺序栈的定义

#define MaxSize 10
typedef struct {//顺序栈的定义
	int data[MaxSize];//静态数组存放栈中元素
	int top;//栈顶指针
}SqStack;

顺序栈的初始化:将栈顶指针设为-1(也可设为0,但后续操作逻辑也需要对应修改)。

void InitSqStack(SqStack& S) {//初始化顺序栈
	S.top = -1;//初始化栈顶指针
	return;
}

判空

bool StackEmpty(SqStack& S) {//判断栈空
	return S.top == -1 ? true : false;
}

进栈

bool Push(SqStack& S, int x) {//新元素入栈
	if (S.top == MaxSize - 1)return false;//栈满报错
	S.data[++S.top] = x;//栈顶指针先加1,再让新元素入栈
	return true;
}

出栈

bool Pop(SqStack& S, int& x) {//出栈操作,并用x返回栈顶元素
	if (S.top == -1)return false;//栈空报错
	x = S.data[S.top--];//x记录栈顶元素,再栈顶指针减1
	return true;
}

读取栈顶元素:和出栈操作非常类似,唯一的不同之处是栈顶元素无需出栈。

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

顺序栈的基本操作实现就这些。

共享栈:利用栈底元素相对不变的特性,可让两个顺序栈共享一个一维数组空间,将两个栈的栈底分别设置在共享空间的两端,两个栈顶向共享空间的中间延伸,从而更有效地利用存储空间。

typedef struct {
	int data[MaxSize];
	int top0;//0号栈栈顶指针
	int top1;//1号栈栈顶指针
}ShStack;//定义共享栈

void InitShStack(ShStack& S) {
	S.top0 = -1, S.top1 = MaxSize;//初始化共享栈的两个栈顶指针
	return;
}
//top0=-1时0号栈空,top1=MaxSize时1号栈空
//共享栈栈满条件:top0+1 == top1

链栈:采用链式存储结构的栈被称为 链栈。链栈的优点是便于多个栈共享存储空间和提高其效率,且不存在栈满上溢的情况。通常采用单链表实现,并规定所有操作都是在单链表表头进行的。

typedef struct Linknode {
	int data;
	struct Linknode* next;
}Linknode, * LiStack;//链栈的定义

由于其基本操作与单链表基本相同,故不作赘述,唯一需要注意的就是只能在表头进行增删查等基本操作。

完整代码可看我的Github:传送门


队列

队列 (Queue)简称队,也是一种操作受限的线性表,只允许在表的一端进行插入,而在表的另一端进行删除。向队列中插入元素称为入队进队;删除元素称为出队离队。其操作的特性是 先进先出(First In First Out, FIFO)。
队列的顺序存储:队列的顺序实现是指分配一块连续的存储单元存放队列中的元素,并附设两个指针:队头指针 f r o n t front front指向队头元素,队尾指针 r e a r rear rear指向队尾元素的下一个位置
定义一个顺序存储类型的队列:

#define MaxSize 10
typedef struct {//定义顺序存储结构的队列
	int data[MaxSize];//用静态数组存放队列元素
	int front, rear;//队头指针和队尾指针
}SqQueue;

初始时:Q.front=Q.rear=0。

void InitSqQueue(SqQueue& Q) {//初始化队列
	Q.front = Q.rear = 0;//初始时,队头、队尾指针都指向0
	return;
}

入队

bool EnQueue(SqQueue& Q, int x) {//新元素入队
	if (队列已满)return false;//伪代码,若队列已满则报错
	Q.data[Q.rear++] = x;//将x插入队尾后,队尾指针加一
	return true;
}

不难看出,当有元素出队时,即使队尾指针已经指向了MaxSize的位置,队列依然是不满的,故不能将“Q.rear==MaxSize”用于判断队列是否存满。
为解决这个问题,引入循环队列
循环队列 :将顺序队列中的元素从逻辑上将其视为一个环,称为循环队列。其核心思想是将指针对MaxSize进行取余运算。

初始时:Q.front=Q.rear=0
队首指针进1:Q.front=(Q.front+1)%MaxSize
队尾指针进1:Q.rear=(Q.rear+1)%MaxSize
队列长度:(Q.rear+MaxSize-Q.front)%MaxSize
出队入队时,指针都按顺时针方向进1(如下图所示)
图片来自王道408数据结构2025考研版

判空

bool QueueEmpty(SqQueue& Q) {//判断队列是否为空
	return Q.rear == Q.front ? true : false;
}

入队:需要牺牲一个存储单元来判断队列是否已满。

bool EnQueue(SqQueue& Q, int x) {//入队
	if ((Q.rear + 1) % MaxSize == Q.front)return false;//若队列已满则报错
	Q.data[Q.rear] = x;//将x插入队尾
	Q.rear = (Q.rear + 1) % MaxSize;//队尾指针加1取模
	//用模运算将存储空间在逻辑上变成了环状
	return true;
}

出队

bool DeQueue(SqQueue& Q, int& x) {//出队(删除一个队头元素,并用x返回)
	if (Q.rear == Q.front)return false;//若队空则报错
	x = Q.data[Q.front];
	Q.front = (Q.front + 1) % MaxSize;//队头指针后移一位
	return true;
}

读取队头元素:基本操作同出队,只是队头元素无需出队。

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

另外判断队满/队空的两种方法:

  • 一种是在结构体中增加一个size元素(int),初始化size值为0,每执行一次插入操作时size++,每执行一次删除操作时size--。在判断队列是满或空时,只需根据size的值即可进行判断。
  • 另一种则是增加一个tag元素(可以用bool),表示最近一次进行的操作是入队还是出队,因为只有出队操作才可能导致队列变空。初始化tag值为0,每次执行插入操作都将tag的值赋为1,每次执行删除操作时都将tag的值赋为0。这样在判断队列是满或空时,只需在判断条件中加上“Q.tag==1”或“Q.tag==0”即可。

需要注意的是,当采用队尾指针指向队尾元素的逻辑时,初始化Q.rear的值要为MaxSize-1,入队时要先移动队尾指针,出队时要后移动队头指针,其余基本操作的逻辑也需进行一定程度的更改。考试时一定要注意审题。


队列的链式存储结构

队列的链式表示称为 链队列 ,它实际上是一个同时有队头指针和队尾指针的单链表。 头指针 指向队头结点, 尾指针 指向队尾结点,即单链表的最后一个节点。
定义一个链队列:需要用到两个结构体,一个表示结点,另一个表示队列。

//队列的链式实现
typedef struct LinkNode{//链式队列每个结点的定义
	int data;
	struct LinkNode* next;
}LinkNode;
typedef struct {//链式队列的定义,只需有头指针和尾指针即可
	LinkNode* front, * rear;
}LinkQueue;

初始化队列:(默认带头结点)

void InitQueue(LinkQueue& Q) {//初始化队列(带头结点)
	Q.front = Q.rear = (LinkNode*)malloc(sizeof(LinkNode));//初始时头尾指针都指向头结点
	Q.front->next = NULL;
	return;
}

断队列是否为

bool IsEmpty(LinkQueue& Q) {//判断队列是否为空
	return Q.front == Q.rear ? true : false;
}

新元素入队

bool EnQueue(LinkQueue& Q, int x) {//新元素入队
	LinkNode* s = (LinkNode*)malloc(sizeof(LinkNode));
	if (s == NULL)return false;
	s->data = x;
	s->next = NULL;
	Q.rear->next = s;//将s插入队尾
	Q.rear = s;//队尾指针指向s
	return true;
}

队头元素出队

bool DeQueue(LinkQueue& Q, int& x) {//队头元素出队(带头结点)
	if (IsEmpty(Q))return false;//空队报错
	LinkNode* p = Q.front->next;
	x = p->data;//用x返回队头元素数据
	Q.front->next = p->next;//修改队头结点的next指针,指向队头元素的后继
	if (Q.rear == p)Q.rear = Q.front;//若被删除结点是队尾结点,则修改rear指针
	free(p);//释放结点空间
	return true;
}

带头结点的链队列的基本操作如上,可以对照单链表进行比较,寻找相同点和不同点。
以下是不带头结点的基本操作实现:需要对第一个元素特殊处理。

void Init(LinkQueue& Q) {//初始化队列(不带头结点)
	Q.front = NULL, Q.rear = NULL;
	return;
}

bool Is_Empty(LinkQueue& Q) {//判断队列是否为空(不带头结点)
	return Q.front == NULL ? true : false;
}

bool EnQueue(LinkQueue& Q, int x) {//新元素入队(不带头结点)
	LinkNode* s = (LinkNode*)malloc(sizeof(LinkNode));
	if (s == NULL)return false;
	s->data = x;
	s->next = NULL;
	if (Is_Empty(Q)) {//不带头结点时需要对第一个元素进行特殊处理
		Q.front = s;//空队列中插入第一个元素
		Q.rear = s;//修改队头队尾指针
	}
	else {
		Q.rear->next = s;//将s插入队尾
		Q.rear = s;//队尾指针指向s
	}
	return true;
}

bool DeQueue(LinkQueue& Q, int& x) {//队头元素出队(不带头结点)
	if (Is_Empty(Q))return false;//空队报错
	LinkNode* p = Q.front;//p指向此次出队的结点
	x = p->data;//用x返回队头元素数据
	Q.front = p->next;//修改队头指针,指向队头元素的后继
	if (Q.rear == p)Q.front = NULL, Q.rear = NULL;//若被删除结点是队尾结点,则头尾指针都指向NULL
	free(p);//释放结点空间
	return true;
}

完整代码可以看我的Github:传送门


双端队列

双端队列 是指允许两端都可以进行插入和删除操作的线性表,两端的地位是平等的。
在双端队列进队时,前端进的元素排列在队列中后端进的元素的前面,后端进的元素排列在队列前端进的元素的后面。(即假设左边为前端,右边为后端,则左边进的元素始终在右边进的元素的左边)
在双端队列出队时,无论是前端还是后端出队,先出的元素排列在后出的元素的前面。(即先出队的元素无论从哪一端出,都在出对序列中排在后续出队元素的前面)

思考:如何由入队序列a,b,c,d得到出队序列d,c,a,b?
答:先从后端依次插入a,b,c,d,然后从后端删除d,c,再从前端删除a,b。

若限制双端队列,只允许从一端进行插入和删除,则双端队列退化为
输出受限的双端队列:允许在一端进行插入和删除,但在另一端只允许插入的双端队列。
输入受限的双端队列:允许在一段进行插入和删除,但在另一端只允许删除的双端队列。

对双端队列,408初始中只考察逻辑,不考察代码实现,因此只需了解并掌握其思想即可。

以上。

  • 28
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值