WHCSRL 技术网

【数据结构】栈和队列内容解析解说

在这里插入图片描述


一、前言

我们在学习栈和队列的内容前,我们先简单讲述一下我们计算机的内存划分,下面是我们计算机内存的简单图解:

在这里插入图片描述

如果我们读者对顺序表和链表的内容不太熟悉的话,建议先对顺序表和单链表中常见接口进行复习,因为我们栈和队列的实现与接口的实现与上述两者紧密相关

顺序表讲解:https://blog.csdn.net/weixin_52664715/article/details/120318125?spm=1001.2014.3001.5501

单链表讲解:https://blog.csdn.net/weixin_52664715/article/details/120336834?spm=1001.2014.3001.5501

栈帧的创建与销毁:https://blog.csdn.net/weixin_52664715/article/details/120395859?spm=1001.2014.3001.5501


二、栈

①栈的概念
在这里插入图片描述

栈:一种特殊的线性表,其只允许在固定的一端进行插入和删除元素操作。进行数据插入和删除操作的一端称为栈顶,另一端称为栈底。栈中的数据元素遵守后进先出LIFO(Last In First Out)的原则。

压栈:栈的插入操作叫做进栈/压栈/入栈,入数据在栈顶。
出栈:栈的删除操作叫做出栈。出数据也在栈顶。

图解:
在这里插入图片描述

在这里插入图片描述

②栈的实现

通过上面的讲述我们知道了栈的结构特点是先进后出,那么相对于我们之前学习的顺序表和链表中的增删查改操作而言,这里我们不存在对栈的头位置(即栈底:最开始的位置)头插和头删操作;

而这个时候我们思考我们学过的顺序结构:顺序表和链表,这个时候我们再对我们这两个结构进行分析:
在这里插入图片描述

1. 当我们使用链表时,因为我们是将先放入的元素作为首元素,那么这个元素就将进入我们栈帧中的栈底位置,也就是我们的首节点,而这个时候我们要实现想栈帧中插入元素或者获得栈顶元素,那么我们就需要一个指针去指向我们链表中的尾位置,也就是我们的栈顶位置,这个时候我们的效率比较低效。

2.当我们使用顺序表时,我们回顾我们顺序表中结构体的内容,一个指针去指向我们的数组,一个变量top去定义我们的数组中当前顺序表中元素的个数(栈中对应我们栈顶的位置),一个变量capacity去定义我们当前数组的最大存储容量。那么这个时候我们只需要去获得我们当前顺序表中的top,其实我们也就获得了当前栈中的栈顶位置,我们也就可以进行我们的栈帧中放进元素,以及获取栈顶元素等操作,所以当我们使用顺序表来实现我们的栈时效率更高。

综上,我们得出我们要想实现我们的栈帧,我们采用顺序表来实现
如果对顺序表的实现不太熟悉的读者,可以看一下这篇博客
https://blog.csdn.net/weixin_52664715/article/details/120318125?spm=1001.2014.3001.5501
如果比较熟悉我们顺序表的内容,那么我们在实现栈中的接口时,就会十分得心应手。


三、栈的常见接口

3.1栈的定义

在这里我们栈的结构体的定义本质上和我们的顺序表相同

//举例代码:
typedef int STDatatype;
typedef struct Stack
{
	STDatatype* a;//我们创建指针去指向我们的动态数组
	//如果我们想要定义静态数组STDatatype a[N]
	int top;//栈顶的位置
	int capacity;//栈的容量
}ST;
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9

3.2栈的初始化

这里栈的初始化依然和我们顺序表中的初始化操作相同,下面相同的步骤不在过多讲述,当有不同内容是我会进行讲解

//举例代码:
oid StackInit(ST* ps)
{
	assert(ps);

	ps->a = NULL;
	ps->top = ps->capacity = 0;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

3.3栈的销毁

我们需要将我们的动态数组进行空间释放,所以我们需要手动free释放我们的空间

//举例代码:
void StackDestory(ST* ps)
{
	assert(ps);
	if (ps->a)
	{
		free(ps->a);
	}
	ps->a = NULL;
	ps->top = ps->capacity = 0;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11

3.4栈的插入元素

这里我们的栈的插入元素和我们顺序表中的多种位置的插入元素不同,这里我们只能在栈顶位置进行元素的插入,也就是我们数组的尾位置"&a[top]"

//举例代码:
void StackPush(ST* ps, STDatatype x)
{
	assert(ps);
	//检查空间是否增容
	if (ps->top == ps->capacity)
	{
		int newcapacity = ps->capacity == 0 ? 4 : ps->capacity * 2;//三目运算:若我们的数组大小为0,我们将空间大小赋值为4个空间大小;若是我们的空间已经充满,我们将我们的空间容量扩大一倍
		STDatatype* tmp = realloc(ps->a, sizeof(STDatatype*)*newcapacity);
		if (tmp == NULL)
		{
			printf("realloc fail 
");
			exit(-1);
		}
		ps->a = tmp;
		ps->capacity = newcapacity;
	}
	ps->a[ps->top] = x;//在栈顶位置插入元素
	ps->top++; //将我们的栈的栈顶位置上移一个单位
}

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21

3.5栈的删除元素

这里我们要想实现栈的元素删除,删除的位置也同样只能是我们的栈顶位置,所以我们只需要将我们的栈顶位置下移一个单位即可

//举例代码:
void StackPop(ST* ps)
{
	assert(ps);
	assert(!StackEmpty(ps));
	--ps->top;//栈顶位置下一个单位
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

3.6栈的长度

我们只需要获取我们栈顶的位置,即就是我们的栈的长度

//举例代码:
int StackSize(ST* ps)//个数
{
	assert(ps);
	return ps->top;//错误点:top是int型的数据,并非指针后移,每当增加一个元素,top就++;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

3.7栈的判空

当我们标记我们栈顶的变量为0时,我们栈帧中没有存储任何元素,那么这个时候我们就可以判读我们的栈帧为空

//举例代码:
bool StackEmpty(ST* ps)
{
	assert(ps);
	return ps->top == 0;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

3.8栈的栈顶元素获取

当我们要实现获取我们的栈顶元素,只要找到我们数组中对应栈顶下标的元素即可

//举例代码:
STDatatype StackTop(ST* ps)//取栈顶数据
{
	assert(ps);
	assert(!StackEmpty(ps));

	return ps->a[ps->top - 1];//这个时候因为我们的数组下标从0开始,所以我们进行-1操作
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

四、队列

①队列的概念
在这里插入图片描述

队列:只允许在一端进行插入数据操作,在另一端进行删除数据操作的特殊线性表,队列具有先进先出FIFO(First In First Out) 入队列:进行插入操作的一端称为队尾 出队列:进行删除操作的一端称为队头

在这里插入图片描述

队列的使用,在我们的日常生活中是是十分常见的,比如我们在银行排队取号办理手续时,就是我们队列的具体化使用,先来的人领取一号小票,下一个人领取二号小票,一次往后,然后我们的窗口按照我们的小票顺序开始叫号进行业务工作,依次往后;

后来者排队领票办理业务,也就相当于队列插入一个元素,先来者窗口优先叫号处理,每当我们解决以为用户的业务我们的队伍就前进一个单位,也就相当于我们的队列出一个元素。
在这里插入图片描述

②队列的实现

在这里我们依然面对着我们栈中的问题,就是我们用什么结构来实现我们的队列结构,在这里我们对两种结构都进行分析
在这里插入图片描述

1.当我们使用顺序表时,这里和我们的栈不一样,栈中我们进行入栈和出栈操作时,我们操作的的位置都是我们的栈顶位置,也就是一个位置,而我们的队列中要实现我们的队列的入元素和出元素时,我们操作的位置是不同的,当我们进行出元素时,我们需要找到我们的首元素 ,而当我们要进行入元素时我们需要找到我们的尾元素,此时如果我们使用我们的顺序表实现的话,不仅位置的查找较为麻烦,同时我们可能需要进行元素的移动覆盖,这样我们的效率较为低下。

2.当我们使用链表实现是,我们此时只需要创建两个指针始终指向我们当前队列的首元素和尾位置即可,这样我们就可以快捷的实现我们的入元素和出元素的操作,所以我们选择使用链表来实现我们的队列

综上,我们得出我们要想实现我们的队列,我们采用链表来实现
如果对链表的实现不太熟悉的读者,可以看一下这篇博客
https://blog.csdn.net/weixin_52664715/article/details/120336834?spm=1001.2014.3001.5501
如果比较熟悉我们链表的内容,那么我们在实现队列中的接口时,就会十分得心应手。


五、常见接口

5.1队列的定义

在这里我们实现队列的定义时,我们先回顾我们上面的分析,我们得知我们可以采用我们的链表来实现我们的队列

此时我们需要创建两个指针来指向我们队列的收尾,但我们如果在函数接口中创建两个指针形参,使得我们的函数接口太复杂,所以这一次我们采用将两个指针创建一个结构体来看待,这样我们通过对结构体指针的解应用就可以获得我们的两个指针,使得我们的函数接口更加简洁

//举例代码:
typedef int QDataType;

typedef struct QueueNode
{
	struct QueueNode* next;
	QDataType data;
}QueueNode;

//我们对队列进行分析可知,我们在实现队列的插入元素时,需要找到当前队列的最后尾tail
//如果我们只用一个phead指针,显然不够用,所以我们要在创建一个指针tail
//这个时候我们将两个指针包在一起

typedef struct Queue
{
	QueueNode* phead;
	QueueNode* ptail;
}Queue;
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18

5.2队列的初始化

这里我们对队列的初始化和我们链表中的初始化执行步骤相同

//举例代码:
void QueueInit(Queue* pq)
{
	assert(pq);//判断我们队列的收尾指针是否为空
	pq->phead = pq->ptail = NULL;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

5.3队列的销毁

这个时候因为我们的队列中都是一个一个通过动态内存函数创建的节点,所以需要我们手动释放空间

//举例代码:
void QueueDestory(Queue* pq)
{
	assert(pq);

	QueueNode* cur = pq->phead;
	while (cur)
	{
		QueueNode* next = cur->next;
		free(cur);
		cur = next;
	}

	pq->phead = pq->ptail = NULL;

}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16

5.4队列入数据

在我们的队列中,我们要想入数据,根据队列的性质我们只能在队尾进行入数据,也就是我们ptail指针指向的节点;

同时当我进行入数据时,我们还需要创建我们新数据的节点;

//举例代码:
void QueuePush(Queue* pq, QDataType x)
{
	assert(pq);
	QueueNode* newnode = (QueueNode*)malloc(sizeof(QueueNode));//这里我们创建一个节点,也就是我们要插入的节点
	if (newnode == NULL)
	{
		printf("malloc fail
");
		exit(-1);
	}
	newnode->data = x;
	newnode->next = NULL;
	//完成我们节点的创建,已经判读是否创建成功,已经赋值

	if (pq->ptail == NULL)//这里我们判断tail和phead都一样
	{
		pq->ptail = pq->phead = newnode;
	}
	else
	{
		pq->ptail->next = newnode;//这里我们先获得的是我们的tail指针,然后我们将tail指针指向的节点的指针域存储我们新节点的地址
		pq->ptail = newnode;//同时我们将tail指针后移到新插入的节点
	}
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24

5.5队列出数据

当我们要在队列中出数据时,也就是我们最先入队列的元素,此时我们只能在队列的首元素的位置,也就是我们phead指向的位置进行操作,出数据之后,我们在链表中我们就需要将这个节点进行删除,也就是我们要进行空间的释放

//举例代码:
void QueuePop(Queue* pq)
{
	assert(pq);
	assert(!QueueEmpty(pq));

	if (pq->phead->next == NULL)
	{
		free(pq->phead);
		pq->phead = pq->ptail = NULL;
	}
	else
	{
		QueueNode* next = pq->phead->next;
		free(pq->phead);
		pq->phead = next;
	}

}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19

5.6队列的长度

在这里我们要想获得我们队列的长度,只需要用指针遍历我们的链表即可获取

//举例代码:
int QueueSize(Queue* pq)
{
	assert(pq);

	int n = 0;
	QueueNode* cur = pq->phead;
	while (cur)
	{
		n++;
		cur = cur->next;
	}
	return n;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14

5.7队列的判空

当我们的ptail指针指向NULL时,说明此时我们的队列中没有元素的存在

//举例代码:
bool QueueEmpty(Queue* pq)
{
	assert(pq);
	return pq->ptail == NULL;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

5.8队列的队尾元素获取

我们要获取我们队列的尾元素,只需要将我们ptail指针指向的元素返回即可

//举例代码:
QDataType QueueBack(Queue* pq)//获取队尾的数据
{
	assert(pq);
	assert(!QueueEmpty(pq));//保证我们的收尾指针不指向NULL

	return pq->ptail->data;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

5.9队列的队首元素获取

我们要获取我们队列的尾元素,只需要将我们phead指针指向的元素返回即可

//举例代码:
QDataType QueueFront(Queue* pq)//获取队头的元素
{
	assert(pq);
	assert(!QueueEmpty(pq));//保证我们的收尾指针不指向NULL

	return pq->phead->data;

}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9

总结

关于栈和队列内容的讲述,我暂且讲述这些,后面还有关于环形队列以及相关的OJ练习,我们会在后面持续更新讲述;在这一章节中本质上是对我们顺序表和链表的复习与再应用,所以仍需我们对顺序表和链表的基本操作熟练掌握。

以上就是我对栈和队列的个人理解

上述内容如果有错误的地方,还麻烦各位大佬指教【膜拜各位了】【膜拜各位了】
在这里插入图片描述

推荐阅读