408复习笔记——数据结构(三):栈和队列

408考研笔记系列(三)(PS:本人使用的是王道四本书和王道视频)


前言

上一章节里,我们知道了一种线性逻辑结构的线性表,并且知道了线性表的两种存储结构,分别是顺序存储结构的顺序表和链式存储结构的链表,其中链表又包括单链表、双链表、循环链表和静态链表,他们每个都有自己的特点;这一章节我们将开始学习两个新的线性逻辑结构的数据结构:栈和队列。


一、简介

栈和队列其实也是一种线性表,只是他们因为结合现实情况而受到了限制。

二、主要内容

2.1 栈

栈的特点便是后进先出,就像我们洗碗一样,先洗好的放在下面,后洗好在下次吃饭时候会先拿出来用;

那么如何实现这样的功能呢?其实非常简单,只需要对我们之前学过的线性表加一个限定条件就可以做到了:只允许在一端进行插入或者删除操作;这样,线性表就被我们改装为后进先出的栈了;

根据存储结构,栈可以分为顺序存储结构的顺序栈和链式存储结构的链栈;他们的运算与线性表相似,主要为创、销、增、删、查;接下来会结合代码对两种存储结构的栈进行解释:

2.1.1 顺序栈

顺序栈,顾名思义,便是存储结构为顺序存储的栈;主要通过数组实现,会提前分配好栈的存储单元,此外,还会保存栈顶指针,用于保存现在栈顶元素在数组中的位置,这里,我相信大家应该不难联想到栈顶指针的作用了,没错,就是方便我们判断栈空、栈满以及记录现在栈顶元素的位置;

typedef struct {
	int data[MaxSize]; // 创建栈的空间,用数组表示
	int top; // 用于表示栈顶元素的在数组中的位置
}SqStack;

初始化栈,主要是将栈顶指针进行初始化,可以初始化为-1,也可以初始化为0,只是初始化不同值的情况下,我们在插入和删除时需要注意栈顶指针的位置:

void Init_Stack(SqStack *S)
{
	// 初始化时将栈顶设为-1
	S->top = -1;
}

出栈和入栈的操作,主要就是根据栈顶指针进行操作,需要注意的是出栈需要判断栈空,出栈需要判断栈满,也是通过栈顶指针完成:

// 入栈
void Push(SqStack *S,int x)
{
	//首先判断栈是否已经满了
	if (S->top == MaxSize - 1)
	{
		printf("SqStack is full");
	}
	else
	{
		S->data[++S->top] = x;
	}
}
// 出栈
void Pop(SqStack *S, int *x)
{
	// 出栈前首先判断栈是否为空
	if (S->top == -1)
	{
		printf("SqStack is empty");
	}
	else
	{
		*x = S->data[S->top--];
	}
}

顺序栈中为了更加有效利用存储空间,提出共享栈,让两个顺序栈共享一个一维数组空间,两个栈的空间相互调节,通过两个栈顶指针相减为1判断栈满,减少存储空间,降低发生上溢的可能;

2.2.2 链栈

链栈便是链式存储结构的栈了,也是通过单链表实现的,所有的操作都是在表头实现的,一般是没有头结点,直接指向栈顶元素;但是因为我的代码是根据我之前链表的代码来的,所以链栈中含有头结点;

typedef struct LinkNode {
	int data;
	struct LinkNode *next;
}LNode,*LinkStack;

链栈因为不会出现栈满上溢的情况,所以入栈不会考虑栈满的问题,而出栈依然需要考虑栈空的问题:

// 入栈
LinkStack Push(LinkStack *LS, int x)
{
	// 这里与顺序栈不同,不需要判断栈是否已满
	LNode* s;
	s = (LNode*)malloc(sizeof(LNode));
	s->data = x;
	s->next = (*LS)->next;
	(*LS)->next = s;
}
LinkStack Pop(LinkStack *LS)
{
	LNode* p;
	if ((*LS)->next == NULL)
	{
		//需要判断链栈是否为空 
		printf("LinkStack is empty");
	}
	else
	{
		p = (*LS)->next;
		(*LS)->next = (*LS)->next->next;
		free(p);
	}
	return LS;
}

2.2 队列

队列和栈类似,都是线性的逻辑结构,也是一种在操作上受限制的线性表;队列只允许在表的一端进行插入操作,又称入队,在另一端进行删除操作,又称出队;从实际来看,其实这是非常常见的,就如同它的名字一般,我们日常生活排的队便是这样;

队列的存储结构也包括两种:线性存储结构的顺序队列和链式存储结构的链队列;

队列的运算同栈一样,主要便是队列的创、销、增、删和查了;当然查在队列和栈的作用并不明显,主要还是队列的入队和出队,以及如何判断顺序队列的队空、队满等;

2.2.1 顺序队列

顺序队列便是存储结构为顺序存储的队列,主要通过一段连续存储空间的数组实现,此外,队列中还记录了队头和队尾指针,即队头和队尾在数组中的下标(这里还有一种情况:队尾指针指向的是队尾元素的下一个位置,我们的代码采用的便是这种情况,两种情况的代码并不相同,需要进行微调,考试中需要注意出题人给的是哪种情况

#define MaxSize 10
typedef struct {
	int data[MaxSize];// 用于存放队列元素
	int front, rear;// 用于存放队头和队尾指针,分别指向队头元素在队列数组中的位置和队尾元素的下一个位置
}SqQueue;

顺序队列当我们入队的元素越来越多,达到数组提供的最大数量时,问题便来了,此时的队列似乎已经满了,但是因为出队操作是在队头完成的,所以此时的队列因为出队操作是有空闲位置的,只是我们的队尾指针已经是数组的最后一个位置;

那么我们应该如何解决这个问题呢?没错,取模就行了,用我们的队尾指针对数组长度取模的余数便可以再次回到数组的开头位置,在逻辑上形成了一个循环,也就成了我们常见的循环队列;

2.2.2 循环队列

循环队列在我们的考试中是非常常见的,关于他的考题也很多,主要在于不同情况下判断队满或者队空;那么都有哪几种情况呢?

一种是在循环队列中空闲出数组的最后一个位置,方便判断队空和队满;另一种是增加一个tag标记,用来记录上一次操作是插入还是删除,并根据这个,判断队空和队满;

那么第一种情况为什么要空闲出一个位置呢?因为如果不空闲出一个位置的话,我们在判断队空的时候是这样子的:

if (SQ->front == SQ->rear)

判断队满时,由于队尾指针指向的是队尾元素的下一个位置,那么如果不留下数组最后一个位置的话,判断队满还是这样子的:

if (SQ->front == SQ->rear)

这便出现问题了,判断队满和队空怎么能是一样的呢?为了避免出现问题,我们就给数组最后一个元素的位置空下来了,这样判断队满就变成了这样:

if ((SQ->rear + 1) % MaxSize == SQ->front)

好啦!这样不就行了嘛,不要纠结之前提到的队尾指针指向的位置有两种情况了,真的没有必要,因为采用队尾指针指向队尾元素的话,如果不空一个位置出来,他们的判断队满和队空还是一样的,都是这个样子的:

if ((SQ->rear + 1) % MaxSize == SQ->front)

给出此时入队和出队操作的代码:

// 入队操作
int EnQueue(SqQueue *SQ,int x)
{
	// 入队前首先判断是否已经队满,这里的判断是因为循环队列中有一个位置空闲,避免队满与队空的判断一样
	if ((SQ->rear + 1) % MaxSize == SQ->front)
	{
		printf("Queue is full");
		return 0;
	}
	SQ->data[SQ->rear] = x;
	SQ->rear = (SQ->rear + 1) % MaxSize;
	return 1;
}
// 用于出队操作,并将出队元素传出
int DeQueue(SqQueue *SQ, int *x)
{
	// 出队前线判断是否已经队空,这里的判断也是依据循环队列中空闲一个元素
	if (SQ->front == SQ->rear)
	{
		printf("Queue is empty");
		return 0;
	}
	*x = SQ->data[SQ->front];
	SQ->front = (SQ->front + 1) % MaxSize;
	return 1;
}

你以为这样就结束了嘛?怎么可能,出题老师会这样结束吗?你浪费了人家数组一个位置的空间啊,这能忍吗?出题老师很愤怒,他说:不行,这数组你必须给我满上,一个空都不能留;

因此我们的第二种情况便应运而生,满上可以,但是我需要一个额外的空间用于记录你上一个操作是插入还是删除;因为如果你上一次操作是删除的话,那么此时判断队空和队满,那么一定不是队满呀!(你都删掉一个了,怎么可能队满嘛)那在满足情况的条件下,就只能队空啦!同理上一次操作是插入的情况下,判断队空队满那一定就是队满啦!

#define MaxSize 10
typedef struct {
	int data[MaxSize];// 用于存放队列元素
	int front, rear;// 用于存放队头和队尾指针,分别指向队头元素在队列数组中的位置和队尾元素的下一个位置
	int tag;// 用于记录上一次操作是插入还是删除操作;若上一次操作是删除操作,那么可用于判断队空;若上一次操作为插入操作,那么可用于判断队满
}SqQueue;
// 加入标记tag后,队列长度可以达到MaxSize

与第一种情况不同,此时我们在初始化时,还需要初始化tag标记为队空的情况,

// 初始化队列
void Init_Queue(SqQueue *SQ)
{
	// 初始化队列时,让队列的队头指针和队尾指针都指向数组起始位置
	SQ->front = SQ->rear = 0;
	// 初始化时,由于队空,因此标记为0
	SQ->tag = 0;
}

入队和出队的代码:

// 入队操作
int EnQueue(SqQueue *SQ, int x)
{
	// 入队前首先判断是否已经队满,这里的判断通过tag标记,避免队满与队空的判断一样
	if (SQ->rear % MaxSize == SQ->front && SQ->tag )
	{
		printf("Queue is full");
		return 0;
	}
	else
	{
		SQ->data[SQ->rear] = x;
		SQ->rear = (SQ->rear + 1) % MaxSize;
		// 每一次入队操作后,需要将tag标记为1
		SQ->tag = 1;
		return 1;
	}
}
// 用于出队操作,并将出队元素传出
int DeQueue(SqQueue *SQ, int *x)
{
	// 出队前线判断是否已经队空,这里的判断也是依据tag标记
	if ( SQ->front == SQ->rear && !SQ->tag )
	{
		printf("Queue is empty");
		return 0;
	}
	else
	{
		*x = SQ->data[SQ->front];
		SQ->front = (SQ->front + 1) % MaxSize;
		//每次出队操作后需要将tag标记为0
		SQ->tag = 0;
		return 1;
	}
}

2.2.3 链式队列

上面介绍的都是顺序存储的队列,接下来便是链式存储的队列;其实链式队列相比较循环队列更加容易操作,因为他不会用数组下标来为难你队空和队满;和链栈一样,链式队列也不会出现队满的情况,那么便不需要去分情况判断了;

但是链式队列也有队头指针和队尾指针,此外,我们一般还会给链队中加入队长元素,方便我们判断队空,机智如我!!!

// 定义链式队列的结点
typedef struct {
	int data;
	struct LinkNode *next;
}LinkNode;
// 定义链式队列
typedef struct {
	LinkNode *front, *rear;// 链式队列中包含指向队头和队尾的指针,由于队列是队头进队尾出,因此需要标记队尾指针
	int length;// 用于记录队列的长度
}LinkQueue;

我们一般将队头设置在链头,为了方便删除队头元素;链式队列的出队和入队代码:

// 入队操作
LinkQueue EnQueue(LinkQueue * LQ, int x)
{
	//在链式队列中,不会存在队满的问题,因此不需要像顺序队列中判断队满的情况
	LinkNode *s;
	s = (LinkNode*)malloc(sizeof(LinkNode));
	s->data = x;
	// 这里在链式队列的头指针出入队元素
	s->next = NULL;
	(*LQ).rear->next = s;
	(*LQ).rear = s;
	(*LQ).length++;
}
// 出队操作
LinkQueue DeQueue(LinkQueue *LQ, int *x)
{
	//出队时需要判断是否已经队空,若队空则不要出队
	if ((*LQ).length == 0)
	{
		printf("Queue is empty");
	}
	LinkNode *p;
	p = (*LQ).front->next;
	*x = p->data;
	(*LQ).front->next = p->next;
	(*LQ).length--;
	if ((*LQ).length == 0)
	{
		// 最后需要判断是为只有最后一个元素,若只有一个元素,那么需要在free前将尾指针指向头指针
		(*LQ).rear = (*LQ).front;
	}
	free(p);
}

出队操作需要注意的一点是:最后需要判断我们删除的是不是最后一个元素,如果删除的是最后一个元素的话,我们需要让队尾指针指向队头指针;

2.2.4 双端队列

接下来将会介绍一种出题人之前考过的一种队列:双端队列;双端队列也就是允许两端都可以进行入队和出队操作;这样的双端队列并不难,比较难的是他的变种:输出受限的双端队列和输入受限的双端队列;

输出受限的双端队列:允许在一端进行插入和删除,但在另一端只允许插入的双端队列;
输入受限的双端队列:允许在一端进行插入和删除,但在另一端只允许删除的双端队列;

通常会问那种序列是合法的等等;有一个比较常见的方法,就是先找开头为输入序号偏后的元素看;

2.3 栈和队列的应用

之前在介绍栈和队列时,,我们便已经提到,栈和队列是为了适应现实的情况而对线性表进行了限制;既然是为了适应现实的情况,那么他们在现实肯定是存在非常广泛的应用的;

2.3.1 栈的应用

栈其实在我们日常的编程生活中是非常常见的,其中最为广泛的应用像栈在括号匹配的应用、以及栈在表达式求值时的应用(考试经常会考察),还有一种在我们日常编程很常见的栈在递归中的应用,这常见于函数调用,没错也叫函数调用栈;

2.3.1.1 栈在括号匹配的应用

这里其实非常简单,就如同编译器在词法分析时判断这段代码是否合理时,会看他的小括号、中括号和大括号是否配对上,以及是否少括号或者多括号;我们可以通过栈很方便的实现这样的功能;
栈在括号匹配的代码:

#define _CRT_SECURE_NO_WARNINGS
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
// 利用栈实现表达式的括号匹配
#define MaxSize 10
int main()
{
	char S[10];// 用数组创建栈的空间
	int top = -1;// 用于表示栈顶元素在数组中的位置
	char Exp[20];// 用于存储输入的表达式
	int i = 0,flag = 0;
	scanf("%s", Exp);
	while (Exp[i] != '\0')
	{
		char C = Exp[i++];
		// 寻找左括号运算符
		if (C == '(' || C == '[' || C == '{')
		{
			// 判断栈是否已经满了
			if (top == MaxSize - 1)
			{
				printf("overflow!!!\n");
				flag = 0;
				break;
			}
			else
			{
				//若栈未满,则在栈中存放左括号运算符
				S[++top] = C;
			}
		}
		// 寻找右括号运算符
		if (C == ')' || C == ']' || C == '}')
		{
			// 首先判断是否已经栈空
			if (top == -1)
			{
				printf("Bracket doesn't match !\n");
				flag = 0;
				break;
			}
			// 查看栈顶运算符,并查看是否与该右括号运算符相对应
			char S_top = S[top--];
			if (S_top == '('&& C == ')' || S_top == '['&&C == ']' || S_top == '{'&&C == '}')
			{
				flag = 1;
			}
			else
			{
				printf("Bracket doesn't match !\n");
				flag = 0;
				break;
			}
		}
		//printf("%c\n", Exp[i++]);
	}
	if (flag && top == -1)
	{
		printf("Bracket match !\n");
	}
	else
	{
		printf("Bracket doesn't match !\n");
	}
	system("pause");
}
2.3.1.2 栈在表达式求值中的应用

这是在考试中非常常见的一个考点,这里我没有去专门写代码,因为考试中,我们只要能够去理解过程是怎样实现的,基本上这类问题是没有问题的;

理解这个应用,我们首先需要知道后缀表达式和前缀表达式,但是因为考试中主要考察的是后缀表达式,因此这里主要讲解如何通过栈将中缀表达式(也就是我们常见的表达式)转换为后缀表达式,以及如何利用栈通过后缀表达式求表达式的值;

中缀表达式变换为后缀表达式
1)如果遇到操作数,我们就直接将其输出。
2)如果遇到操作符,则我们将其放入到栈中,遇到左括号时我们也将其放入栈中。
3)如果遇到一个右括号,则将栈元素弹出,将弹出的操作符输出直到遇到左括号为止。注意,左括号只弹出并不输出。
4)如果遇到任何其他的操作符,如(“+”, “*”,“(”)等,从栈中弹出元素直到遇到发现更低优先级的元素(或者栈为空)为止。弹出完这些元素后,才将遇到的操作符压入到栈中。有一点需要注意,只有在遇到" ) “的情况下我们才弹出” ( “,其他情况我们都不会弹出” ( "。
5)如果我们读到了输入的末尾,则将栈中所有元素依次弹出。

这里给出一个实例的过程图,更加的形象,可以观察其栈中运算符的变换过程:
在这里插入图片描述
后缀表达式求表达式值:
)遍历表达式,遇到的数字首先放入栈中:
2)接着读到运算符,则弹出运算数,先弹出的是右边的运算数,计算结果并将结果压入到栈中。

这里也给出后缀表达式求值的过程图,注意观察栈的变换
在这里插入图片描述

2.3.1.3 栈在递归的应用

递归的过程中会不停调用函数,其中函数调用栈会存储调用返回的地址、实参以及局部变量;但是太多的层的递归可能会导致栈溢出,也会导致效率降低;

可以用栈模拟递归过程,以此来消除递归,但是对于单向递归和尾递归而言,也可以用迭代的方法来消除递归;

2.3.2 队列的应用

队列的应用也是十分广泛的;包括很多应用我们在之后的学习过程中还会深入学习,这更体现出了数据结构的重要性,数据结构无处不在;

常见应用包括:队列在层次遍历的应用、队列在广度优先搜索算法和操作系统在解决CPU资源竞争时;这里不做深入展开,因为考试并未将这里的重点放在队列的应用,我们还是需要重点掌握栈在表达式求值时的应用;

2.4 特殊矩阵的压缩存储

以上已经把栈和队列相关的内容基本完善了,这里补充一种我们在考试中经常会遇到的一类考点:特殊矩阵的压缩存储;主要是利用数组对一些有特殊形式的矩阵的存储方式进行优化,也可以称之为压缩;这里的特殊矩阵主要包括对称矩阵、三角矩阵和三对角矩阵和稀疏矩阵的两种存储方式

值得注意的是:在压缩存储矩阵时,有按行存储和按列存储两种方式存储到数组中,而且需要注意数组的起始是0还是1,如果题目没有给出,那么默认情况下是从0开始的,因此在答题时需要注意以上两点,因为这种问题的出题点一般为选择题,所以如果不留意间可能就会丢分;

2.4.1 对称矩阵

对称矩阵,顾名思义,它是一种对称的矩阵,那么他是关于什么对称的呢?主要是根据主对角线对称,并将主对角线的上下两部分分别划分上三角矩阵和下三角矩阵,其中上三角矩阵和下三角矩阵一样;因此为了节省更多的存储空间,我们只在数组中存储 主对角线上和上下三角矩阵中的一个;
在这里插入图片描述

对称矩阵的题目有时给的可能是上三角部分元素插入数组中,但是题目问的确实下三角矩阵的某个元素,这个时候不要感觉奇怪,因为这是对称矩阵嘛,所以将角标替换后,这个元素便出现在了上三角部分啦!

2.4.2 三角矩阵

通过对称矩阵,我们知道主对角线会把矩阵划分为上三角矩阵和下三角矩阵两部分,三角矩阵指的便是这两种中的任意一种;其他位置上是某一个常数,因此三角矩阵会比对称矩阵多一个单位的存储空间,用来存储该常数;
在这里插入图片描述

2.4.3 三对角矩阵

三对角矩阵又称带状矩阵,看图就明白啦!

2.4.4 稀疏矩阵

简而言之,就是非零元素的个数远小于矩阵元素的个数;因为有太多的零,因此,为了节省存储空间,我们将非零元素及其相应的行和列构成一个三元组,只需要存储这些三元组便可以了;

三元组是顺序存储结构下的存储方式,而在链式存储结构下,我们往往采用十字链表法进行存储;
在这里插入图片描述

三、常见题及易错题归纳

线性表答案:C、C、A、C

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
答案下一章

(这才刚做了三章,就想玩了,不行,一定要坚持!!!要不明天下午去吃点好吃的吧,但是好像快没钱了,QAQ)

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值